From afb19836d6730ade5035f6f86ad90715be336af2 Mon Sep 17 00:00:00 2001 From: Michele Balistreri Date: Mon, 23 Sep 2024 11:23:29 +0200 Subject: [PATCH] Add support for certificates (#31) * add ident command * add Certificate struct * fix ident command --- Keycard.podspec | 2 +- Package.resolved | 2 +- Package.swift | 3 +- Sources/Keycard/Certificate.swift | 35 ++++++++++++++++ Sources/Keycard/Crypto.swift | 36 +++++++++++++---- Sources/Keycard/FileLoader.swift | 5 +++ Sources/Keycard/Keycard.swift | 1 + Sources/Keycard/KeycardCommandSet.swift | 7 +++- Sources/Keycard/RecoverableSignature.swift | 47 +++++++++++++++++++--- Sources/Keycard/TinyBERTLV.swift | 4 ++ 10 files changed, 125 insertions(+), 17 deletions(-) create mode 100644 Sources/Keycard/Certificate.swift diff --git a/Keycard.podspec b/Keycard.podspec index f830f60..6fb72c4 100644 --- a/Keycard.podspec +++ b/Keycard.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = 'Keycard' - spec.version = '3.0.7' + spec.version = '3.1.0' spec.authors = {'Bitgamma' => 'opensource@bitgamma.com'} spec.homepage = 'https://github.com/status-im/Keycard.swift' spec.license = { :type => 'Apache' } diff --git a/Package.resolved b/Package.resolved index 6dd0697..c30f46e 100644 --- a/Package.resolved +++ b/Package.resolved @@ -24,7 +24,7 @@ "repositoryURL": "https://github.com/status-im/secp256k1.swift.git", "state": { "branch": "master", - "revision": "d2c49786e9245d77f4eba6ce78a87f87506623c5", + "revision": "4ab977cc2b2d7319be858bcb30a5d189bb149884", "version": null } }, diff --git a/Package.swift b/Package.swift index 9cfb4ef..8614cb2 100644 --- a/Package.swift +++ b/Package.swift @@ -25,7 +25,8 @@ let package = Package( // Targets can depend on other targets in this package, and on products in packages which this package depends on. .target( name: "Keycard", - dependencies: ["secp256k1", "CryptoSwift", "ZipArchive", "BigInt"]), + dependencies: ["secp256k1", "CryptoSwift", "ZipArchive", "BigInt"], + swiftSettings: [.define("USE_SPM")]), .testTarget( name: "KeycardTests", dependencies: ["Keycard"]), diff --git a/Sources/Keycard/Certificate.swift b/Sources/Keycard/Certificate.swift new file mode 100644 index 0000000..bc05645 --- /dev/null +++ b/Sources/Keycard/Certificate.swift @@ -0,0 +1,35 @@ +enum CertificateTag: UInt8 { + case certificate = 0x8A +} + +public struct Certificate { + let identPub: [UInt8] + let caSignature: RecoverableSignature + + public static func fromTLV(certData: [UInt8]) -> Certificate { + let pub = Array(certData[0..<33]) + let r = Array(certData[33..<65]) + let s = Array(certData[65..<97]) + let recId = certData[97] + + let hash = Crypto.shared.sha256(pub) + let caPub = Crypto.shared.secp256k1RecoverPublic(r: r, s: s, recId: recId, hash: hash, compressed: true) + let caSig = RecoverableSignature(r: r, s: s, recId: recId, publicKey: caPub, compressed: true) + + return Certificate(identPub: pub, caSignature: caSig); + } + + public static func verifyIdentity(hash: [UInt8], tlvData: [UInt8]) throws -> [UInt8]? { + let tlv = TinyBERTLV(tlvData) + _ = try tlv.enterConstructed(tag: ECDSASignatureTag.signatureTemplate.rawValue) + let certData = try tlv.readPrimitive(tag: CertificateTag.certificate.rawValue) + let cert = Certificate.fromTLV(certData: certData) + let signature = tlv.peekUnread() + + if (!Crypto.shared.secp256k1Verify(signature: signature, hash: hash, pubKey: cert.identPub)) { + return nil + } + + return cert.caSignature.publicKey + } +} diff --git a/Sources/Keycard/Crypto.swift b/Sources/Keycard/Crypto.swift index d5f3e67..53fbc27 100644 --- a/Sources/Keycard/Crypto.swift +++ b/Sources/Keycard/Crypto.swift @@ -195,16 +195,16 @@ class Crypto { func secp256k1PublicFromPrivate(_ privKey: [UInt8]) -> [UInt8] { var pubKey = secp256k1_pubkey() _ = secp256k1_ec_pubkey_create(secp256k1Ctx, &pubKey, privKey) - return _secp256k1PubToBytes(&pubKey) + return _secp256k1PubToBytes(&pubKey, false) } - func secp256k1RecoverPublic(r: [UInt8], s: [UInt8], recId: UInt8, hash: [UInt8]) -> [UInt8] { + func secp256k1RecoverPublic(r: [UInt8], s: [UInt8], recId: UInt8, hash: [UInt8], compressed: Bool) -> [UInt8] { var sig = secp256k1_ecdsa_recoverable_signature() _ = secp256k1_ecdsa_recoverable_signature_parse_compact(secp256k1Ctx, &sig, r + s, Int32(recId)) var pubKey = secp256k1_pubkey() _ = secp256k1_ecdsa_recover(secp256k1Ctx, &pubKey, &sig, hash) - return _secp256k1PubToBytes(&pubKey) + return _secp256k1PubToBytes(&pubKey, compressed) } func secp256k1Sign(hash: [UInt8], privKey: [UInt8]) -> [UInt8] { @@ -217,11 +217,33 @@ class Crypto { secp256k1_ecdsa_signature_serialize_der(secp256k1Ctx, &derSig, &derOutLen, &sig) return Array(derSig[0.. Bool { + var sig = secp256k1_ecdsa_signature() + _ = secp256k1_ecdsa_signature_parse_der(secp256k1Ctx, &sig, signature, signature.count) + var signorm = secp256k1_ecdsa_signature() + _ = secp256k1_ecdsa_signature_normalize(secp256k1Ctx, &signorm, &sig) + + var pkey = secp256k1_pubkey(); + _ = secp256k1_ec_pubkey_parse(secp256k1Ctx, &pkey, pubKey, pubKey.count) + + return secp256k1_ecdsa_verify(secp256k1Ctx, &signorm, hash, &pkey) != 0 + } - private func _secp256k1PubToBytes(_ pubKey: inout secp256k1_pubkey) -> [UInt8] { - var pubKeyBytes = [UInt8](repeating: 0, count: 65) - var outputLen = 65 - _ = secp256k1_ec_pubkey_serialize(secp256k1Ctx, &pubKeyBytes, &outputLen, &pubKey, UInt32(SECP256K1_EC_UNCOMPRESSED)) + private func _secp256k1PubToBytes(_ pubKey: inout secp256k1_pubkey, _ compressed: Bool) -> [UInt8] { + var outputLen: Int + var compressedFlag: UInt32 + + if (compressed) { + outputLen = 33 + compressedFlag = UInt32(SECP256K1_EC_COMPRESSED) + } else { + outputLen = 65 + compressedFlag = UInt32(SECP256K1_EC_UNCOMPRESSED) + } + + var pubKeyBytes = [UInt8](repeating: 0, count: outputLen) + _ = secp256k1_ec_pubkey_serialize(secp256k1Ctx, &pubKeyBytes, &outputLen, &pubKey, compressedFlag) return pubKeyBytes } diff --git a/Sources/Keycard/FileLoader.swift b/Sources/Keycard/FileLoader.swift index 4258fb6..e779638 100644 --- a/Sources/Keycard/FileLoader.swift +++ b/Sources/Keycard/FileLoader.swift @@ -1,5 +1,10 @@ import Foundation + +#if USE_SPM +import ZipArchive +#else import SSZipArchive +#endif struct FileLoader { private static let blockSize = 247 // 255 - 8 bytes for MAC diff --git a/Sources/Keycard/Keycard.swift b/Sources/Keycard/Keycard.swift index f189883..188e5b6 100644 --- a/Sources/Keycard/Keycard.swift +++ b/Sources/Keycard/Keycard.swift @@ -29,6 +29,7 @@ public enum KeycardINS: UInt8 { case initialize = 0xfe case factoryReset = 0xfd case getStatus = 0xf2 + case identifyCard = 0x14 case verifyPIN = 0x20 case changePIN = 0x21 case unblockPIN = 0x22 diff --git a/Sources/Keycard/KeycardCommandSet.swift b/Sources/Keycard/KeycardCommandSet.swift index e2651b1..7f25083 100644 --- a/Sources/Keycard/KeycardCommandSet.swift +++ b/Sources/Keycard/KeycardCommandSet.swift @@ -61,6 +61,11 @@ public class KeycardCommandSet { public func unpairOthers() throws { try secureChannel.unpairOthers(channel: cardChannel) } + + public func identifyCard(challenge: [UInt8]) throws -> APDUResponse { + let cmd = APDUCommand(cla: CLA.proprietary.rawValue, ins: KeycardINS.identifyCard.rawValue, p1: 0x00, p2: 0x00, data: challenge) + return try cardChannel.send(cmd) + } public func openSecureChannel(index: UInt8, data: [UInt8]) throws -> APDUResponse { try secureChannel.openSecureChannel(channel: cardChannel, index: index, data: data) @@ -302,5 +307,5 @@ public class KeycardCommandSet { public func factoryReset() throws -> APDUResponse { let cmd = APDUCommand(cla: CLA.proprietary.rawValue, ins: KeycardINS.factoryReset.rawValue, p1: FactoryResetP1.magic.rawValue, p2: FactoryResetP2.magic.rawValue, data: []) return try cardChannel.send(cmd) - } + } } diff --git a/Sources/Keycard/RecoverableSignature.swift b/Sources/Keycard/RecoverableSignature.swift index e55313c..8a6aa95 100644 --- a/Sources/Keycard/RecoverableSignature.swift +++ b/Sources/Keycard/RecoverableSignature.swift @@ -1,5 +1,6 @@ enum ECDSASignatureTag: UInt8 { case signatureTemplate = 0xA0 + case rawSignature = 0x80 case ecdsaTemplate = 0x30 } @@ -8,29 +9,63 @@ public struct RecoverableSignature { public let recId: UInt8 public let r: [UInt8] public let s: [UInt8] + public let compressed: Bool + + public init(r: [UInt8], s: [UInt8], recId: UInt8, publicKey: [UInt8], compressed: Bool) { + self.r = r + self.s = s + self.recId = recId + self.publicKey = publicKey + self.compressed = compressed + } public init(hash: [UInt8], data: [UInt8]) throws { let tlv = TinyBERTLV(data) + let tag = try tlv.readTag() + tlv.unreadLastTag() + + if (tag == ECDSASignatureTag.rawSignature.rawValue) { + try self.init(hash: hash, signature: tlv.readPrimitive(tag: tag)) + } else if (tag == ECDSASignatureTag.signatureTemplate.rawValue) { + try self.init(hash: hash, tlv: tlv) + } else { + throw TLVError.unexpectedTag(expected: ECDSASignatureTag.signatureTemplate.rawValue, actual: tag) + } + } + + private init(hash: [UInt8], tlv: TinyBERTLV) throws { _ = try tlv.enterConstructed(tag: ECDSASignatureTag.signatureTemplate.rawValue) self.publicKey = try tlv.readPrimitive(tag: AppInfoTag.pubKey.rawValue) _ = try tlv.enterConstructed(tag: ECDSASignatureTag.ecdsaTemplate.rawValue) self.r = try Util.shared.dropZeroPrefix(uint8: tlv.readPrimitive(tag: TLVTag.int.rawValue)) self.s = try Util.shared.dropZeroPrefix(uint8: tlv.readPrimitive(tag: TLVTag.int.rawValue)) - + self.compressed = false + self.recId = try RecoverableSignature.calculateRecId(hash: hash, pubkey: self.publicKey, r: self.r, s: self.s, compressed: self.compressed) + } + + private init(hash: [UInt8], signature: [UInt8]) throws { + self.r = Array(signature[0..<32]) + self.s = Array(signature[32..<64]) + self.recId = signature[64] + self.compressed = false + self.publicKey = Crypto.shared.secp256k1RecoverPublic(r: self.r, s: self.s, recId: self.recId, hash: hash, compressed: self.compressed) + } + + public static func calculateRecId(hash: [UInt8], pubkey: [UInt8], r: [UInt8], s: [UInt8], compressed: Bool) throws -> UInt8 { var foundID: UInt8 = UInt8.max for i: UInt8 in 0...3 { - let pub = Crypto.shared.secp256k1RecoverPublic(r: r, s: s, recId: i, hash: hash) - if (pub == self.publicKey) { + let pub = Crypto.shared.secp256k1RecoverPublic(r: r, s: s, recId: i, hash: hash, compressed: compressed) + if (pub == pubkey) { foundID = i break } } - if (foundID != UInt8.max) { - self.recId = foundID - } else { + if (foundID == UInt8.max) { throw CardError.unrecoverableSignature } + + return foundID } } diff --git a/Sources/Keycard/TinyBERTLV.swift b/Sources/Keycard/TinyBERTLV.swift index bbf195e..3117caa 100644 --- a/Sources/Keycard/TinyBERTLV.swift +++ b/Sources/Keycard/TinyBERTLV.swift @@ -113,6 +113,10 @@ class TinyBERTLV { throw TLVError.unexpectedLength(length: val.count) } } + + func peekUnread() -> [UInt8] { + return Array(self.buf[self.pos.. Int { var len = Int(buf[pos])