// Copyright © 2019 IBM. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // 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 #elseif os(Linux) import OpenSSL #endif /** Represents an elliptic curve public key. Supported curves are: - prime256v1 - secp384r1 - NID_secp521r1 You can generate an elliptic curve Key using OpenSSL: https://wiki.openssl.org/index.php/Command_Line_Elliptic_Curve_Operations#Generating_EC_Keys_and_Parameters ### Usage Example: ```swift let pemKey = """ -----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEikc5m6C2xtDWeeAeT18WElO37zvF Oz8p4kAlhvgIHN23XIClNESgKVmLgSSq2asqiwdrU5YHbcHFkgdABM1SPA== -----END PUBLIC KEY----- """ let publicKey = try ECPublicKey(key: pemKey) let base64Sig = "MEYCIQCvgBLn+tQoBDBR3D2G3485GloYGNxuk6PqR4qjr5GDqAIhAKNvsqvesVBD/MLub/KAyzLLNGtUZyQDxYZj/4vmHwWF" let signature = try ECSignature(asn1: Data(base64Encoded: base64Sig)) let verified = signature.verify(plaintext: "Hello world", using: publicKey) ``` */ @available(macOS 10.13, iOS 11, watchOS 4.0, tvOS 11.0, *) public class ECPublicKey { /// A String description of the curve this key was generated from. public let curveId: String /// The `EllipticCurve` this key was generated from. public let curve: EllipticCurve #if os(Linux) typealias NativeKey = OpaquePointer? let pubKeyBytes: Data deinit { EC_KEY_free(.make(optional: self.nativeKey)) } #else typealias NativeKey = SecKey #endif let nativeKey: NativeKey /// The public key represented as a PEM String. public let pemString: String /** Initialize an ECPublicKey from a `.pem` file format. ### Usage Example: ### ```swift let publicKeyString = """ -----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEikc5m6C2xtDWeeAeT18WElO37zvF Oz8p4kAlhvgIHN23XIClNESgKVmLgSSq2asqiwdrU5YHbcHFkgdABM1SPA== -----END PUBLIC KEY----- """ let pemKey = try ECPublicKey(key: publicKeyString) ``` - Parameter key: The elliptic curve public key as a PEM string. - Returns: An ECPublicKey. - Throws: An ECError if the PEM string can't be decoded or is not a valid key. */ public convenience init(key: String) throws { let strippedKey = String(key.filter { !" \n\t\r".contains($0) }) let pemComponents = strippedKey.components(separatedBy: "-----") guard pemComponents.count == 5 else { throw ECError.invalidPEMString } guard let der = Data(base64Encoded: pemComponents[2]) else { throw ECError.invalidPEMString } if pemComponents[1] == "BEGINPUBLICKEY" { try self.init(der: der) } else { throw ECError.unknownPEMHeader } } /// Initialize an ECPublicKey from `.der` file data. /// This is equivalent to a PEM String that has had the "-----BEGIN PUBLIC KEY-----" /// header and footer stripped and been base64 encoded to ASN1 Data. /// - Parameter der: The elliptic curve public key Data. /// - Returns: An ECPublicKey. /// - Throws: An ECError if the Data can't be decoded or is not a valid key. public init(der: Data) throws { pemString = ECPublicKey.derToPublicPEM(derData: der) let (result, _) = ASN1.toASN1Element(data: der) guard case let ASN1.ASN1Element.seq(elements: seq) = result, seq.count > 1, case let ASN1.ASN1Element.seq(elements: ids) = seq[0], ids.count > 1, case let ASN1.ASN1Element.bytes(data: privateKeyID) = ids[1], case let ASN1.ASN1Element.bytes(data: publicKeyData) = seq[1] else { throw ECError.failedASN1Decoding } self.curve = try EllipticCurve.objectToCurve(ObjectIdentifier: privateKeyID) self.curveId = curve.description let keyData = publicKeyData.drop(while: { $0 == 0x00}) #if os(Linux) self.pubKeyBytes = keyData let bigNum = BN_new() defer { BN_free(bigNum) } publicKeyData.withUnsafeBytes({ (pubKeyBytes: UnsafeRawBufferPointer) -> Void in BN_bin2bn(pubKeyBytes.baseAddress?.assumingMemoryBound(to: UInt8.self), Int32(publicKeyData.count), bigNum) }) let ecKey = EC_KEY_new_by_curve_name(curve.nativeCurve) let ecGroup = EC_KEY_get0_group(ecKey) let ecPoint = EC_POINT_new(ecGroup) defer { EC_POINT_free(ecPoint) } EC_POINT_bn2point(ecGroup, bigNum, ecPoint, nil) guard EC_KEY_set_public_key(ecKey, ecPoint) == 1 else { EC_KEY_free(ecKey) throw ECError.failedNativeKeyCreation } self.nativeKey = ecKey #else var error: Unmanaged? = nil guard let secKey = SecKeyCreateWithData(keyData as CFData, [kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom, kSecAttrKeyClass: kSecAttrKeyClassPublic] as CFDictionary, &error) else { if let secError = error?.takeRetainedValue() { throw secError } else { throw ECError.failedNativeKeyCreation } } self.nativeKey = secKey #endif } private static func derToPublicPEM(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 PUBLIC KEY-----\n" + joinedLines + "\n-----END PUBLIC KEY-----" } }