Skip to content

Commit

Permalink
feat: Add ability to generate keys within BlueECC (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
Andrew-Lees11 committed Mar 12, 2019
1 parent 04e69ff commit 5c5a4a2
Show file tree
Hide file tree
Showing 5 changed files with 382 additions and 54 deletions.
32 changes: 22 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,26 @@ import CryptorECC

#### Elliptic curve private key

Generate your elliptic curve private key using a third party provider:
you can generate an ECPrivate key using BlueECC.

You can generate a `p-256` private key as a `.p8` file for Apple services from [https://developer.apple.com/account/ios/authkey](https://developer.apple.com/account/ios/authkey/). This will produce a key that should be formatted as follows:
```swift
let p256PrivateKey = try ECPrivateKey.make(for: .prime256v1)
```

You can then view the key in it's PEM format as follows:

```swift
let privateKeyPEM = p256PrivateKey.pemString
```

The following curves are supported:
- prime256v1
- secp384r1
- secp521r1

Alternatively, you may generate private key using a third party provider:

- You can generate a `p-256` private key as a `.p8` file for Apple services from [https://developer.apple.com/account/ios/authkey](https://developer.apple.com/account/ios/authkey/). This will produce a key that should be formatted as follows:
```swift
let privateKey =
"""
Expand All @@ -68,12 +85,7 @@ PyniQCWG+Agc3bdcgKU0RKApWYuBJKrZqyqLB2tTlgdtwcWSB0AEzVI8
"""
```

Alternatively, you can use OpenSSL [Command Line Elliptic Curve Operations](https://wiki.openssl.org/index.php/Command_Line_Elliptic_Curve_Operations).
OpenSSL can be installed via brew:

```
$ brew install openssl
```
- You can use OpenSSL [Command Line Elliptic Curve Operations](https://wiki.openssl.org/index.php/Command_Line_Elliptic_Curve_Operations).

The following commands generate private keys for the three supported curves as `.pem` files:
```
Expand All @@ -96,14 +108,14 @@ KVmLgSSq2asqiwdrU5YHbcHFkgdABM1SPA==
"""
```

The key can then be used to initialize an `ECPrivateKey` instance:
The key string can then be used to initialize an `ECPrivateKey` instance:
```swift
let eccPrivateKey = try ECPrivateKey(key: privateKey)
```

#### Elliptic curve public key

Use OpenSSL to generate an elliptic curve public key `.pem` file from any of the above elliptic curve private key files:
You can use OpenSSL to generate an elliptic curve public key `.pem` file from any of the above elliptic curve private key files:
```
$ openssl ec -in key.pem -pubout -out public.pem
```
Expand Down
223 changes: 220 additions & 3 deletions Sources/CryptorECC/ECPrivateKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Foundation
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
import CommonCrypto
Expand Down Expand Up @@ -51,6 +50,9 @@ public class ECPrivateKey {
/// The `EllipticCurve` this key was generated from.
public let curve: EllipticCurve

/// The private key represented as a PEM String.
public let pemString: String

#if os(Linux)
typealias NativeKey = OpaquePointer?
deinit { EC_KEY_free(.make(optional: self.nativeKey)) }
Expand All @@ -59,6 +61,7 @@ public class ECPrivateKey {
#endif
let nativeKey: NativeKey
let pubKeyBytes: Data
private var stripped: Bool = false


/**
Expand Down Expand Up @@ -147,9 +150,16 @@ public class ECPrivateKey {
throw ECError.failedASN1Decoding
}
let trimmedPubBytes = publicKeyData.drop(while: { $0 == 0x00})
if trimmedPubBytes.count != publicKeyData.count {
stripped = true
}
self.nativeKey = try ECPrivateKey.bytesToNativeKey(privateKeyData: privateKeyData,
publicKeyData: trimmedPubBytes,
curve: curve)
let derData = ECPrivateKey.generateASN1(privateKey: privateKeyData,
publicKey: publicKeyData,
curve: curve)
self.pemString = ECPrivateKey.derToPrivatePEM(derData: derData)
self.pubKeyBytes = trimmedPubBytes
self.curveId = curve.description
}
Expand All @@ -161,6 +171,7 @@ public class ECPrivateKey {
/// - Returns: An ECPrivateKey.
/// - Throws: An ECError if the Data can't be decoded or is not a valid key.
public init(sec1DER: Data) throws {
self.pemString = ECPrivateKey.derToPrivatePEM(derData: sec1DER)
let (result, _) = ASN1.toASN1Element(data: sec1DER)
guard case let ASN1.ASN1Element.seq(elements: seq) = result,
seq.count > 3,
Expand All @@ -177,6 +188,9 @@ public class ECPrivateKey {
throw ECError.failedASN1Decoding
}
let trimmedPubBytes = publicKeyData.drop(while: { $0 == 0x00})
if trimmedPubBytes.count != publicKeyData.count {
stripped = true
}
self.nativeKey = try ECPrivateKey.bytesToNativeKey(privateKeyData: privateKeyData,
publicKeyData: trimmedPubBytes,
curve: curve)
Expand Down Expand Up @@ -215,13 +229,206 @@ public class ECPrivateKey {
}
// If we stripped the leading zero earlier, add it back here
var pubBytes = self.pubKeyBytes
if pubBytes.startIndex != 0 {
if stripped {
pubBytes = Data(count: 1) + self.pubKeyBytes
}
return try ECPublicKey(der: keyHeader + pubBytes)
}

/**
Make an new ECPrivate key from a supported `EllipticCurve`.
- Parameter for curve: The elliptic curve that is used to generate the key.
- Returns: An ECPrivateKey.
- Throws: An ECError if the key fails to be created.
*/
public static func make(for curve: EllipticCurve) throws -> ECPrivateKey {
return try ECPrivateKey(for: curve)
}


/**
Initialise an new ECPrivate key from a supported `Curve`
- Parameter for curve: The elliptic curve that is used to generate the key.
- Returns: An ECPrivateKey.
- Throws: An ECError if the key fails to be created.
*/
private init(for curve: EllipticCurve) throws {
self.curve = curve
self.curveId = curve.description
self.stripped = true
#if os(Linux)
let ec_key = EC_KEY_new_by_curve_name(curve.nativeCurve)
EC_KEY_generate_key(ec_key)
self.nativeKey = ec_key
let pub_bn_ctx = BN_CTX_new()
BN_CTX_start(pub_bn_ctx)
let pub = EC_KEY_get0_public_key(ec_key)
let ec_group = EC_KEY_get0_group(ec_key)
let pub_bn = BN_new()
EC_POINT_point2bn(ec_group, pub, POINT_CONVERSION_UNCOMPRESSED, pub_bn, pub_bn_ctx)
let pubk = UnsafeMutablePointer<UInt8>.allocate(capacity: curve.keySize)
BN_bn2bin(pub_bn, pubk)
self.pubKeyBytes = Data(bytes: pubk, count: curve.keySize)
defer {
BN_CTX_end(pub_bn_ctx)
BN_CTX_free(pub_bn_ctx)
BN_clear_free(pub_bn)
#if swift(>=4.1)
pubk.deallocate()
#else
pubk.deallocate(capacity: curve.keySize)
#endif
}
#else
let kAsymmetricCryptoManagerKeyType = kSecAttrKeyTypeECSECPrimeRandom
let kAsymmetricCryptoManagerKeySize: Int
if curve == .prime256v1 {
kAsymmetricCryptoManagerKeySize = 256
} else if curve == .secp384r1 {
kAsymmetricCryptoManagerKeySize = 384
} else {
kAsymmetricCryptoManagerKeySize = 521
}
// parameters
let parameters: [String: AnyObject] = [
kSecAttrKeyType as String: kAsymmetricCryptoManagerKeyType,
kSecAttrKeySizeInBits as String: kAsymmetricCryptoManagerKeySize as AnyObject,
]
var pubKey, privKey: SecKey?
let status = SecKeyGeneratePair(parameters as CFDictionary, &pubKey, &privKey)
guard status == 0, let newPubKey = pubKey, let newPrivKey = privKey else {
throw ECError.failedNativeKeyCreation
}
var error: Unmanaged<CFError>? = nil
guard let pubBytes = SecKeyCopyExternalRepresentation(newPubKey, &error) else {
guard let error = error?.takeRetainedValue() else {
throw ECError.failedNativeKeyCreation
}
throw error
}
self.pubKeyBytes = pubBytes as Data
self.nativeKey = newPrivKey
#endif
self.pemString = try ECPrivateKey.decodeToPEM(nativeKey: self.nativeKey, curve: self.curve)
}

/// Decode this ECPrivateKey to it's PEM format
private static func decodeToPEM(nativeKey: NativeKey, curve: EllipticCurve) throws -> String {
#if os(Linux)
let asn1Bio = BIO_new(BIO_s_mem())
defer { BIO_free_all(asn1Bio) }
// The return value of i2d_ECPrivateKey_bio is supposed to be the DER size.
// However it is just returning 1 for success.
// Since the size is fixed we have just used the known values here.
guard i2d_ECPrivateKey_bio(asn1Bio, nativeKey) >= 0 else {
throw ECError.failedNativeKeyCreation
}
let asn1Size: Int32
if curve == .prime256v1 {
asn1Size = 364
} else if curve == .secp384r1 {
asn1Size = 510
} else {
asn1Size = 673
}
let asn1 = UnsafeMutablePointer<UInt8>.allocate(capacity: Int(asn1Size))
let readLength = BIO_read(asn1Bio, asn1, asn1Size)
guard readLength > 0 else {
throw ECError.failedASN1Decoding
}
let asn1Data = Data(bytes: asn1, count: Int(readLength))
// OpenSSL 1.1 already returns the shortened ANS1 so can return it straight away
if readLength < asn1Size - 1 {
return ECPrivateKey.derToPrivatePEM(derData: asn1Data)
}
// Otherwise need to decode ASN1 to get public and private key
#if swift(>=4.1)
asn1.deallocate()
#else
asn1.deallocate(capacity: Int(asn1Size))
#endif
let (result, _) = ASN1.toASN1Element(data: asn1Data)
guard case let ASN1.ASN1Element.seq(elements: seq) = result,
seq.count > 3,
case let ASN1.ASN1Element.bytes(data: privateKeyData) = seq[1]
else {
throw ECError.failedASN1Decoding
}
guard case let ASN1.ASN1Element.constructed(tag: _, elem: publicElement) = seq[3],
case let ASN1.ASN1Element.bytes(data: publicKeyData) = publicElement
else {
throw ECError.failedASN1Decoding
}
#else
var error: Unmanaged<CFError>? = nil
/*
From Apple docs:
For an elliptic curve private key, `SecKeyCopyExternalRepresentation` output is formatted as the public key concatenated with the big endian encoding of the secret scalar, or 04 || X || Y || K.
*/
guard let keyBytes = SecKeyCopyExternalRepresentation(nativeKey, &error) else {
guard let error = error?.takeRetainedValue() else {
throw ECError.failedNativeKeyCreation
}
throw error
}
let keyData = keyBytes as Data
let privateKeyData = keyData.dropFirst(curve.keySize)
let publicKeyData = Data(bytes: [0x00]) + keyData.dropLast(keyData.count - curve.keySize)
#endif
let derData = ECPrivateKey.generateASN1(privateKey: privateKeyData, publicKey: publicKeyData, curve: curve)
return ECPrivateKey.derToPrivatePEM(derData: derData)
}

private static func generateASN1(privateKey: Data, publicKey: Data, curve: EllipticCurve) -> Data {
var keyHeader: Data
// Add the ASN1 header for the private key. The bytes have the following structure:
// SEQUENCE (4 elem)
// INTEGER 1
// OCTET STRING (32 byte) (This is the `privateKeyBytes`)
// [0] (1 elem)
// OBJECT IDENTIFIER
// [1] (1 elem)
// BIT STRING (This is the `pubKeyBytes`)
if curve == .prime256v1 {
keyHeader = Data(bytes: [0x30, 0x77,
0x02, 0x01, 0x01,
0x04, 0x20])
keyHeader += privateKey
keyHeader += Data(bytes: [0xA0,
0x0A, 0x06, 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07,
0xA1,
0x44, 0x03, 0x42])
keyHeader += publicKey
} else if curve == .secp384r1 {
keyHeader = Data(bytes: [0x30, 0x81, 0xA4,
0x02, 0x01, 0x01,
0x04, 0x30])
keyHeader += privateKey
keyHeader += Data(bytes: [0xA0,
0x07, 0x06, 0x05, 0x2B, 0x81, 0x04, 0x00, 0x22,
0xA1,
0x64, 0x03, 0x62])
keyHeader += publicKey
} else {
// 521 Private key can be 65 or 66 bytes long
if privateKey.count == 65 {
keyHeader = Data(bytes: [0x30, 0x81, 0xDB,
0x02, 0x01, 0x01,
0x04, 0x41])
} else {
keyHeader = Data(bytes: [0x30, 0x81, 0xDC,
0x02, 0x01, 0x01,
0x04, 0x42])
}
keyHeader += privateKey
keyHeader += Data(bytes: [0xA0,
0x07, 0x06, 0x05, 0x2B, 0x81, 0x04, 0x00, 0x23,
0xA1,
0x81, 0x89, 0x03, 0x81, 0x86])
keyHeader += publicKey
}
return keyHeader
}

private static func bytesToNativeKey(privateKeyData: Data, publicKeyData: Data, curve: EllipticCurve) throws -> NativeKey {
#if os(Linux)
let bigNum = BN_new()
Expand Down Expand Up @@ -254,4 +461,14 @@ public class ECPrivateKey {
return secKey
#endif
}

private static func derToPrivatePEM(derData: Data) -> String {
// First convert the DER data to a base64 string...
let base64String = derData.base64EncodedString()
// Split the string into strings of length 64.
let lines = base64String.split(to: 64)
// Join those lines with a new line...
let joinedLines = lines.joined(separator: "\n")
return "-----BEGIN EC PRIVATE KEY-----\n" + joinedLines + "\n-----END EC PRIVATE KEY-----"
}
}
Loading

0 comments on commit 5c5a4a2

Please sign in to comment.