diff --git a/PatientScannerDemo.xcodeproj/project.pbxproj b/PatientScannerDemo.xcodeproj/project.pbxproj index 72e86a0..7f197b0 100644 --- a/PatientScannerDemo.xcodeproj/project.pbxproj +++ b/PatientScannerDemo.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + CE1BDF99262A4CD600766F97 /* X509.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1BDF98262A4CD600766F97 /* X509.swift */; }; CE3CC93C2628A7820079FB78 /* ASN1.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3CC93B2628A7820079FB78 /* ASN1.swift */; }; CE3CC9442628C2130079FB78 /* CBOR.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3CC9432628C2130079FB78 /* CBOR.swift */; }; CE7DE7FA2625EF18007E6694 /* SwiftCBOR in Frameworks */ = {isa = PBXBuildFile; productRef = CE7DE7F92625EF18007E6694 /* SwiftCBOR */; }; @@ -21,7 +22,7 @@ CEC2C4C22625ED030056E406 /* ZLib.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC2C4BF2625ED030056E406 /* ZLib.swift */; }; CEC2C4C32625ED030056E406 /* JWK.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC2C4C02625ED030056E406 /* JWK.swift */; }; CEC2C4C42625ED030056E406 /* Base45.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC2C4C12625ED030056E406 /* Base45.swift */; }; - CEFAD86D2625F164009AFEF9 /* EC256.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEFAD86C2625F164009AFEF9 /* EC256.swift */; }; + CEFAD86D2625F164009AFEF9 /* Signature.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEFAD86C2625F164009AFEF9 /* Signature.swift */; }; CEFAD8722625F29E009AFEF9 /* String+JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEFAD8712625F29E009AFEF9 /* String+JSON.swift */; }; CEFAD87A26271414009AFEF9 /* COSE.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEFAD87926271414009AFEF9 /* COSE.swift */; }; CEFAD87F262714C4009AFEF9 /* EHNTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEFAD87E262714C4009AFEF9 /* EHNTests.swift */; }; @@ -46,6 +47,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + CE1BDF98262A4CD600766F97 /* X509.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = X509.swift; sourceTree = ""; }; CE3CC93B2628A7820079FB78 /* ASN1.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ASN1.swift; sourceTree = ""; }; CE3CC9432628C2130079FB78 /* CBOR.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CBOR.swift; sourceTree = ""; }; CEA6D6E8261F8D2700715333 /* PatientScannerDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PatientScannerDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -65,7 +67,7 @@ CEC2C4BF2625ED030056E406 /* ZLib.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZLib.swift; sourceTree = ""; }; CEC2C4C02625ED030056E406 /* JWK.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JWK.swift; sourceTree = ""; }; CEC2C4C12625ED030056E406 /* Base45.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Base45.swift; sourceTree = ""; }; - CEFAD86C2625F164009AFEF9 /* EC256.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EC256.swift; sourceTree = ""; }; + CEFAD86C2625F164009AFEF9 /* Signature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Signature.swift; sourceTree = ""; }; CEFAD8712625F29E009AFEF9 /* String+JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+JSON.swift"; sourceTree = ""; }; CEFAD87926271414009AFEF9 /* COSE.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = COSE.swift; sourceTree = ""; }; CEFAD87E262714C4009AFEF9 /* EHNTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EHNTests.swift; sourceTree = ""; }; @@ -132,11 +134,12 @@ CEA6D6F4261F8D2900715333 /* Assets.xcassets */, CEA6D6F6261F8D2900715333 /* LaunchScreen.storyboard */, CEA6D6F9261F8D2900715333 /* Info.plist */, - CEFAD86C2625F164009AFEF9 /* EC256.swift */, + CEFAD86C2625F164009AFEF9 /* Signature.swift */, CE3CC93B2628A7820079FB78 /* ASN1.swift */, CEFAD87926271414009AFEF9 /* COSE.swift */, CEFAD88626271B9A009AFEF9 /* Data+hexString.swift */, CE3CC9432628C2130079FB78 /* CBOR.swift */, + CE1BDF98262A4CD600766F97 /* X509.swift */, ); path = PatientScannerDemo; sourceTree = ""; @@ -300,10 +303,11 @@ CEC2C4C32625ED030056E406 /* JWK.swift in Sources */, CEC2C4C42625ED030056E406 /* Base45.swift in Sources */, CE3CC9442628C2130079FB78 /* CBOR.swift in Sources */, + CE1BDF99262A4CD600766F97 /* X509.swift in Sources */, CEA6D6F0261F8D2700715333 /* ViewController.swift in Sources */, CE3CC93C2628A7820079FB78 /* ASN1.swift in Sources */, CEFAD87A26271414009AFEF9 /* COSE.swift in Sources */, - CEFAD86D2625F164009AFEF9 /* EC256.swift in Sources */, + CEFAD86D2625F164009AFEF9 /* Signature.swift in Sources */, CEA6D6EC261F8D2700715333 /* AppDelegate.swift in Sources */, CEFAD8722625F29E009AFEF9 /* String+JSON.swift in Sources */, CEC2C4C22625ED030056E406 /* ZLib.swift in Sources */, diff --git a/PatientScannerDemo/CBOR.swift b/PatientScannerDemo/CBOR.swift index ceb750b..02ae8b2 100644 --- a/PatientScannerDemo/CBOR.swift +++ b/PatientScannerDemo/CBOR.swift @@ -50,7 +50,7 @@ struct CBOR { #else return nil #endif - return Data(hexString: str)?.uint ?? str.encode() + return Data(hexString: str)?.uint ?? str.data(using: .utf8)?.uint case let .byteString(uint): return uint default: diff --git a/PatientScannerDemo/COSE.swift b/PatientScannerDemo/COSE.swift index 6e470d9..5977616 100644 --- a/PatientScannerDemo/COSE.swift +++ b/PatientScannerDemo/COSE.swift @@ -17,6 +17,14 @@ struct COSE { } return verify(cbor, with: xHex, and: yHex) } + public static func verify(_ cborData: Data, with rsa: String) -> Bool { + let decoder = SwiftCBOR.CBORDecoder(input: cborData.uint) + + guard let cbor = try? decoder.decodeItem() else { + return false + } + return verify(cbor, with: rsa) + } public static func verify(_ cbor: SwiftCBOR.CBOR, with xHex: String, and yHex: String) -> Bool { let COSE_TAG = UInt64(18) @@ -42,6 +50,33 @@ struct COSE { guard let key = JWK.ecFrom(x: xHex, y: yHex) else { return false } - return EC256.verify(signature: s, for: d, with: key) + return Signature.verify(s, for: d, with: key) + } + public static func verify(_ cbor: SwiftCBOR.CBOR, with rsa: String) -> Bool { + let COSE_TAG = UInt64(18) + + guard + case let SwiftCBOR.CBOR.tagged(tag, cborElement) = cbor, + tag.rawValue == COSE_TAG, // SIGN1 + case let SwiftCBOR.CBOR.array(array) = cborElement, + case let SwiftCBOR.CBOR.byteString(signature) = array[3] + else { + return false + } + + let signedPayload: [UInt8] = SwiftCBOR.CBOR.encode( + [ + "Signature1", + array[0], + SwiftCBOR.CBOR.byteString([]), + array[2] + ] + ) + let d = Data(signedPayload) + let s = Data(signature) + guard let key = X509.rsa(from: rsa) else { + return false + } + return Signature.verify(s, for: d, with: key) } } diff --git a/PatientScannerDemo/EC256.swift b/PatientScannerDemo/EC256.swift deleted file mode 100644 index 55e6ece..0000000 --- a/PatientScannerDemo/EC256.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// EC256.swift -// PatientScannerDemo -// -// Created by Yannick Spreen on 4/13/21. -// -// https://developer.apple.com/forums/thread/83136 -// - -import Foundation - -struct EC256 { - public static func verify(signature: Data, for data: Data, with publicKey: SecKey) -> Bool { - guard SecKeyIsAlgorithmSupported(publicKey, .verify, .ecdsaSignatureMessageX962SHA256) else { - print("Pubkey not supported.") - return false - } - - let sig = ASN1.signature(from: signature) - - // verify signature - var error: Unmanaged? - let result = SecKeyVerifySignature( - publicKey, - .ecdsaSignatureMessageX962SHA256, - data as NSData, - sig as NSData, - &error - ) - if let err = error?.takeUnretainedValue().localizedDescription { - print(err) - } - error?.release() - - return result - } -} diff --git a/PatientScannerDemo/Signature.swift b/PatientScannerDemo/Signature.swift new file mode 100644 index 0000000..467decb --- /dev/null +++ b/PatientScannerDemo/Signature.swift @@ -0,0 +1,60 @@ +// +// EC256.swift +// PatientScannerDemo +// +// Created by Yannick Spreen on 4/13/21. +// +// https://developer.apple.com/forums/thread/83136 +// + +import Foundation + +struct Signature { + public static func verify(_ signature: Data, for data: Data, with publicKey: SecKey) -> Bool { + if SecKeyIsAlgorithmSupported(publicKey, .verify, .ecdsaSignatureMessageX962SHA256) { + return verifyEC(signature, for: data, with: publicKey) + } + if SecKeyIsAlgorithmSupported(publicKey, .verify, .rsaSignatureMessagePSSSHA256) { + return verifyRSA(signature, for: data, with: publicKey) + } + return false + } + + static func verifyEC(_ signature: Data, for data: Data, with publicKey: SecKey) -> Bool { + let sig = ASN1.signature(from: signature) + + var error: Unmanaged? + let result = SecKeyVerifySignature( + publicKey, + .ecdsaSignatureMessageX962SHA256, + data as NSData, + sig as NSData, + &error + ) + if let err = error?.takeUnretainedValue().localizedDescription { + print(err) + } + error?.release() + + return result + } + + static func verifyRSA(_ signature: Data, for data: Data, with publicKey: SecKey) -> Bool { + let sig = signature + + var error: Unmanaged? + let result = SecKeyVerifySignature( + publicKey, + .rsaSignatureMessagePSSSHA256, + data as NSData, + sig as NSData, + &error + ) + if let err = error?.takeUnretainedValue().localizedDescription { + print(err) + } + error?.release() + + return result + } +} diff --git a/PatientScannerDemo/X509.swift b/PatientScannerDemo/X509.swift new file mode 100644 index 0000000..a8c37e0 --- /dev/null +++ b/PatientScannerDemo/X509.swift @@ -0,0 +1,21 @@ +// +// X509.swift +// PatientScannerDemo +// +// Created by Yannick Spreen on 4/17/21. +// + +import Foundation + +struct X509 { + public static func rsa(from encodedCert: String) -> SecKey? { + guard + let encodedCertData = Data(base64Encoded: encodedCert), + let cert = SecCertificateCreateWithData(nil, encodedCertData as CFData), + let publicKey = SecCertificateCopyKey(cert) + else { + return nil + } + return publicKey + } +} diff --git a/PatientScannerDemoTests/EHNTests.swift b/PatientScannerDemoTests/EHNTests.swift index 55218be..1295814 100644 --- a/PatientScannerDemoTests/EHNTests.swift +++ b/PatientScannerDemoTests/EHNTests.swift @@ -10,8 +10,7 @@ import XCTest class EHNTests: XCTestCase { - func test_cose() throws { - + func testCoseEcdsa() throws { var barcode = "HC1NCFY70R30FFWTWGSLKC 4O992$V M63TMF2V*D9LPC.3EHPCGEC27B72VF/347O4-M6Y9M6FOYG4ILDEI8GR3ZI$15MABL:E9CVBGEEWRMLE C39S0/ANZ52T82Z-73D63P1U 1$PKC 72H2XX09WDH889V5" let trustJson = """ @@ -74,6 +73,96 @@ class EHNTests: XCTestCase { print("Nope - all failed - sadness all around") XCTAssert(false) } + func testCoseATCose() throws { + var barcode = "HC1NCFI.L3B6AP2YQ2%MNBCHC51/CAOZDL+OL7S99U60SVOGSHD%24R4PCTH7Y9KL6EVJBIMBL364D X1KKLZ*I9K4RLDR7B011EROZS6022WR5VX4QG9W72HWCC12B:TWZG.%N8GWBCOV0O%5F9U7H 8P4L.%2/:6TXB/-J6ZJ.*QYIBYSD6/T.6RDMMK0PX6HTPO1REO*CWMFG5CJ38+FBS+8DB9*2FFP9Z8HEM137CRBQ.893$I.R7 DRFOVMUQRVDP4IU613/G-0LZ:43+MM:QO.CQPJU21-7PJ*L/*DVLO:Q5G-7%WF4ZO*JJR:KH+O1707M6.73VXG0+0050+IMIFH:3V+DL3JK/21/LEI$T5TUT8P:40P10AXGBRS%-GZS8TGF-IT3-HU2SVJUAVG%AGO98R00Y.QT23DVP0TB*-J6YJP 8TU9U/OJSQGGMK+K3MH IR U7H%9/UU07AGEWMVTOBAYIV4SPGC2Y5FJJ67I4/J4FXM3GP:SAOZQJPL%WGP4BT59Q8K5MB*M44%K9JTWB9K+LS-2I9ST+3J%0X%1YGN0E0KL3/FHTZHEF9WIC4Q1HVK74L186BWT9+TJR7C2M%9JXN8S%0PVU8GFBET%0PG RR1KD2F11G-CW8A4124N9VN-T-:292AX*B%ER*$12-O:6K:/N$ 7IYFFXQ/5F NC49M/W5501B2OKU7E:NHMM8SETWUK*I$JR" + + // Remove HC1 header if any + if (barcode.hasPrefix("HC1")) { + barcode = String(barcode.suffix(barcode.count-3)) + } + + guard + let compressed = try? barcode.fromBase45() + else { + XCTAssert(false) + return + } + + let data = decompress(compressed) + + guard + let kidBytes = CBOR.kid(from: data), + let kid = String(data: Data(kidBytes), encoding: .utf8), + let url = URL(string: "https://dev.a-sit.at/certservice/cert/\(kid)") + else { + XCTAssert(false) + return + } + let expectation = XCTestExpectation(description: "Download PubKey") + URLSession.shared.dataTask(with: URLRequest(url: url)) { body, response, error in + guard + error == nil, + let status = (response as? HTTPURLResponse)?.statusCode, + 200 == status, + let body = body + else { + XCTAssert(false) + return + } + let encodedCert = body.base64EncodedString() + if COSE.verify(data, with: encodedCert) { + expectation.fulfill() + } else { + XCTAssert(false) + } + }.resume() + wait(for: [expectation], timeout: 15) + } + func testCoseATRsa() throws { + var barcode = "HC1NCFI.LUZB.P2YQ2E R002$DLTLIYXM/+0JNEAFUP.CV5GKCL /A0SQT/O8ZA85D0IJ$5D4.J+MR8-EGK6DM1R58V1ERUCWM4.E4ZM4$WG O E6KX9$KHCQMFE0B$KPKA*A2+ELQ*5ZQE/*1ZMSM5S.:KGQ33LQOWN67QW68LXM867D6837QCWEU*E*OKZCJPSCPIM1:Q83FBODLRD/W8K$E4 SG4P2JM6GCMU9FFSZAKNJH6ZNZVSWU78MHB4TO5J0N51U2F:17AB8O0*N0CQSA-9VIN904*STT$6R1LLXDT8J1ZN4I4368L02N72ZMGV496.0NO3OQCPFVF+C$BKQH1$+8L:AAEGRIB548B68D/3-ZTZ.JNM84E0F59$N6DY2*6FACA448900U42ZEMMO7-LO.45*IMXYIODF:82RN4L8Q%20HDJ-YCHIA1YT87A2/C1NO1F6DPCLCK9+BDXEEK23JH0/I8XJQEQ1T8BQ5GNDRFPQKKV6WT$A+98TIOUSAFA0J8OMY87$P-LESPS1KO+:O%HOY*C8XH4ZE7D16QR WNN$EP:U7E2HTLS023X0GJBW887LCKVHDGJ+Q88I0A73$H6R30XYH-LDC$4+24*JSE*H98A471BJ3:R0LU3MZA+ 4/GMZ9J D4E7L.TI$L8L:TN4G2S552748V$%D0CPH9RJ-7Z4AXFN*SS/ZC44O3P5%%AE936OUSR1H7WP%CP8KQUCY9I%A5FDBV+0A%HPVP5SKP9TKAFKT9FYROAABDBEHVQ9317O0D7MS5Z.Q2+K2.1RGGS3B5OTO2ROD60Z6LE98E94TV09JXM395VA0N.98EP3*4E5SHA7I96Q70UF74SPSIJI.BUHQJ$:K$9M51H9Z6RPHR.K-XT855UJJZC8C*MSITGMRE%O+:JQXV$LS7SJ1:DE LCX7RP7T JH4RR-NK.PASIWKA8J5Z1NA*BE7KFTJXO4BM5QVF.S0-5CG8KQWB8RCA1VP*1Q.ELZ1Q21G.JEGLOFH1:FR09$SCY/OGBU1QIE/2 9SK3" + + // Remove HC1 header if any + if (barcode.hasPrefix("HC1")) { + barcode = String(barcode.suffix(barcode.count-3)) + } + + guard + let compressed = try? barcode.fromBase45() + else { + XCTAssert(false) + return + } + + let data = decompress(compressed) + + guard + let kidBytes = CBOR.kid(from: data), + let kid = String(data: Data(kidBytes), encoding: .utf8), + let url = URL(string: "https://dev.a-sit.at/certservice/cert/\(kid)") + else { + XCTAssert(false) + return + } + let expectation = XCTestExpectation(description: "Download PubKey") + URLSession.shared.dataTask(with: URLRequest(url: url)) { body, response, error in + guard + error == nil, + let status = (response as? HTTPURLResponse)?.statusCode, + 200 == status, + let body = body + else { + XCTAssert(false) + return + } + let encodedCert = body.base64EncodedString() + if COSE.verify(data, with: encodedCert) { + expectation.fulfill() + } else { + XCTAssert(false) + } + }.resume() + wait(for: [expectation], timeout: 15) + } } /**