From 81c81f299612274bea173555ddc0a36398a1a83e Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Wed, 10 Mar 2021 13:39:13 +0100 Subject: [PATCH 01/22] Fix Github action --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b9db227..14abd32 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,7 @@ jobs: - name: Checkout the code uses: actions/checkout@v2 - name: Run tests with Thread sanitizer - run: swift test --eanble-test-discovery --sanitize=thread + run: swift test --enable-test-discovery --sanitize=thread macOS: runs-on: macos-latest steps: From 0a6e5c0b31b31feafcc7b047f608435d316a4daa Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Wed, 10 Mar 2021 13:44:00 +0100 Subject: [PATCH 02/22] Remove reference to old XML parsing library --- Sources/NemID/Response Handler/DefaultNemIDResponseHandler.swift | 1 - Sources/NemID/XMLDSig/ParsedXMLDSigResponse.swift | 1 - 2 files changed, 2 deletions(-) diff --git a/Sources/NemID/Response Handler/DefaultNemIDResponseHandler.swift b/Sources/NemID/Response Handler/DefaultNemIDResponseHandler.swift index aec5863..ffcb1d4 100644 --- a/Sources/NemID/Response Handler/DefaultNemIDResponseHandler.swift +++ b/Sources/NemID/Response Handler/DefaultNemIDResponseHandler.swift @@ -1,5 +1,4 @@ import Foundation -import SwiftyXMLParser import Crypto import NIO @_implementationOnly import CNemIDBoringSSL diff --git a/Sources/NemID/XMLDSig/ParsedXMLDSigResponse.swift b/Sources/NemID/XMLDSig/ParsedXMLDSigResponse.swift index db63281..e1631c6 100644 --- a/Sources/NemID/XMLDSig/ParsedXMLDSigResponse.swift +++ b/Sources/NemID/XMLDSig/ParsedXMLDSigResponse.swift @@ -1,5 +1,4 @@ import Foundation -import SwiftyXMLParser struct ParsedXMLDSigResponse { /// Returns the value as base64 encoded string From 5afd8bb8854c95c39f54dbd7d162bea1a2f052c1 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Wed, 10 Mar 2021 19:13:45 +0100 Subject: [PATCH 03/22] Add tests for libxml2 parser --- .../XMLDSigParser/libxml2DSigParser.swift | 2 +- .../NemIDTests/libxml2XMLDigParserTests.swift | 47 ++++++++++++++++--- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/Sources/NemID/XMLDSig/XMLDSigParser/libxml2DSigParser.swift b/Sources/NemID/XMLDSig/XMLDSigParser/libxml2DSigParser.swift index 66567e3..5e074a7 100644 --- a/Sources/NemID/XMLDSig/XMLDSigParser/libxml2DSigParser.swift +++ b/Sources/NemID/XMLDSig/XMLDSigParser/libxml2DSigParser.swift @@ -95,7 +95,7 @@ struct libxml2XMLDSigParser: XMLDSigParser { /// Parses a XPath query and returns the first element as XML data. private func parseFirstXPathElementAsXML(query: String, context: xmlXPathContextPtr, in xmlDoc: xmlDocPtr) -> Data? { - guard let xPathObject = xmlXPathEvalExpression(#"/openoces:signature/ds:Signature/ds:Object[@Id="ToBeSigned"]"#, context) else { + guard let xPathObject = xmlXPathEvalExpression(query, context) else { return nil } guard let nodePtr = xPathObject.pointee.nodesetval?.pointee.nodeTab[0] else { return nil } diff --git a/Tests/NemIDTests/libxml2XMLDigParserTests.swift b/Tests/NemIDTests/libxml2XMLDigParserTests.swift index d3d52e6..296ea1e 100644 --- a/Tests/NemIDTests/libxml2XMLDigParserTests.swift +++ b/Tests/NemIDTests/libxml2XMLDigParserTests.swift @@ -7,30 +7,65 @@ final class libxml2XMLDigParserTests: XCTestCase { let sut = libxml2XMLDSigParser() func test_parse_signatureValue_returnsSignatureValue() throws { - let result = try sut.parse([UInt8](xml.utf8)) + let result = try sut.parse([UInt8](exampleXMLResponse.utf8)) XCTAssertEqual(result.signatureValue, "signature-value") } func test_parse_referenceDigestValue_returnsFirstReferenceDigestValue() throws { - let result = try sut.parse([UInt8](xml.utf8)) + let result = try sut.parse([UInt8](exampleXMLResponse.utf8)) XCTAssertEqual(result.referenceDigestValue, "digest-value") } func test_parse_objectToBeSigned_returnsEntireXMLObject() throws { - XCTFail() + let result = try sut.parse([UInt8](exampleXMLStructure.utf8)) + XCTAssertEqual(String(data: result.objectToBeSigned, encoding: .utf8), """ + + object + + """ + ) } func test_parse_signedInfo_returnsEntireXMLObject() throws { - XCTFail() + let result = try sut.parse([UInt8](exampleXMLStructure.utf8)) + XCTAssertEqual(String(data: result.signedInfo, encoding: .utf8), """ + + + digest-value + + + """ + ) } func test_parse_x509Certificates_returnsArrayOfCerts() throws { - let result = try sut.parse([UInt8](xml.utf8)) + let result = try sut.parse([UInt8](exampleXMLResponse.utf8)) XCTAssertEqual(result.x509Certificates, ["cert1", "cert2", "cert3"]) } } -let xml = """ +fileprivate let exampleXMLStructure = """ + + + + +digest-value + + +signature-value + + +cert1 + + + +object + + + +""" + +fileprivate let exampleXMLResponse = """ From 9cf6fa2c2ec0bf99208ef1fdc1a9076bcb44d39e Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Wed, 10 Mar 2021 19:14:57 +0100 Subject: [PATCH 04/22] Install libxml2 in Github actions --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 14abd32..3775476 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,6 +15,8 @@ jobs: - swift:5.3-focal container: ${{ matrix.image }} steps: + - name: Install dependencies + run: apt install -y libxml2-dev - name: Checkout the code uses: actions/checkout@v2 - name: Run tests with Thread sanitizer From 41cc75b82013ed8f0323fbf5ee2cb229c4dcb99d Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Wed, 10 Mar 2021 19:19:41 +0100 Subject: [PATCH 05/22] Update apt in github actions --- .github/workflows/test.yml | 2 ++ Sources/NemID/Client Parameters/ParametersSigner.swift | 6 +++++- Tests/NemIDTests/ParameterSignerTests.swift | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3775476..d4d099b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,6 +15,8 @@ jobs: - swift:5.3-focal container: ${{ matrix.image }} steps: + - name: Update apt + run: apt update -y - name: Install dependencies run: apt install -y libxml2-dev - name: Checkout the code diff --git a/Sources/NemID/Client Parameters/ParametersSigner.swift b/Sources/NemID/Client Parameters/ParametersSigner.swift index 3b34295..6fc49a5 100644 --- a/Sources/NemID/Client Parameters/ParametersSigner.swift +++ b/Sources/NemID/Client Parameters/ParametersSigner.swift @@ -5,7 +5,11 @@ enum NemIDParameterSigningError: Error { case invalidData } -public struct NemIDParametersSigner { +public protocol NemIDParametersSigner { + func sign(_ parameters: NemIDUnsignedClientParameters) throws -> NemIDSignedClientParameters +} + +public struct DefaultNemIDParametersSigner: NemIDParametersSigner { private let rsaSigner: RSASigner public init(rsaSigner: RSASigner) { diff --git a/Tests/NemIDTests/ParameterSignerTests.swift b/Tests/NemIDTests/ParameterSignerTests.swift index e2b6cf0..3f66a7d 100644 --- a/Tests/NemIDTests/ParameterSignerTests.swift +++ b/Tests/NemIDTests/ParameterSignerTests.swift @@ -16,7 +16,7 @@ final class ParameterSignerTests: XCTestCase { let rsaKey = try RSAKey.private(pem: rsaPrivateKey) let rsaSigner = RSASigner(key: rsaKey) - let signer = NemIDParametersSigner(rsaSigner: rsaSigner) + let signer = DefaultNemIDParametersSigner(rsaSigner: rsaSigner) let signedParameters = try signer.sign(parameters) From 69c168bfa0f25652c143d2791b9ab8501c3148eb Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Wed, 10 Mar 2021 20:09:04 +0100 Subject: [PATCH 06/22] Change DI structure --- .../Client Parameters/ClientParameters.swift | 34 ---- .../Client Parameters/ParametersSigner.swift | 43 ++++- .../UnsignedClientParameters.swift | 3 - Sources/NemID/NemIDClient.swift | 38 ++++ Sources/NemID/NemIDConfiguration.swift | 10 + .../DefaultNemIDResponseHandler.swift | 173 ----------------- .../NemIDResponseHandler.swift | 175 +++++++++++++++++- Tests/NemIDTests/ParameterSignerTests.swift | 6 +- 8 files changed, 254 insertions(+), 228 deletions(-) create mode 100644 Sources/NemID/NemIDClient.swift create mode 100644 Sources/NemID/NemIDConfiguration.swift delete mode 100644 Sources/NemID/Response Handler/DefaultNemIDResponseHandler.swift diff --git a/Sources/NemID/Client Parameters/ClientParameters.swift b/Sources/NemID/Client Parameters/ClientParameters.swift index dfbd1aa..3e91da7 100644 --- a/Sources/NemID/Client Parameters/ClientParameters.swift +++ b/Sources/NemID/Client Parameters/ClientParameters.swift @@ -25,41 +25,7 @@ public protocol NemIDClientParameters { /// Indicates that the “Remember userid checkbox” should not be initially checked. /// This is only relevant in responsive mode and when REMEMBER_USERID is also set. var rememberUserIDInitialStatus: Bool? { get } - /// Base64 encoded DER representation of the certificate used for identifying the OCES Service Provider - var SPCert: String { get } /// Current time when generating parameters. The timestamp parameter is converted to UTC and must match the NemID server time. /// NemID accepts timestamps within the boundaries of +-3 minutes. var timestamp: Date { get } } - -// MARK: - NemIDClientParameters+Normalized -extension NemIDClientParameters { - /// Returns a normalized string of the parameters, as described in the NemID documentation. - /// - /// 1. The parameters are sorted alphabetically by name. The sorting is case-insensitive. - /// 2. Each parameter is concatenated to the result string as an alternating sequence of name and value - var normalized: String { - var parameters: [String: String] = [ - "CLIENTFLOW": self.clientFlow.rawValue, - "LANGUAGE": self.language.rawValue, - "SP_CERT": self.SPCert, - "TIMESTAMP": String(self.timestamp.timeIntervalSince1970 * 1000) - ] - - if let origin = self.origin?.absoluteString { - parameters["ORIGIN"] = origin - } - if let rememberUserID = self.rememberUserID { - parameters["REMEMBER_USERID"] = rememberUserID - } - if let rememberUserIDInitialStatus = self.rememberUserIDInitialStatus { - parameters["REMEMBER_USERID_INITIAL_STATUS"] = rememberUserIDInitialStatus ? "TRUE": "FALSE" - } - - let sortedAlphabeticallyByKeys = parameters.sorted(by: { $0.key.lowercased() < $1.key.lowercased()} ) - return sortedAlphabeticallyByKeys - .reduce(into: "") { result, parameter in - result += "\(parameter.key)\(parameter.value)" - } - } -} diff --git a/Sources/NemID/Client Parameters/ParametersSigner.swift b/Sources/NemID/Client Parameters/ParametersSigner.swift index 6fc49a5..8c1be83 100644 --- a/Sources/NemID/Client Parameters/ParametersSigner.swift +++ b/Sources/NemID/Client Parameters/ParametersSigner.swift @@ -5,19 +5,17 @@ enum NemIDParameterSigningError: Error { case invalidData } -public protocol NemIDParametersSigner { - func sign(_ parameters: NemIDUnsignedClientParameters) throws -> NemIDSignedClientParameters -} - -public struct DefaultNemIDParametersSigner: NemIDParametersSigner { +struct NemIDParametersSigner { private let rsaSigner: RSASigner + private let configuration: NemIDConfiguration - public init(rsaSigner: RSASigner) { + public init(rsaSigner: RSASigner, configuration: NemIDConfiguration) { self.rsaSigner = rsaSigner + self.configuration = configuration } public func sign(_ parameters: NemIDUnsignedClientParameters) throws -> NemIDSignedClientParameters { - let normalizedData = [UInt8](parameters.normalized.utf8) + let normalizedData = [UInt8](normalizedParameters(parameters).utf8) let digest = SHA256.hash(data: normalizedData) // RSASigner also SHA256 hashes the data. let signature = try rsaSigner.sign(normalizedData) @@ -31,11 +29,40 @@ public struct DefaultNemIDParametersSigner: NemIDParametersSigner { origin: parameters.origin, rememberUserID: parameters.rememberUserID, rememberUserIDInitialStatus: parameters.rememberUserIDInitialStatus, - SPCert: parameters.SPCert, + SPCert: configuration.spCertificate, timestamp: parameters.timestamp, digestSignature: base64SignedDigest, paramsDigest: base64ParamsDigest ) } + + /// Returns a normalized string of the parameters, as described in the NemID documentation. + /// + /// 1. The parameters are sorted alphabetically by name. The sorting is case-insensitive. + /// 2. Each parameter is concatenated to the result string as an alternating sequence of name and value + private func normalizedParameters(_ unsignedParameters: NemIDUnsignedClientParameters) -> String { + var parameters: [String: String] = [ + "CLIENTFLOW": unsignedParameters.clientFlow.rawValue, + "LANGUAGE": unsignedParameters.language.rawValue, + "SP_CERT": configuration.spCertificate, + "TIMESTAMP": String(unsignedParameters.timestamp.timeIntervalSince1970 * 1000) + ] + + if let origin = unsignedParameters.origin?.absoluteString { + parameters["ORIGIN"] = origin + } + if let rememberUserID = unsignedParameters.rememberUserID { + parameters["REMEMBER_USERID"] = rememberUserID + } + if let rememberUserIDInitialStatus = unsignedParameters.rememberUserIDInitialStatus { + parameters["REMEMBER_USERID_INITIAL_STATUS"] = rememberUserIDInitialStatus ? "TRUE": "FALSE" + } + + let sortedAlphabeticallyByKeys = parameters.sorted(by: { $0.key.lowercased() < $1.key.lowercased()} ) + return sortedAlphabeticallyByKeys + .reduce(into: "") { result, parameter in + result += "\(parameter.key)\(parameter.value)" + } + } } diff --git a/Sources/NemID/Client Parameters/UnsignedClientParameters.swift b/Sources/NemID/Client Parameters/UnsignedClientParameters.swift index 873badd..38628a7 100644 --- a/Sources/NemID/Client Parameters/UnsignedClientParameters.swift +++ b/Sources/NemID/Client Parameters/UnsignedClientParameters.swift @@ -6,7 +6,6 @@ public struct NemIDUnsignedClientParameters: NemIDClientParameters { public let origin: URL? public let rememberUserID: String? public let rememberUserIDInitialStatus: Bool? - public let SPCert: String public let timestamp: Date public init( @@ -15,7 +14,6 @@ public struct NemIDUnsignedClientParameters: NemIDClientParameters { origin: URL?, rememberUserID: String?, rememberUserIDInitialStatus: Bool?, - SPCert: String, timestamp: Date ) { self.clientFlow = clientFlow @@ -23,7 +21,6 @@ public struct NemIDUnsignedClientParameters: NemIDClientParameters { self.origin = origin self.rememberUserID = rememberUserID self.rememberUserIDInitialStatus = rememberUserIDInitialStatus - self.SPCert = SPCert self.timestamp = timestamp } } diff --git a/Sources/NemID/NemIDClient.swift b/Sources/NemID/NemIDClient.swift new file mode 100644 index 0000000..12496d0 --- /dev/null +++ b/Sources/NemID/NemIDClient.swift @@ -0,0 +1,38 @@ +import Foundation +import NIO +import Logging +import AsyncHTTPClient + +public protocol NemIDClient { + func delegating(to eventLoop: EventLoop) -> Self +} + +public struct LiveNemIDClient: NemIDClient { + private let httpClient: HTTPClient + private let logger: Logger + private let eventLoop: EventLoop + private let configuration: NemIDConfiguration + private let responseHandler: NemIDResponseHandler + private let parametersSigner: NemIDParametersSigner + + public init(eventLoop: EventLoop, httpClient: HTTPClient, logger: Logger, configuration: NemIDConfiguration) { + self.eventLoop = eventLoop + self.httpClient = httpClient + self.logger = logger + self.configuration = configuration + responseHandler = NemIDResponseHandler( + xmlParser: libxml2XMLDSigParser(), + certificateExtractor: DefaultCertificateExtractor(), + ocspClient: HTTPOCSPClient(client: httpClient, eventLoop: eventLoop, logger: logger), + eventLoop: eventLoop + ) + parametersSigner = NemIDParametersSigner( + rsaSigner: RSASigner(key: try! .private(pem: "")), + configuration: configuration + ) + } + + public func delegating(to eventLoop: EventLoop) -> LiveNemIDClient { + LiveNemIDClient(eventLoop: eventLoop, httpClient: self.httpClient, logger: self.logger, configuration: self.configuration) + } +} diff --git a/Sources/NemID/NemIDConfiguration.swift b/Sources/NemID/NemIDConfiguration.swift new file mode 100644 index 0000000..f65d558 --- /dev/null +++ b/Sources/NemID/NemIDConfiguration.swift @@ -0,0 +1,10 @@ +import Foundation + +public struct NemIDConfiguration { + /// The certificate of the OCES service-provider as a Base64 encoded DER. + public let spCertificate: String + + public init(spCertificate: String) { + self.spCertificate = spCertificate + } +} diff --git a/Sources/NemID/Response Handler/DefaultNemIDResponseHandler.swift b/Sources/NemID/Response Handler/DefaultNemIDResponseHandler.swift deleted file mode 100644 index ffcb1d4..0000000 --- a/Sources/NemID/Response Handler/DefaultNemIDResponseHandler.swift +++ /dev/null @@ -1,173 +0,0 @@ -import Foundation -import Crypto -import NIO -@_implementationOnly import CNemIDBoringSSL - -#warning("if something fails it might be releated to this:") -/* - //remember to skip the first byte it is the number of unused bits and it is always 0 for keys and certificates from nodes-php - */ -struct DefaultNemIDResponseHandler: NemIDResponseHandler { - private let xmlParser: XMLDSigParser - private let certificateExtractor: CertificateExtrator - private let ocspClient: OCSPClient - private let eventLoop: EventLoop - - public init(xmlParser: XMLDSigParser, certificateExtractor: CertificateExtrator, ocspClient: OCSPClient, eventLoop: EventLoop) { - self.xmlParser = xmlParser - self.certificateExtractor = certificateExtractor - self.ocspClient = ocspClient - self.eventLoop = eventLoop - } - - func verifyAndExtractUser(fromXML xmlData: [UInt8]) -> EventLoopFuture { - do { - let parsedResponse = try xmlParser.parse(xmlData) - // Extract certificate chain. - let certificates = try certificateExtractor.extract(from: parsedResponse) - - // Validate XML signature with leaf certificate - try validateXMLSignature(parsedResponse, wasSignedBy: certificates.leaf) - - // Validate certificate chain - try validateCertificateChain(certificates) - - // Verify that certificate has not been revoked (OCSP) - let ocspRequest = try OCSPRequest(certificate: certificates.leaf, issuer: certificates.intermediate) - return ocspClient - .send(request: ocspRequest) - .flatMapThrowing { response in - try validateOCSPResponse(response, chain: certificates) - return try NemIDUser(from: certificates.leaf) - } - } catch { - return eventLoop.makeFailedFuture(error) - } - } - - private func validateOCSPResponse(_ response: OCSPResponse, chain: CertificateChain) throws { - // Check response status - guard response.responseStatus == .successful else { - throw NemIDResponseHandlerError.ocspRequestWasNotSuccessful - } - guard let basicResponse = response.basicOCSPResponse else { - throw NemIDResponseHandlerError.ocspBasicResponseIsNotPresent - } - - // Validate that signature is tbsResponseData signed by accompanying certificate - guard let ocspCertificate = basicResponse.certs.first else { - throw NemIDResponseHandlerError.ocspCertificateNotFoundInResponse - } - let signer = RSASigner(key: try ocspCertificate.publicKey()) - guard try signer.verify(basicResponse.signature, signs: basicResponse.tbsResponseData.derBytes) else { - throw NemIDResponseHandlerError.ocspSignatureWasNotSignedByCertificate - } - - // Validate that accompanying certificate was signed by issuer. - guard try ocspCertificate.isSignedBy(by: chain.intermediate) else { - throw NemIDResponseHandlerError.ocspCertificateWasNotSignedByIssuer - } - - // Validate certificate recovation status - guard let certResponse = basicResponse.tbsResponseData.responses.first else { - throw NemIDResponseHandlerError.ocspCertificateResponseNotPresent - } - guard certResponse.certStatus == .good else { - throw NemIDResponseHandlerError.ocspCertificateStatusIsNotGood - } - - // Check hash algorithm - guard certResponse.certID.hashAlgorithm == .sha256 else { - throw NemIDResponseHandlerError.ocspCertificateWrongHashAlgorithm - } - - // Check hash name, key hash and serial number are the ones we sent in the request. - try chain.leaf.withSerialNumber { serialNumber in - var ptr: UnsafeMutablePointer? - ptr = nil - let leafSerialNumberSize = CNemIDBoringSSL_BN_bn2bin(serialNumber, ptr) - let leafSerialNumberBytes = [UInt8](UnsafeMutableBufferPointer(start: ptr, count: leafSerialNumberSize)) - guard certResponse.certID.serialNumber == leafSerialNumberBytes else { fatalError() } - } - guard chain.intermediate.hashedPublicKey == certResponse.certID.issuerKeyHash else { fatalError() } - guard chain.intermediate.hashedSubject == certResponse.certID.issuerNameHash else { fatalError() } - - // Check OCSP revocation dates - guard certResponse.nextUpdate >= Date() && certResponse.thisUpdate <= Date() else { - throw NemIDResponseHandlerError.ocspResponseIsOutsideAllowedTime - } - - // Check OCSP signing key usage - - // Check OCSP extension - } - - private func validateCertificateChain(_ chain: CertificateChain) throws { - // Verify that leaf certificate has digitalSignature key usage - guard chain.leaf.hasKeyUsage(.digitalSignature) else { - throw NemIDResponseHandlerError.leafDidNotHaveDigitalSignatureKeyUsage - } - - // Verify certificate times. - for certificate in chain { - guard let notAfter = certificate.notAfter(), - let notBefore = certificate.notBefore() - else { - throw NemIDResponseHandlerError.failedToExtractCertificateDates - } - guard notAfter < Date() && notBefore > Date() else { - throw NemIDResponseHandlerError.certificateIsOutsideValidTime - } - } - - #warning("Path len validation???") - - // Verify that intermediate and root has cA constraint - guard chain.root.hasCAFlag() && chain.intermediate.hasCAFlag() else { - throw NemIDResponseHandlerError.issuerDidNotHaveCAFlag - } - - // Verify that intermediate and root has keyCertSign usage - guard chain.intermediate.hasKeyUsage(.keyCertSign) && chain.root.hasKeyUsage(.keyCertSign) else { - throw NemIDResponseHandlerError.issuerDidNotHaveKeyCertSignKeyUsage - } - - // Verify the actual chain signing. - guard try chain.leaf.isSignedBy(by: chain.intermediate), - try chain.intermediate.isSignedBy(by: chain.root), - try chain.root.isSignedBy(by: chain.root) - else { - throw NemIDResponseHandlerError.certificateWasNotSignedByCorrectCertificate - } - - // Verify that root certificate is a trusted OCES certificate. - #warning("todo") - } - - /// Verifies the signed element in the xml response - private func validateXMLSignature(_ response: ParsedXMLDSigResponse, wasSignedBy certificate: X509Certificate) throws { - guard let signedInfoC14N = response.signedInfo.C14N() else { - throw NemIDResponseHandlerError.failedToExtractSignedInfo - } - guard let referenceDigestBase64Decoded = Data(base64Encoded: response.referenceDigestValue) else { - throw NemIDResponseHandlerError.failedToExtractReferenceDigest - } - guard let objectToBeSignedC14N = response.objectToBeSigned.C14N() else { - throw NemIDResponseHandlerError.failedToExtractObjectToBeSigned - } - guard let signatureValueBase64Decoded = Data(base64Encoded: response.signatureValue) else { - throw NemIDResponseHandlerError.failedToExtractSignatureValue - } - - // Verify reference object digest was made from ToBeSigned object. - guard SHA256.hash(data: objectToBeSignedC14N) == referenceDigestBase64Decoded else { - throw NemIDResponseHandlerError.digestDidNotMatchSignedObject - } - - // Verify that signedInfo was signed with certificate - let signer = RSASigner(key: try certificate.publicKey()) - guard try signer.verify([UInt8](signatureValueBase64Decoded), signs: signedInfoC14N) else { - throw NemIDResponseHandlerError.signedInfoWasNotSignedByCertificate - } - } -} diff --git a/Sources/NemID/Response Handler/NemIDResponseHandler.swift b/Sources/NemID/Response Handler/NemIDResponseHandler.swift index 583f92f..9dfe0df 100644 --- a/Sources/NemID/Response Handler/NemIDResponseHandler.swift +++ b/Sources/NemID/Response Handler/NemIDResponseHandler.swift @@ -1,7 +1,25 @@ import Foundation +import Crypto import NIO +@_implementationOnly import CNemIDBoringSSL -protocol NemIDResponseHandler { +#warning("if something fails it might be releated to this:") +/* + //remember to skip the first byte it is the number of unused bits and it is always 0 for keys and certificates from nodes-php + */ +struct NemIDResponseHandler { + private let xmlParser: XMLDSigParser + private let certificateExtractor: CertificateExtrator + private let ocspClient: OCSPClient + private let eventLoop: EventLoop + + init(xmlParser: XMLDSigParser, certificateExtractor: CertificateExtrator, ocspClient: OCSPClient, eventLoop: EventLoop) { + self.xmlParser = xmlParser + self.certificateExtractor = certificateExtractor + self.ocspClient = ocspClient + self.eventLoop = eventLoop + } + /// Verifies a response from a NemID client flow such as logging in and extratcs the user as `NemIDUser` /// /// Does the checks in respect to the NemID documentation p. 34: @@ -14,11 +32,154 @@ protocol NemIDResponseHandler { /// - Parameters: /// - response: The XML as XML data received from the client. /// - Returns: A `EventLoopFuture` containg the verified certificate user as `NemIDUser`. - func verifyAndExtractUser(fromXML xmlData: [UInt8]) -> EventLoopFuture -} - -extension NemIDResponseHandler { - func verifyAndExtractUser(fromXML xmlString: String) -> EventLoopFuture { - verifyAndExtractUser(fromXML: [UInt8].init(xmlString.utf8)) + func verifyAndExtractUser(fromXML xmlData: [UInt8]) -> EventLoopFuture { + do { + let parsedResponse = try xmlParser.parse(xmlData) + // Extract certificate chain. + let certificates = try certificateExtractor.extract(from: parsedResponse) + + // Validate XML signature with leaf certificate + try validateXMLSignature(parsedResponse, wasSignedBy: certificates.leaf) + + // Validate certificate chain + try validateCertificateChain(certificates) + + // Verify that certificate has not been revoked (OCSP) + let ocspRequest = try OCSPRequest(certificate: certificates.leaf, issuer: certificates.intermediate) + return ocspClient + .send(request: ocspRequest) + .flatMapThrowing { response in + try validateOCSPResponse(response, chain: certificates) + return try NemIDUser(from: certificates.leaf) + } + } catch { + return eventLoop.makeFailedFuture(error) + } + } + + private func validateOCSPResponse(_ response: OCSPResponse, chain: CertificateChain) throws { + // Check response status + guard response.responseStatus == .successful else { + throw NemIDResponseHandlerError.ocspRequestWasNotSuccessful + } + guard let basicResponse = response.basicOCSPResponse else { + throw NemIDResponseHandlerError.ocspBasicResponseIsNotPresent + } + + // Validate that signature is tbsResponseData signed by accompanying certificate + guard let ocspCertificate = basicResponse.certs.first else { + throw NemIDResponseHandlerError.ocspCertificateNotFoundInResponse + } + let signer = RSASigner(key: try ocspCertificate.publicKey()) + guard try signer.verify(basicResponse.signature, signs: basicResponse.tbsResponseData.derBytes) else { + throw NemIDResponseHandlerError.ocspSignatureWasNotSignedByCertificate + } + + // Validate that accompanying certificate was signed by issuer. + guard try ocspCertificate.isSignedBy(by: chain.intermediate) else { + throw NemIDResponseHandlerError.ocspCertificateWasNotSignedByIssuer + } + + // Validate certificate recovation status + guard let certResponse = basicResponse.tbsResponseData.responses.first else { + throw NemIDResponseHandlerError.ocspCertificateResponseNotPresent + } + guard certResponse.certStatus == .good else { + throw NemIDResponseHandlerError.ocspCertificateStatusIsNotGood + } + + // Check hash algorithm + guard certResponse.certID.hashAlgorithm == .sha256 else { + throw NemIDResponseHandlerError.ocspCertificateWrongHashAlgorithm + } + + // Check hash name, key hash and serial number are the ones we sent in the request. + try chain.leaf.withSerialNumber { serialNumber in + var ptr: UnsafeMutablePointer? + ptr = nil + let leafSerialNumberSize = CNemIDBoringSSL_BN_bn2bin(serialNumber, ptr) + let leafSerialNumberBytes = [UInt8](UnsafeMutableBufferPointer(start: ptr, count: leafSerialNumberSize)) + guard certResponse.certID.serialNumber == leafSerialNumberBytes else { fatalError() } + } + guard chain.intermediate.hashedPublicKey == certResponse.certID.issuerKeyHash else { fatalError() } + guard chain.intermediate.hashedSubject == certResponse.certID.issuerNameHash else { fatalError() } + + // Check OCSP revocation dates + guard certResponse.nextUpdate >= Date() && certResponse.thisUpdate <= Date() else { + throw NemIDResponseHandlerError.ocspResponseIsOutsideAllowedTime + } + + // Check OCSP signing key usage + + // Check OCSP extension + } + + private func validateCertificateChain(_ chain: CertificateChain) throws { + // Verify that leaf certificate has digitalSignature key usage + guard chain.leaf.hasKeyUsage(.digitalSignature) else { + throw NemIDResponseHandlerError.leafDidNotHaveDigitalSignatureKeyUsage + } + + // Verify certificate times. + for certificate in chain { + guard let notAfter = certificate.notAfter(), + let notBefore = certificate.notBefore() + else { + throw NemIDResponseHandlerError.failedToExtractCertificateDates + } + guard notAfter < Date() && notBefore > Date() else { + throw NemIDResponseHandlerError.certificateIsOutsideValidTime + } + } + + #warning("Path len validation???") + + // Verify that intermediate and root has cA constraint + guard chain.root.hasCAFlag() && chain.intermediate.hasCAFlag() else { + throw NemIDResponseHandlerError.issuerDidNotHaveCAFlag + } + + // Verify that intermediate and root has keyCertSign usage + guard chain.intermediate.hasKeyUsage(.keyCertSign) && chain.root.hasKeyUsage(.keyCertSign) else { + throw NemIDResponseHandlerError.issuerDidNotHaveKeyCertSignKeyUsage + } + + // Verify the actual chain signing. + guard try chain.leaf.isSignedBy(by: chain.intermediate), + try chain.intermediate.isSignedBy(by: chain.root), + try chain.root.isSignedBy(by: chain.root) + else { + throw NemIDResponseHandlerError.certificateWasNotSignedByCorrectCertificate + } + + // Verify that root certificate is a trusted OCES certificate. + #warning("todo") + } + + /// Verifies the signed element in the xml response + private func validateXMLSignature(_ response: ParsedXMLDSigResponse, wasSignedBy certificate: X509Certificate) throws { + guard let signedInfoC14N = response.signedInfo.C14N() else { + throw NemIDResponseHandlerError.failedToExtractSignedInfo + } + guard let referenceDigestBase64Decoded = Data(base64Encoded: response.referenceDigestValue) else { + throw NemIDResponseHandlerError.failedToExtractReferenceDigest + } + guard let objectToBeSignedC14N = response.objectToBeSigned.C14N() else { + throw NemIDResponseHandlerError.failedToExtractObjectToBeSigned + } + guard let signatureValueBase64Decoded = Data(base64Encoded: response.signatureValue) else { + throw NemIDResponseHandlerError.failedToExtractSignatureValue + } + + // Verify reference object digest was made from ToBeSigned object. + guard SHA256.hash(data: objectToBeSignedC14N) == referenceDigestBase64Decoded else { + throw NemIDResponseHandlerError.digestDidNotMatchSignedObject + } + + // Verify that signedInfo was signed with certificate + let signer = RSASigner(key: try certificate.publicKey()) + guard try signer.verify([UInt8](signatureValueBase64Decoded), signs: signedInfoC14N) else { + throw NemIDResponseHandlerError.signedInfoWasNotSignedByCertificate + } } } diff --git a/Tests/NemIDTests/ParameterSignerTests.swift b/Tests/NemIDTests/ParameterSignerTests.swift index 3f66a7d..4eba934 100644 --- a/Tests/NemIDTests/ParameterSignerTests.swift +++ b/Tests/NemIDTests/ParameterSignerTests.swift @@ -1,5 +1,5 @@ import XCTest -import NemID +@testable import NemID final class ParameterSignerTests: XCTestCase { func test_sign_returnsSignedParameters() throws { @@ -11,12 +11,12 @@ final class ParameterSignerTests: XCTestCase { origin: URL(string: "https://nemid.dk")!, rememberUserID: nil, rememberUserIDInitialStatus: nil, - SPCert: "cert", timestamp: date) let rsaKey = try RSAKey.private(pem: rsaPrivateKey) let rsaSigner = RSASigner(key: rsaKey) - let signer = DefaultNemIDParametersSigner(rsaSigner: rsaSigner) + let configuration = NemIDConfiguration(spCertificate: "cert") + let signer = NemIDParametersSigner(rsaSigner: rsaSigner, configuration: configuration) let signedParameters = try signer.sign(parameters) From 2542eaacc02dcd592545378f10c4d21a5be32c5b Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Thu, 11 Mar 2021 10:30:33 +0100 Subject: [PATCH 07/22] Ignore vendored C libraries in git language detection --- .gitattributes | 2 ++ .../NemID/{NemIDClient.swift => NemIDLoginService.swift} | 8 ++++---- Sources/NemID/Response Handler/NemIDResponseHandler.swift | 2 ++ 3 files changed, 8 insertions(+), 4 deletions(-) create mode 100644 .gitattributes rename Sources/NemID/{NemIDClient.swift => NemIDLoginService.swift} (83%) diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..43ba5a9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +Sources/CNemIDBoringSSL/* linguist-vendored +Sources/Clibxml2/* linguist-vendored \ No newline at end of file diff --git a/Sources/NemID/NemIDClient.swift b/Sources/NemID/NemIDLoginService.swift similarity index 83% rename from Sources/NemID/NemIDClient.swift rename to Sources/NemID/NemIDLoginService.swift index 12496d0..911f9c6 100644 --- a/Sources/NemID/NemIDClient.swift +++ b/Sources/NemID/NemIDLoginService.swift @@ -3,11 +3,11 @@ import NIO import Logging import AsyncHTTPClient -public protocol NemIDClient { +public protocol NemIDLoginService { func delegating(to eventLoop: EventLoop) -> Self } -public struct LiveNemIDClient: NemIDClient { +public struct LiveNemIDLoginService: NemIDLoginService { private let httpClient: HTTPClient private let logger: Logger private let eventLoop: EventLoop @@ -32,7 +32,7 @@ public struct LiveNemIDClient: NemIDClient { ) } - public func delegating(to eventLoop: EventLoop) -> LiveNemIDClient { - LiveNemIDClient(eventLoop: eventLoop, httpClient: self.httpClient, logger: self.logger, configuration: self.configuration) + public func delegating(to eventLoop: EventLoop) -> LiveNemIDLoginService { + LiveNemIDLoginService(eventLoop: eventLoop, httpClient: self.httpClient, logger: self.logger, configuration: self.configuration) } } diff --git a/Sources/NemID/Response Handler/NemIDResponseHandler.swift b/Sources/NemID/Response Handler/NemIDResponseHandler.swift index 9dfe0df..b47202d 100644 --- a/Sources/NemID/Response Handler/NemIDResponseHandler.swift +++ b/Sources/NemID/Response Handler/NemIDResponseHandler.swift @@ -110,8 +110,10 @@ struct NemIDResponseHandler { } // Check OCSP signing key usage + #warning("todo") // Check OCSP extension + #warning("todo") } private func validateCertificateChain(_ chain: CertificateChain) throws { From ddd43bb51f988070703096bc080b301b6b8c634c Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Fri, 12 Mar 2021 13:27:12 +0100 Subject: [PATCH 08/22] Finish OCSP response validation --- .../NemIDResponseHandler.swift | 8 +++++-- .../NemIDResponseHandlerError.swift | 2 ++ Sources/NemID/X509/X509Certificate.swift | 24 +++++++++++++++++++ Tests/NemIDTests/X509CertificateTests.swift | 14 +++++++++++ 4 files changed, 46 insertions(+), 2 deletions(-) diff --git a/Sources/NemID/Response Handler/NemIDResponseHandler.swift b/Sources/NemID/Response Handler/NemIDResponseHandler.swift index b47202d..d004174 100644 --- a/Sources/NemID/Response Handler/NemIDResponseHandler.swift +++ b/Sources/NemID/Response Handler/NemIDResponseHandler.swift @@ -110,10 +110,14 @@ struct NemIDResponseHandler { } // Check OCSP signing key usage - #warning("todo") + guard ocspCertificate.hasExtendedKeyUsage(.ocspSigning) else { + throw NemIDResponseHandlerError.ocspCertificateDidNotHaveOCSPSigningExtendedKeyUsage + } // Check OCSP extension - #warning("todo") + guard !ocspCertificate.hasOCSPNoCheckExtension() else { + throw NemIDResponseHandlerError.ocspCertificateHasNoCheckExtension + } } private func validateCertificateChain(_ chain: CertificateChain) throws { diff --git a/Sources/NemID/Response Handler/NemIDResponseHandlerError.swift b/Sources/NemID/Response Handler/NemIDResponseHandlerError.swift index ac6332b..21b7fbd 100644 --- a/Sources/NemID/Response Handler/NemIDResponseHandlerError.swift +++ b/Sources/NemID/Response Handler/NemIDResponseHandlerError.swift @@ -22,4 +22,6 @@ enum NemIDResponseHandlerError: Error { case ocspCertificateStatusIsNotGood case ocspCertificateWrongHashAlgorithm case ocspResponseIsOutsideAllowedTime + case ocspCertificateDidNotHaveOCSPSigningExtendedKeyUsage + case ocspCertificateHasNoCheckExtension } diff --git a/Sources/NemID/X509/X509Certificate.swift b/Sources/NemID/X509/X509Certificate.swift index efb4b59..067ab8d 100644 --- a/Sources/NemID/X509/X509Certificate.swift +++ b/Sources/NemID/X509/X509Certificate.swift @@ -88,12 +88,23 @@ final class X509Certificate: BIOLoadable { return Int32(flags) & EXFLAG_CA == EXFLAG_CA } + func hasOCSPNoCheckExtension() -> Bool { + // -1 means not found, otherwise returns index of extension. + CNemIDBoringSSL_X509_get_ext_by_NID(self.ref, NID_id_pkix_OCSP_noCheck, -1) >= 0 + } + /// Check if this certificate has a specific `KeyUsage` func hasKeyUsage(_ usage: KeyUsage) -> Bool { let keyUsage = CNemIDBoringSSL_X509_get_key_usage(self.ref) return Int32(keyUsage) & usage.value == usage.value } + /// Check if this certificate has a specific extended key usage. + func hasExtendedKeyUsage(_ usage: ExtendedKeyUsage) -> Bool { + let extendedKeyUsage = CNemIDBoringSSL_X509_get_extended_key_usage(self.ref) + return numericCast(extendedKeyUsage) & usage.value == usage.value + } + /// Returns a pointer to the public key, which is only valid for the lifetime of the closure func withPublicKey(_ handler: (UnsafeMutablePointer?) throws -> T) throws -> T { guard let pubKey = CNemIDBoringSSL_X509_get_pubkey(self.ref) else { throw X509CertificateError.failedToRetrievePublicKey } @@ -242,3 +253,16 @@ extension X509Certificate { static let serialNumber = NameComponent(NID_serialNumber) } } + +// MARK: ExtendedKeyUsage +extension X509Certificate { + struct ExtendedKeyUsage { + let value: Int32 + + init(_ value: Int32) { + self.value = value + } + + static let ocspSigning = ExtendedKeyUsage(XKU_OCSP_SIGN) + } +} diff --git a/Tests/NemIDTests/X509CertificateTests.swift b/Tests/NemIDTests/X509CertificateTests.swift index 1c0cab7..0f8b22a 100644 --- a/Tests/NemIDTests/X509CertificateTests.swift +++ b/Tests/NemIDTests/X509CertificateTests.swift @@ -49,4 +49,18 @@ final class X509CertificateTests: XCTestCase { let certificate = try X509Certificate(der: Data(base64Encoded: TestCertificates.googleLeaf, options: .ignoreUnknownCharacters)!) XCTAssertEqual(certificate.subjectCommonName, "*.google.com") } + + func test_hasExtendedKeyUsage_ocspSinging_withGoogleCert_returnsFalse() throws { + let certificate = try X509Certificate(der: Data(base64Encoded: TestCertificates.googleLeaf, options: .ignoreUnknownCharacters)!) + XCTAssertEqual(certificate.hasExtendedKeyUsage(.ocspSigning), false) + } + + func test_hasOCSPNoCheckExtension_withGoogleCert_returnsFalse() throws { + let certificate = try X509Certificate(der: Data(base64Encoded: TestCertificates.googleLeaf, options: .ignoreUnknownCharacters)!) + XCTAssertEqual(certificate.hasOCSPNoCheckExtension(), false) + } + + func test_hasExtendedKeyUsage_ocspSinging_withOCSPCertificate_returnsTrue() throws { + XCTFail("Needs example cert.") + } } From 57b6269201eaa35a4383f0dfaf8f6e5420f9e4bc Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Fri, 12 Mar 2021 13:49:58 +0100 Subject: [PATCH 09/22] Fix nits --- Sources/NemID/NemIDUser.swift | 4 +++- Sources/NemID/Response Handler/NemIDResponseHandler.swift | 4 +--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/NemID/NemIDUser.swift b/Sources/NemID/NemIDUser.swift index 7113ac2..71fda0b 100644 --- a/Sources/NemID/NemIDUser.swift +++ b/Sources/NemID/NemIDUser.swift @@ -15,7 +15,9 @@ public struct NemIDUser { public let name: String? init(from certificate: X509Certificate) throws { - guard let commonName = certificate.subjectCommonName else { throw InitializationError.failedToExtractCommonNameFromCertificate } + guard let commonName = certificate.subjectCommonName else { + throw InitializationError.failedToExtractCommonNameFromCertificate + } guard let pid = certificate.subjectSerialNumber?.components(separatedBy: "PID:").last else { throw InitializationError.failedToExtractPIDFromCertificate } diff --git a/Sources/NemID/Response Handler/NemIDResponseHandler.swift b/Sources/NemID/Response Handler/NemIDResponseHandler.swift index d004174..d08e44a 100644 --- a/Sources/NemID/Response Handler/NemIDResponseHandler.swift +++ b/Sources/NemID/Response Handler/NemIDResponseHandler.swift @@ -126,8 +126,8 @@ struct NemIDResponseHandler { throw NemIDResponseHandlerError.leafDidNotHaveDigitalSignatureKeyUsage } - // Verify certificate times. for certificate in chain { + // Verify dates guard let notAfter = certificate.notAfter(), let notBefore = certificate.notBefore() else { @@ -138,8 +138,6 @@ struct NemIDResponseHandler { } } - #warning("Path len validation???") - // Verify that intermediate and root has cA constraint guard chain.root.hasCAFlag() && chain.intermediate.hasCAFlag() else { throw NemIDResponseHandlerError.issuerDidNotHaveCAFlag From e919a6abd0938dfb214424962d933fff33b4adca Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Sun, 14 Mar 2021 21:00:37 +0100 Subject: [PATCH 10/22] Start on PID-CPR match --- Package.resolved | 9 +++ Package.swift | 2 + Sources/NemID/NemIDConfiguration.swift | 12 +++- Sources/NemID/NemIDEnvironment.swift | 17 ++++++ .../PID-CPR Match/HTTPPIDCPRMatchClient.swift | 60 +++++++++++++++++++ .../Models/PIDCPRMatchRequest.swift | 47 +++++++++++++++ .../PID-CPR Match/PIDCPRMatchClient.swift | 7 +++ .../NemID/Utilities/String+urlEncoded.swift | 25 ++++++++ Tests/NemIDTests/ParameterSignerTests.swift | 2 +- 9 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 Sources/NemID/NemIDEnvironment.swift create mode 100644 Sources/NemID/PID-CPR Match/HTTPPIDCPRMatchClient.swift create mode 100644 Sources/NemID/PID-CPR Match/Models/PIDCPRMatchRequest.swift create mode 100644 Sources/NemID/PID-CPR Match/PIDCPRMatchClient.swift create mode 100644 Sources/NemID/Utilities/String+urlEncoded.swift diff --git a/Package.resolved b/Package.resolved index 9e39434..34cf9a8 100644 --- a/Package.resolved +++ b/Package.resolved @@ -63,6 +63,15 @@ "revision": "1d28d48e071727f4558a8a4bb1894472abc47a58", "version": "1.9.2" } + }, + { + "package": "XMLCoder", + "repositoryURL": "https://github.com/MaxDesiatov/XMLCoder", + "state": { + "branch": null, + "revision": "b8fa7fe4c0849f8a64cc246e71c5d976809222e4", + "version": "0.12.0" + } } ] }, diff --git a/Package.swift b/Package.swift index 56c4fd7..94cc074 100644 --- a/Package.swift +++ b/Package.swift @@ -17,6 +17,7 @@ let package = Package( dependencies: [ .package(url: "https://github.com/apple/swift-crypto.git", from: "1.1.3"), .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.0.0"), + .package(url: "https://github.com/MaxDesiatov/XMLCoder", from: "0.12.0"), ], targets: [ .target( @@ -26,6 +27,7 @@ let package = Package( "CNemIDBoringSSL", .product(name: "Crypto", package: "swift-crypto"), .product(name: "AsyncHTTPClient", package: "async-http-client"), + "XMLCoder", ] ), .target(name: "CNemIDBoringSSL"), diff --git a/Sources/NemID/NemIDConfiguration.swift b/Sources/NemID/NemIDConfiguration.swift index f65d558..982b873 100644 --- a/Sources/NemID/NemIDConfiguration.swift +++ b/Sources/NemID/NemIDConfiguration.swift @@ -3,8 +3,18 @@ import Foundation public struct NemIDConfiguration { /// The certificate of the OCES service-provider as a Base64 encoded DER. public let spCertificate: String + /// The service provider ID supplied by NemID (also known as SPID) + public let serviceProviderID: String + /// The NemID environment to use + public let environment: NemIDEnvironment - public init(spCertificate: String) { + public init( + spCertificate: String, + serviceProviderID: String, + environment: NemIDEnvironment + ) { self.spCertificate = spCertificate + self.serviceProviderID = serviceProviderID + self.environment = environment } } diff --git a/Sources/NemID/NemIDEnvironment.swift b/Sources/NemID/NemIDEnvironment.swift new file mode 100644 index 0000000..38e49fa --- /dev/null +++ b/Sources/NemID/NemIDEnvironment.swift @@ -0,0 +1,17 @@ +import Foundation + +public struct NemIDEnvironment { + let pidCPRMatchEndpoint: String + + init(pidCPRMatchEndpoint: String) { + self.pidCPRMatchEndpoint = pidCPRMatchEndpoint + } + + static let production = NemIDEnvironment( + pidCPRMatchEndpoint: "https://pidws.certifikat.dk/pid_serviceprovider_server/pidxml/" + ) + + static let preproduction = NemIDEnvironment( + pidCPRMatchEndpoint: "https://pidws.pp.certifikat.dk/pid_serviceprovider_server/pidxml/" + ) +} diff --git a/Sources/NemID/PID-CPR Match/HTTPPIDCPRMatchClient.swift b/Sources/NemID/PID-CPR Match/HTTPPIDCPRMatchClient.swift new file mode 100644 index 0000000..96a3449 --- /dev/null +++ b/Sources/NemID/PID-CPR Match/HTTPPIDCPRMatchClient.swift @@ -0,0 +1,60 @@ +import Foundation +import NIO +import AsyncHTTPClient +import XMLCoder +import Logging + +struct HTTPPIDCPRMatchClient: PIDCPRMatchClient { + enum HTTPPIDCPRMatchClientError: Error { + case failedToBuildRequest + } + + private let configuration: NemIDConfiguration + private let eventLoop: EventLoop + private let xmlEncoder = XMLEncoder() + private let httpClient: HTTPClient + private let logger: Logger + + init( + configuration: NemIDConfiguration, + eventLoop: EventLoop, + httpClient: HTTPClient, + logger: Logger + ) { + self.configuration = configuration + self.eventLoop = eventLoop + self.httpClient = httpClient + self.logger = logger + } + + func verifyPID(_ pid: String, matches cpr: String) -> EventLoopFuture { + do { + let request = PIDCPRMatchRequest( + method: .init( + request: .init( + id: UUID().uuidString, + serviceProviderID: configuration.serviceProviderID, + pid: pid, + cpr: cpr + ) + ) + ) + + let requestData = try xmlEncoder.encode(request, withRootKey: "method", header: .init(version: 1.0, encoding: "iso-8859-1")) + guard let requestString = String(data: requestData, encoding: .utf8) else { + throw HTTPPIDCPRMatchClientError.failedToBuildRequest + } + let formEncodedRequest = try "PID_REQUEST=\(requestString)".urlEncoded() + + var httpRequest = try HTTPClient.Request(url: configuration.environment.pidCPRMatchEndpoint, method: .POST) + httpRequest.headers.add(name: "Content-Type", value: "application/x-www-form-urlencoded") + httpRequest.body = .string(formEncodedRequest) + + #warning("need to add certificate to request") + fatalError() +// return httpClient.execute(request: httpRequest, eventLoop: .delegate(on: eventLoop), logger: logger) + } catch { + return eventLoop.makeFailedFuture(error) + } + } +} diff --git a/Sources/NemID/PID-CPR Match/Models/PIDCPRMatchRequest.swift b/Sources/NemID/PID-CPR Match/Models/PIDCPRMatchRequest.swift new file mode 100644 index 0000000..7f9d4aa --- /dev/null +++ b/Sources/NemID/PID-CPR Match/Models/PIDCPRMatchRequest.swift @@ -0,0 +1,47 @@ +import Foundation +import XMLCoder + +struct PIDCPRMatchRequest: Encodable { + struct Method: Encodable, DynamicNodeEncoding { + struct Request: Encodable, DynamicNodeEncoding { + let id: String + let serviceProviderID: String + let pid: String + let cpr: String + + enum CodingKeys: String, CodingKey { + case id + case serviceProviderID = "serviceId" + case pid + case cpr + } + + static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding { + switch key { + case CodingKeys.id: return .attribute + default: return .element + } + } + } + + let name: String = "pidCprRequest" + let version: String = "1.0" + let request: Request + + enum CodingKeys: String, CodingKey { + case name + case version + case request + } + + static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding { + switch key { + case CodingKeys.name: return .attribute + case CodingKeys.version: return .attribute + default: return .element + } + } + } + + let method: Method +} diff --git a/Sources/NemID/PID-CPR Match/PIDCPRMatchClient.swift b/Sources/NemID/PID-CPR Match/PIDCPRMatchClient.swift new file mode 100644 index 0000000..6ebfa52 --- /dev/null +++ b/Sources/NemID/PID-CPR Match/PIDCPRMatchClient.swift @@ -0,0 +1,7 @@ +import Foundation +import NIO + +public protocol PIDCPRMatchClient { + /// Verifies that a given User PID`pid` matches a danish CPR number `cpr`. + func verifyPID(_ pid: String, matches cpr: String) -> EventLoopFuture +} diff --git a/Sources/NemID/Utilities/String+urlEncoded.swift b/Sources/NemID/Utilities/String+urlEncoded.swift new file mode 100644 index 0000000..368fb56 --- /dev/null +++ b/Sources/NemID/Utilities/String+urlEncoded.swift @@ -0,0 +1,25 @@ +import Foundation + +/* https://github.com/vapor/vapor/blob/062a4088a26ff9e41daacb8aa311f0289232d814/Sources/Vapor/URLEncodedForm/URLEncodedFormSerializer.swift */ +extension String { + /// Prepares a `String` for inclusion in form-urlencoded data. + func urlEncoded(codingPath: [CodingKey] = []) throws -> String { + guard let result = self.addingPercentEncoding( + withAllowedCharacters: _allowedCharacters + ) else { + throw EncodingError.invalidValue(self, EncodingError.Context( + codingPath: codingPath, + debugDescription: "Unable to add percent encoding to \(self)" + )) + } + return result + } +} + +/// Characters allowed in form-urlencoded data. +private var _allowedCharacters: CharacterSet = { + var allowed = CharacterSet.urlQueryAllowed + // these symbols are reserved for url-encoded form + allowed.remove(charactersIn: "?&=[];+") + return allowed +}() diff --git a/Tests/NemIDTests/ParameterSignerTests.swift b/Tests/NemIDTests/ParameterSignerTests.swift index 4eba934..e0c35e8 100644 --- a/Tests/NemIDTests/ParameterSignerTests.swift +++ b/Tests/NemIDTests/ParameterSignerTests.swift @@ -15,7 +15,7 @@ final class ParameterSignerTests: XCTestCase { let rsaKey = try RSAKey.private(pem: rsaPrivateKey) let rsaSigner = RSASigner(key: rsaKey) - let configuration = NemIDConfiguration(spCertificate: "cert") + let configuration = NemIDConfiguration(spCertificate: "cert", serviceProviderID: "", environment: .preproduction) let signer = NemIDParametersSigner(rsaSigner: rsaSigner, configuration: configuration) let signedParameters = try signer.sign(parameters) From 2d279fbe60571bdcab0f10656b6b158798c2f804 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Sun, 21 Mar 2021 13:27:33 +0100 Subject: [PATCH 11/22] Make stuff public --- Sources/NemID/NemIDEnvironment.swift | 4 +- Sources/NemID/NemIDLoginService.swift | 41 +++++++++++++++---- ...swift => HTTPNemIDPIDCPRMatchClient.swift} | 2 +- ...ent.swift => NemIDPIDCPRMatchClient.swift} | 2 +- 4 files changed, 37 insertions(+), 12 deletions(-) rename Sources/NemID/PID-CPR Match/{HTTPPIDCPRMatchClient.swift => HTTPNemIDPIDCPRMatchClient.swift} (97%) rename Sources/NemID/PID-CPR Match/{PIDCPRMatchClient.swift => NemIDPIDCPRMatchClient.swift} (82%) diff --git a/Sources/NemID/NemIDEnvironment.swift b/Sources/NemID/NemIDEnvironment.swift index 38e49fa..ff597d6 100644 --- a/Sources/NemID/NemIDEnvironment.swift +++ b/Sources/NemID/NemIDEnvironment.swift @@ -7,11 +7,11 @@ public struct NemIDEnvironment { self.pidCPRMatchEndpoint = pidCPRMatchEndpoint } - static let production = NemIDEnvironment( + public static let production = NemIDEnvironment( pidCPRMatchEndpoint: "https://pidws.certifikat.dk/pid_serviceprovider_server/pidxml/" ) - static let preproduction = NemIDEnvironment( + public static let preproduction = NemIDEnvironment( pidCPRMatchEndpoint: "https://pidws.pp.certifikat.dk/pid_serviceprovider_server/pidxml/" ) } diff --git a/Sources/NemID/NemIDLoginService.swift b/Sources/NemID/NemIDLoginService.swift index 911f9c6..229f2c2 100644 --- a/Sources/NemID/NemIDLoginService.swift +++ b/Sources/NemID/NemIDLoginService.swift @@ -4,7 +4,16 @@ import Logging import AsyncHTTPClient public protocol NemIDLoginService { + /// Signs the supplied parameters and returns the parameters as signed, ready to be sent to the client. + func signParameters(_ parameters: NemIDUnsignedClientParameters) -> EventLoopFuture + + /// Validates a XMLDSig message from client, according to the NemID documentation, and returns the certificate user. + /// - Parameters: + /// - response: The XMLDSig xml-document as UTF-8 encoded bytes + func validateAndExtractUser(fromResponse response: [UInt8]) -> EventLoopFuture + func delegating(to eventLoop: EventLoop) -> Self + func logging(to logger: Logger) -> Self } public struct LiveNemIDLoginService: NemIDLoginService { @@ -12,27 +21,43 @@ public struct LiveNemIDLoginService: NemIDLoginService { private let logger: Logger private let eventLoop: EventLoop private let configuration: NemIDConfiguration - private let responseHandler: NemIDResponseHandler - private let parametersSigner: NemIDParametersSigner public init(eventLoop: EventLoop, httpClient: HTTPClient, logger: Logger, configuration: NemIDConfiguration) { self.eventLoop = eventLoop self.httpClient = httpClient self.logger = logger self.configuration = configuration - responseHandler = NemIDResponseHandler( + } + + public func validateAndExtractUser(fromResponse response: [UInt8]) -> EventLoopFuture { + let responseHandler = NemIDResponseHandler( xmlParser: libxml2XMLDSigParser(), certificateExtractor: DefaultCertificateExtractor(), ocspClient: HTTPOCSPClient(client: httpClient, eventLoop: eventLoop, logger: logger), - eventLoop: eventLoop - ) - parametersSigner = NemIDParametersSigner( - rsaSigner: RSASigner(key: try! .private(pem: "")), - configuration: configuration + eventLoop: self.eventLoop ) + + return responseHandler.verifyAndExtractUser(fromXML: response) + } + + public func signParameters(_ parameters: NemIDUnsignedClientParameters) -> EventLoopFuture { + do { + let parametersSigner = NemIDParametersSigner( + rsaSigner: RSASigner(key: try! .private(pem: "")), + configuration: self.configuration + ) + + return eventLoop.makeSucceededFuture(try parametersSigner.sign(parameters)) + } catch { + return eventLoop.makeFailedFuture(error) + } } public func delegating(to eventLoop: EventLoop) -> LiveNemIDLoginService { LiveNemIDLoginService(eventLoop: eventLoop, httpClient: self.httpClient, logger: self.logger, configuration: self.configuration) } + + public func logging(to logger: Logger) -> LiveNemIDLoginService { + LiveNemIDLoginService(eventLoop: self.eventLoop, httpClient: self.httpClient, logger: logger, configuration: self.configuration) + } } diff --git a/Sources/NemID/PID-CPR Match/HTTPPIDCPRMatchClient.swift b/Sources/NemID/PID-CPR Match/HTTPNemIDPIDCPRMatchClient.swift similarity index 97% rename from Sources/NemID/PID-CPR Match/HTTPPIDCPRMatchClient.swift rename to Sources/NemID/PID-CPR Match/HTTPNemIDPIDCPRMatchClient.swift index 96a3449..b7e5bed 100644 --- a/Sources/NemID/PID-CPR Match/HTTPPIDCPRMatchClient.swift +++ b/Sources/NemID/PID-CPR Match/HTTPNemIDPIDCPRMatchClient.swift @@ -4,7 +4,7 @@ import AsyncHTTPClient import XMLCoder import Logging -struct HTTPPIDCPRMatchClient: PIDCPRMatchClient { +struct HTTPNemIDPIDCPRMatchClient: NemIDPIDCPRMatchClient { enum HTTPPIDCPRMatchClientError: Error { case failedToBuildRequest } diff --git a/Sources/NemID/PID-CPR Match/PIDCPRMatchClient.swift b/Sources/NemID/PID-CPR Match/NemIDPIDCPRMatchClient.swift similarity index 82% rename from Sources/NemID/PID-CPR Match/PIDCPRMatchClient.swift rename to Sources/NemID/PID-CPR Match/NemIDPIDCPRMatchClient.swift index 6ebfa52..30138c2 100644 --- a/Sources/NemID/PID-CPR Match/PIDCPRMatchClient.swift +++ b/Sources/NemID/PID-CPR Match/NemIDPIDCPRMatchClient.swift @@ -1,7 +1,7 @@ import Foundation import NIO -public protocol PIDCPRMatchClient { +public protocol NemIDPIDCPRMatchClient { /// Verifies that a given User PID`pid` matches a danish CPR number `cpr`. func verifyPID(_ pid: String, matches cpr: String) -> EventLoopFuture } From 795a343085839b1442e2ef97d9cf7c2b1108da60 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Fri, 16 Apr 2021 12:32:59 +0200 Subject: [PATCH 12/22] Improve notBefore and notAfter impl. --- Package.swift | 2 +- .../Utilities/ASN1_TIME+timeSinceEpoch.swift | 20 ++++++++++ Sources/NemID/X509/X509Certificate.swift | 40 ++----------------- 3 files changed, 25 insertions(+), 37 deletions(-) create mode 100644 Sources/NemID/Utilities/ASN1_TIME+timeSinceEpoch.swift diff --git a/Package.swift b/Package.swift index 94cc074..876b2bb 100644 --- a/Package.swift +++ b/Package.swift @@ -17,7 +17,7 @@ let package = Package( dependencies: [ .package(url: "https://github.com/apple/swift-crypto.git", from: "1.1.3"), .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.0.0"), - .package(url: "https://github.com/MaxDesiatov/XMLCoder", from: "0.12.0"), + .package(url: "https://github.com/MaxDesiatov/XMLCoder.git", from: "0.12.0"), ], targets: [ .target( diff --git a/Sources/NemID/Utilities/ASN1_TIME+timeSinceEpoch.swift b/Sources/NemID/Utilities/ASN1_TIME+timeSinceEpoch.swift new file mode 100644 index 0000000..3194013 --- /dev/null +++ b/Sources/NemID/Utilities/ASN1_TIME+timeSinceEpoch.swift @@ -0,0 +1,20 @@ +@_implementationOnly import CNemIDBoringSSL + +// https://github.com/apple/swift-nio-ssl +extension UnsafePointer where Pointee == ASN1_TIME { + var timeSinceEpoch: time_t { + let epochTime = CNemIDBoringSSL_ASN1_TIME_new()! + defer { CNemIDBoringSSL_ASN1_TIME_free(epochTime) } + + // This sets the ASN1_TIME to epoch time. + CNemIDBoringSSL_ASN1_TIME_set(epochTime, 0) + var day = CInt(0) + var seconds = CInt(0) + + let rc = CNemIDBoringSSL_ASN1_TIME_diff(&day, &seconds, epochTime, self) + precondition(rc != 0) + + // 86400 seconds in a day + return time_t(day) * 86400 + time_t(seconds) + } +} diff --git a/Sources/NemID/X509/X509Certificate.swift b/Sources/NemID/X509/X509Certificate.swift index 067ab8d..dfb2a2d 100644 --- a/Sources/NemID/X509/X509Certificate.swift +++ b/Sources/NemID/X509/X509Certificate.swift @@ -40,46 +40,14 @@ final class X509Certificate: BIOLoadable { /// Returns the certificate notBefore as a `Date` func notBefore() -> Date? { - guard let asn1Time = CNemIDBoringSSL_X509_get0_notBefore(self.ref) else { return nil } - - var _generalizedTimePtr: UnsafeMutablePointer? - CNemIDBoringSSL_ASN1_TIME_to_generalizedtime(asn1Time, &_generalizedTimePtr) - guard let generalizedTimePtr = _generalizedTimePtr else { return nil } - defer { CNemIDBoringSSL_ASN1_GENERALIZEDTIME_free(generalizedTimePtr) } - - guard let generalizedTimeString = String( - bytesNoCopy: UnsafeMutableRawPointer(mutating: generalizedTimePtr.pointee.data), - length: numericCast(generalizedTimePtr.pointee.length), - encoding: .ascii, - freeWhenDone: false - ) - else { - return nil - } - - return GeneralizedTimeFormatter.toDate(generalizedTimeString) + guard let notBefore = CNemIDBoringSSL_X509_get0_notBefore(self.ref) else { return nil } + return Date(timeIntervalSince1970: TimeInterval(notBefore.timeSinceEpoch)) } /// Returns the certificate notAfter as a `Date` func notAfter() -> Date? { - guard let asn1Time = CNemIDBoringSSL_X509_get0_notAfter(self.ref) else { return nil } - - var _generalizedTimePtr: UnsafeMutablePointer? - CNemIDBoringSSL_ASN1_TIME_to_generalizedtime(asn1Time, &_generalizedTimePtr) - guard let generalizedTimePtr = _generalizedTimePtr else { return nil } - defer { CNemIDBoringSSL_ASN1_GENERALIZEDTIME_free(generalizedTimePtr) } - - guard let generalizedTimeString = String( - bytesNoCopy: UnsafeMutableRawPointer(mutating: generalizedTimePtr.pointee.data), - length: numericCast(generalizedTimePtr.pointee.length), - encoding: .ascii, - freeWhenDone: false - ) - else { - return nil - } - - return GeneralizedTimeFormatter.toDate(generalizedTimeString) + guard let notAfter = CNemIDBoringSSL_X509_get0_notAfter(self.ref) else { return nil } + return Date(timeIntervalSince1970: TimeInterval(notAfter.timeSinceEpoch)) } /// Returns a `Bool` indicating whether this certificate has the "CA" constraint flag set. From 26e1079068b1afa8b2e0c886421ce7a59f711211 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Sun, 18 Apr 2021 17:07:13 +0200 Subject: [PATCH 13/22] Fix various bugs after testing with NemID --- .../Client Parameters/ClientParameters.swift | 18 +- .../Client Parameters/ParametersSigner.swift | 35 ++- .../SignedClientParameters.swift | 18 +- .../UnsignedClientParameters.swift | 5 +- Sources/NemID/NemIDConfiguration.swift | 10 +- Sources/NemID/NemIDError.swift | 280 ++++++++++++++++++ Sources/NemID/NemIDLoginService.swift | 12 +- Sources/NemID/OCSP/OCSPResponse.swift | 52 ++-- Sources/NemID/RSA/RSAKey.swift | 2 +- Sources/NemID/RSA/RSASigner.swift | 22 +- .../NemIDResponseHandler.swift | 50 ++-- .../NemIDResponseHandlerError.swift | 1 + Sources/NemID/Utilities/Bool+NemID.swift | 10 + Sources/NemID/X509/X509Certificate.swift | 52 +++- .../XMLDSigParser/libxml2DSigParser.swift | 6 +- Tests/NemIDTests/ParameterSignerTests.swift | 19 +- .../NemIDTests/libxml2XMLDigParserTests.swift | 125 ++++++++ 17 files changed, 627 insertions(+), 90 deletions(-) create mode 100644 Sources/NemID/NemIDError.swift create mode 100644 Sources/NemID/Utilities/Bool+NemID.swift diff --git a/Sources/NemID/Client Parameters/ClientParameters.swift b/Sources/NemID/Client Parameters/ClientParameters.swift index 3e91da7..1e96acd 100644 --- a/Sources/NemID/Client Parameters/ClientParameters.swift +++ b/Sources/NemID/Client Parameters/ClientParameters.swift @@ -1,11 +1,11 @@ import Foundation -public enum NemIDClientParametersClientFlow: String, Encodable { +public enum NemIDClientParametersClientFlow: String, Codable { /// 2 factor OCES login - case ocesLogin2 + case ocesLogin2 = "OCESLOGIN2" } -public enum NemIDClientParametersClientLanguage: String, Encodable { +public enum NemIDClientParametersClientLanguage: String, Codable { case danish = "DA" case english = "EN" case greenlandic = "KL" @@ -14,18 +14,30 @@ public enum NemIDClientParametersClientLanguage: String, Encodable { public protocol NemIDClientParameters { /// Determines which NemID flow to start var clientFlow: NemIDClientParametersClientFlow { get } + /// Client language var language: NemIDClientParametersClientLanguage { get } + /// The origin of the Service Provider site which will send parameters to the NemID JavaScript client. /// The NemID JavaScript client will abort with APP001 or APP007 if a postMessage command is received from any other origin. var origin: URL? { get } + /// Base64 encoded token returned from the client when the user chooses to remember his user id. /// At next login/signing this parameter must be specified in order to enable the remember user id functionality. var rememberUserID: String? { get } + /// Indicates that the “Remember userid checkbox” should not be initially checked. /// This is only relevant in responsive mode and when REMEMBER_USERID is also set. var rememberUserIDInitialStatus: Bool? { get } + /// Current time when generating parameters. The timestamp parameter is converted to UTC and must match the NemID server time. /// NemID accepts timestamps within the boundaries of +-3 minutes. var timestamp: Date { get } + + /// Indicates if the JS an awaiting app client should send out app approval event + /// The event can be caught outside the JS client using the event handler set-up from section 3.3 where command will be “AwaitingAppApproval + /// + /// TRUE => Enable awaiting client should send out app approval event. + /// Any other value or unset (default) => No event + var enableAwaitingAppApprovalEvent: Bool? { get } } diff --git a/Sources/NemID/Client Parameters/ParametersSigner.swift b/Sources/NemID/Client Parameters/ParametersSigner.swift index 8c1be83..2f39a7c 100644 --- a/Sources/NemID/Client Parameters/ParametersSigner.swift +++ b/Sources/NemID/Client Parameters/ParametersSigner.swift @@ -6,33 +6,38 @@ enum NemIDParameterSigningError: Error { } struct NemIDParametersSigner { - private let rsaSigner: RSASigner private let configuration: NemIDConfiguration - public init(rsaSigner: RSASigner, configuration: NemIDConfiguration) { - self.rsaSigner = rsaSigner + public init(configuration: NemIDConfiguration) { self.configuration = configuration } + /// Follows steps according to NemID documentation 3.2 public func sign(_ parameters: NemIDUnsignedClientParameters) throws -> NemIDSignedClientParameters { - let normalizedData = [UInt8](normalizedParameters(parameters).utf8) + // Step 1: + let normalizedData = try [UInt8](normalizedParameters(parameters).utf8) let digest = SHA256.hash(data: normalizedData) - // RSASigner also SHA256 hashes the data. - let signature = try rsaSigner.sign(normalizedData) + // Step 2: + let signer = RSASigner(key: configuration.privateKey) + let signature = try signer.sign(normalizedData) + + // Step 3: let base64ParamsDigest = Data(digest).base64EncodedString() let base64SignedDigest = Data(signature).base64EncodedString() - return NemIDSignedClientParameters( + // Step 4: + return try NemIDSignedClientParameters( clientFlow: parameters.clientFlow, language: parameters.language, origin: parameters.origin, rememberUserID: parameters.rememberUserID, rememberUserIDInitialStatus: parameters.rememberUserIDInitialStatus, - SPCert: configuration.spCertificate, + spCert: configuration.spCertificate.toBase64EncodedDER(), timestamp: parameters.timestamp, digestSignature: base64SignedDigest, - paramsDigest: base64ParamsDigest + paramsDigest: base64ParamsDigest, + enableAwaitingAppApprovalEvent: parameters.enableAwaitingAppApprovalEvent ) } @@ -40,12 +45,12 @@ struct NemIDParametersSigner { /// /// 1. The parameters are sorted alphabetically by name. The sorting is case-insensitive. /// 2. Each parameter is concatenated to the result string as an alternating sequence of name and value - private func normalizedParameters(_ unsignedParameters: NemIDUnsignedClientParameters) -> String { - var parameters: [String: String] = [ + private func normalizedParameters(_ unsignedParameters: NemIDUnsignedClientParameters) throws -> String { + var parameters: [String: String] = try [ "CLIENTFLOW": unsignedParameters.clientFlow.rawValue, "LANGUAGE": unsignedParameters.language.rawValue, - "SP_CERT": configuration.spCertificate, - "TIMESTAMP": String(unsignedParameters.timestamp.timeIntervalSince1970 * 1000) + "SP_CERT": configuration.spCertificate.toBase64EncodedDER(), + "TIMESTAMP": String(Int(unsignedParameters.timestamp.timeIntervalSince1970 * 1000)) ] if let origin = unsignedParameters.origin?.absoluteString { @@ -58,6 +63,10 @@ struct NemIDParametersSigner { parameters["REMEMBER_USERID_INITIAL_STATUS"] = rememberUserIDInitialStatus ? "TRUE": "FALSE" } + if let enableAwaitingAppApprovalEvent = unsignedParameters.enableAwaitingAppApprovalEvent { + parameters["ENABLE_AWAITING_APP_APPROVAL_EVENT"] = enableAwaitingAppApprovalEvent ? "TRUE" : "FALSE" + } + let sortedAlphabeticallyByKeys = parameters.sorted(by: { $0.key.lowercased() < $1.key.lowercased()} ) return sortedAlphabeticallyByKeys .reduce(into: "") { result, parameter in diff --git a/Sources/NemID/Client Parameters/SignedClientParameters.swift b/Sources/NemID/Client Parameters/SignedClientParameters.swift index 3543f92..d811d9b 100644 --- a/Sources/NemID/Client Parameters/SignedClientParameters.swift +++ b/Sources/NemID/Client Parameters/SignedClientParameters.swift @@ -1,18 +1,19 @@ import Foundation /// This is the type which the SP-server will send to NemID client as JSON. -public struct NemIDSignedClientParameters: NemIDClientParameters, Encodable { +public struct NemIDSignedClientParameters: NemIDClientParameters, Codable { public let clientFlow: NemIDClientParametersClientFlow public let language: NemIDClientParametersClientLanguage public let origin: URL? public let rememberUserID: String? public let rememberUserIDInitialStatus: Bool? - public let SPCert: String + public let spCert: String public let timestamp: Date /// Base64 encoded RSA256 signature of the calculated parameter digest. public let digestSignature: String /// Base64 encoded representation of the calculated parameter digest. public let paramsDigest: String + public let enableAwaitingAppApprovalEvent: Bool? enum CodingKeys: String, CodingKey { case clientFlow = "CLIENTFLOW" @@ -20,21 +21,24 @@ public struct NemIDSignedClientParameters: NemIDClientParameters, Encodable { case origin = "ORIGIN" case rememberUserID = "REMEMBER_USERID" case rememberUserIDInitialStatus = "REMEMBER_USERID_INITIAL_STATUS" - case SPCert = "SP_CERT" + case spCert = "SP_CERT" case timestamp = "TIMESTAMP" case digestSignature = "DIGEST_SIGNATURE" case paramsDigest = "PARAMS_DIGEST" + case enableAwaitingAppApprovalEvent = "ENABLE_AWAITING_APP_APPROVAL_EVENT" } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(clientFlow, forKey: .clientFlow) try container.encode(language, forKey: .language) - try container.encode(origin, forKey: .origin) - try container.encode(rememberUserID, forKey: .rememberUserID) - try container.encode(rememberUserIDInitialStatus, forKey: .rememberUserIDInitialStatus) - try container.encode(timestamp.timeIntervalSince1970 * 1000, forKey: .timestamp) + try container.encodeIfPresent(origin, forKey: .origin) + try container.encodeIfPresent(rememberUserID, forKey: .rememberUserID) + try container.encodeIfPresent(rememberUserIDInitialStatus?.nemIDRepresentation, forKey: .rememberUserIDInitialStatus) + try container.encode(spCert, forKey: .spCert) + try container.encode(String(Int(timestamp.timeIntervalSince1970 * 1000)), forKey: .timestamp) try container.encode(digestSignature, forKey: .digestSignature) try container.encode(paramsDigest, forKey: .paramsDigest) + try container.encodeIfPresent(enableAwaitingAppApprovalEvent?.nemIDRepresentation, forKey: .enableAwaitingAppApprovalEvent) } } diff --git a/Sources/NemID/Client Parameters/UnsignedClientParameters.swift b/Sources/NemID/Client Parameters/UnsignedClientParameters.swift index 38628a7..99df47f 100644 --- a/Sources/NemID/Client Parameters/UnsignedClientParameters.swift +++ b/Sources/NemID/Client Parameters/UnsignedClientParameters.swift @@ -7,6 +7,7 @@ public struct NemIDUnsignedClientParameters: NemIDClientParameters { public let rememberUserID: String? public let rememberUserIDInitialStatus: Bool? public let timestamp: Date + public let enableAwaitingAppApprovalEvent: Bool? public init( clientFlow: NemIDClientParametersClientFlow, @@ -14,7 +15,8 @@ public struct NemIDUnsignedClientParameters: NemIDClientParameters { origin: URL?, rememberUserID: String?, rememberUserIDInitialStatus: Bool?, - timestamp: Date + timestamp: Date, + enableAwaitingAppApprovalEvent: Bool? ) { self.clientFlow = clientFlow self.language = language @@ -22,5 +24,6 @@ public struct NemIDUnsignedClientParameters: NemIDClientParameters { self.rememberUserID = rememberUserID self.rememberUserIDInitialStatus = rememberUserIDInitialStatus self.timestamp = timestamp + self.enableAwaitingAppApprovalEvent = enableAwaitingAppApprovalEvent } } diff --git a/Sources/NemID/NemIDConfiguration.swift b/Sources/NemID/NemIDConfiguration.swift index 982b873..b3386ea 100644 --- a/Sources/NemID/NemIDConfiguration.swift +++ b/Sources/NemID/NemIDConfiguration.swift @@ -1,19 +1,23 @@ import Foundation public struct NemIDConfiguration { - /// The certificate of the OCES service-provider as a Base64 encoded DER. - public let spCertificate: String + /// The certificate of the OCES service-provider. + public let spCertificate: X509Certificate + /// The private key for the OCES service provider. + public let privateKey: RSAKey /// The service provider ID supplied by NemID (also known as SPID) public let serviceProviderID: String /// The NemID environment to use public let environment: NemIDEnvironment public init( - spCertificate: String, + spCertificate: X509Certificate, + privateKey: RSAKey, serviceProviderID: String, environment: NemIDEnvironment ) { self.spCertificate = spCertificate + self.privateKey = privateKey self.serviceProviderID = serviceProviderID self.environment = environment } diff --git a/Sources/NemID/NemIDError.swift b/Sources/NemID/NemIDError.swift new file mode 100644 index 0000000..edffc08 --- /dev/null +++ b/Sources/NemID/NemIDError.swift @@ -0,0 +1,280 @@ +import Foundation + +/// See a full list at [NemID Errors](https://www.nets.eu/dk-da/kundeservice/nemid-tjenesteudbyder/NemID-tjenesteudbyderpakken/Documents/NemID%20Error%20Codes.pdf) +public enum NemIDError: String { + case APP001 + case APP002 + case APP003 + case APP004 + case APP007 + case APP008 + case APP009 + case APP010 + + case AUTH001 + case AUTH004 + case AUTH005 + case AUTH006 + case AUTH007 + case AUTH008 + case AUTH009 + case AUTH010 + case AUTH011 + case AUTH012 + case AUTH013 + case AUTH017 + case AUTH018 + case AUTH019 + case AUTH020 + case AUTH021 + + case CAN001 + case CAN002 + case CAN003 + case CAN004 + case CAN005 + case CAN007 + case CAN008 + + case LOCK001 + case LOCK002 + case LOCK003 + + case SRV001 + case SRV002 + case SRV003 + case SRV004 + case SRV005 + case SRV006 + case SRV007 + case SRV008 + case SRV010 + case SRV011 + case SRV012 + + case OCES001 + case OCES002 + case OCES003 + case OCES004 + case OCES005 + case OCES006 + + /// Returns a text suitable for showing the user in English. + public var englishDescrption: String { self.metadata.englishDescription } + + /// Returns a text suitable for showing the user in Danish. + public var danishDescription: String { self.metadata.danishDescription } + + // MARK: Private + private struct Metadata { + let englishDescription: String + let danishDescription: String + + static var `default`: Self = .init( + englishDescription: "A technical error has occured. Contact the service provider if the problem persists.", + danishDescription: "Der er opstået en teknisk fejl. Kontakt tjenesteudbyder hvis problemet forsætter." + ) + } + + private var metadata: Metadata { + switch self { + case .APP001: + return .default + case .APP002: + return .default + case .APP003: + return .default + case .APP004: + return .default + case .APP007: + return .default + case .APP008: + return .default + case .APP009: + return .default + case .APP010: + return .default + case .AUTH001: + return .init( + englishDescription: "Your NemID is blocked. Please contact NemID support.", + danishDescription: "Dit NemID er spærret. Kontakt NemID support." + ) + case .AUTH004: + return .init( + englishDescription: "Your NemID is temporarily locked and you cannot log on until the 8 hour time lock has been lifted.", + danishDescription: "Dit NemID er midlertidigt låst i 8 timer og du kan ikke logge på før spærringen er ophævet." + ) + case .AUTH005: + return .init( + englishDescription: "Your NemID is blocked. Please contact NemID support.", + danishDescription: "Dit NemID er spærret. Kontakt NemID support." + ) + case .AUTH006: + return .init( + englishDescription: "You have used all the codes on your code card.", + danishDescription: "Du har brugt alle nøgler på nøglekortet." + ) + case .AUTH007: + return .init( + englishDescription: "Your NemID password is blocked due to too many failed password attempts.", + danishDescription: "Din NemID-adgangskode er spærret på grund af for mange fejlede forsøg." + ) + case .AUTH008: + return .init( + englishDescription: "Your NemID is not active and you need support to issue a new activation password to activate.", + danishDescription: "Dit NemID er ikke aktivt og du skal bestille en ny midlertidig adgangskode til aktivering hos support." + ) + case .AUTH009: + return .default + case .AUTH010: + return .default + case .AUTH011: + return .init( + englishDescription: "NemID login on mobile does not support authentication using a temporary password.", + danishDescription: "NemID på mobil understøtter ikke brug af midlertidig adgangskode." + ) + case .AUTH012: + return .default + case .AUTH013: + return .default + case .AUTH017: + return .init( + englishDescription: "Something in the browser environment has caused NemID to stop working. This could be because of an incompatible plug- in, too restrictive privacy settings or other environment factors.", + danishDescription: "En teknisk fejl i browseren gør at NemID ikke kan starte." + ) + case .AUTH018: + return .init( + englishDescription: "Your code app is revoked. To use it again please reactivate it.", + danishDescription: "Din nøgleapp er spærret. For at bruge den igen skal den genaktiveres." + ) + case .AUTH019: + return .init( + englishDescription: "It is not possible to login with a code card, please use a code app or code token.", + danishDescription: "Det er ikke muligt at logge ind med nøglekort, brug anden løsning nøgleapp eller nøgleviser." + ) + case .AUTH020: + return .init( + englishDescription: "Unable to login with 1-factor, please try with 2-factor login.", + danishDescription: "Kunne ikke logge ind med 1- faktor, prøv med 2-faktor login." + ) + case .AUTH021: + return .init( + englishDescription: "This NemID is no longer valid due to insufficient identification of the user", + danishDescription: "Det er ikke længere muligt at logge ind med dette NemID pga. manglende opdatering af identitetsoplysninger." + ) + case .CAN001: + return .init( + englishDescription: "You have cancelled the activation of NemID after submitting the activation password.", + danishDescription: "Du har afbrudt aktiveringen efter du har brugt den midlertidige adgangskode." + ) + case .CAN002: + return .init(englishDescription: "You have canelled the login.", danishDescription: "Du har afbrudt login.") + case .CAN003: + return .init( + englishDescription: "The connection to the application has timed out or has been interrupted by another app", + danishDescription: "Forbindelsen til applikationen er timet ud eller er blevet afbrudt af en anden app." + ) + case .CAN004: + return .init(englishDescription: "The session is cancelled", danishDescription: "Session er afbrudt") + case .CAN005: + return .init( + englishDescription: "You took too long to authenticate the request you had sent to your code app.", + danishDescription: "Det tog for lang tid, før du godkendte den anmodning, du havde sendt til din nøgleapp" + ) + case .CAN007: + return .init( + englishDescription: "You rejected your code app authentication request. If this was incorrect, you can submit a new request after clicking “OK” to finish.", + danishDescription: "Du har afvist din anmodning om godkendelse i din nøgleapp. Hvis det var en fejl, kan du sende en ny anmodning, når du har afsluttet ved at klikke på ”Ok”." + ) + case .CAN008: + return .init( + englishDescription: "You sent a new authentication request to your code app overwriting an existing one.", + danishDescription: "Du har sendt en ny anmodning til godkendelse i din nøgleapp, som overskriver en eksisterende." + ) + case .LOCK001: + return .init( + englishDescription: "You have used the wrong user ID or password too many times. Your NemID is now blocked for 8 hours after which you can try again.", + danishDescription: "Du har angivet forkert bruger- id eller adgangskode for mange gange. NemID er nu spærret i 8 timer, hvorefter du kan forsøge igen" + ) + case .LOCK002: + return .init( + englishDescription: "You have used a wrong password too many times. Your NemID is blocked and cannot be used.", + danishDescription: "Du har angivet en forkert adgangskode for mange gange. Dit NemID er spærret." + ) + case .LOCK003: + return .init( + englishDescription: "You have entered a wrong NemID key too many times. Your NemID is blocked and cannot be used.", + danishDescription: "Du har angivet forkert NemID nøgle for mange gange. Dit NemID er spærret." + ) + case .SRV001: + return .default + case .SRV002: + return .default + case .SRV003: + return .default + case .SRV004: + return .default + case .SRV005: + return .default + case .SRV006: + return .init(englishDescription: "Time limit exceeded", danishDescription: "Tidsgrænse er overskredet.") + case .SRV007: + return .init( + englishDescription: "Please update to the most recent version of the application", + danishDescription: "Opdater venligst til den nyeste version af applikationen." + ) + case .SRV008: + return .default + case .SRV010: + return .default + case .SRV011: + return .default + case .SRV012: + return .init(englishDescription: "IP address changed in flow", danishDescription: "IP adresse ændredes under transkationen.") + case .OCES001: + return .init(englishDescription: "You only have NemID for online banking.", danishDescription: "Du har kun NemID til netbank.") + case .OCES002: + return .init( + englishDescription: "If you wish to use NemID for other services than online banking, you have to affiliate a public digital signature to your NemID.", + danishDescription: "Ønsker du at bruge NemID til andet end netbank, skal du først tilknytte en offentlig digital signatur." + ) + case .OCES003: + return .init( + englishDescription: "You have attempted to log on using a NemID with no public digital signature", + danishDescription: "Der er ikke tilknyttet en offentlig digital signatur til det NemID du har forsøgt at logge på med." + ) + case .OCES004: + return .init( + englishDescription: "You can only use this NemID for your online banking service.", + danishDescription: "Du kan kun bruge dette NemID til netbank." + ) + case .OCES005: + return .init( + englishDescription: "Issuing your public digital signature failed.", + danishDescription: "Udstedelsen af din offentlige digitale signatur mislykkedes." + ) + case .OCES006: + return .init( + englishDescription: "You currently don’t have an active public digital signature (OCES certificate)affiliated with your NemID.", + danishDescription: "Du har ikke en aktiv offentlig digital signatur tilknyttet NemID i øjeblikket." + ) + } + } +} + +// MARK: - Codable +extension NemIDError: Codable {} + +// MARK: - CustomStringConvertible +extension NemIDError: CustomStringConvertible { + public var description: String { + "NemIDError.\(self.rawValue): \(self.englishDescrption)" + } +} + +// MARK: - LocalizedError +extension NemIDError: LocalizedError { + public var errorDescription: String? { + self.englishDescrption + } +} diff --git a/Sources/NemID/NemIDLoginService.swift b/Sources/NemID/NemIDLoginService.swift index 229f2c2..2f29d5c 100644 --- a/Sources/NemID/NemIDLoginService.swift +++ b/Sources/NemID/NemIDLoginService.swift @@ -10,7 +10,7 @@ public protocol NemIDLoginService { /// Validates a XMLDSig message from client, according to the NemID documentation, and returns the certificate user. /// - Parameters: /// - response: The XMLDSig xml-document as UTF-8 encoded bytes - func validateAndExtractUser(fromResponse response: [UInt8]) -> EventLoopFuture + func validateAndExtractUser(fromResponse response: Data) -> EventLoopFuture func delegating(to eventLoop: EventLoop) -> Self func logging(to logger: Logger) -> Self @@ -29,7 +29,7 @@ public struct LiveNemIDLoginService: NemIDLoginService { self.configuration = configuration } - public func validateAndExtractUser(fromResponse response: [UInt8]) -> EventLoopFuture { + public func validateAndExtractUser(fromResponse response: Data) -> EventLoopFuture { let responseHandler = NemIDResponseHandler( xmlParser: libxml2XMLDSigParser(), certificateExtractor: DefaultCertificateExtractor(), @@ -37,16 +37,12 @@ public struct LiveNemIDLoginService: NemIDLoginService { eventLoop: self.eventLoop ) - return responseHandler.verifyAndExtractUser(fromXML: response) + return responseHandler.verifyAndExtractUser(from: response) } public func signParameters(_ parameters: NemIDUnsignedClientParameters) -> EventLoopFuture { do { - let parametersSigner = NemIDParametersSigner( - rsaSigner: RSASigner(key: try! .private(pem: "")), - configuration: self.configuration - ) - + let parametersSigner = NemIDParametersSigner(configuration: self.configuration) return eventLoop.makeSucceededFuture(try parametersSigner.sign(parameters)) } catch { return eventLoop.makeFailedFuture(error) diff --git a/Sources/NemID/OCSP/OCSPResponse.swift b/Sources/NemID/OCSP/OCSPResponse.swift index a3ec7d1..eb3b624 100644 --- a/Sources/NemID/OCSP/OCSPResponse.swift +++ b/Sources/NemID/OCSP/OCSPResponse.swift @@ -29,7 +29,7 @@ struct OCSPResponse { throw OCSPResponseError.failedToParseResponse } - // Parse response status + // Parse responseStatus var responseStatusValue: UInt8 = 0 guard CNemIDBoringSSL_CBS_get_u8(&responseStatusCBS, &responseStatusValue) == 1 else { throw OCSPResponseError.failedToParseResponse @@ -40,7 +40,7 @@ struct OCSPResponse { } self.responseStatus = responseStatus - // Parse response bytes + // Parse responseBytes ([0] EXPLICIT ResponseBytes OPTIONAL) var responseBytesCBS = CBS() var isResponseBytesPresent: Int32 = 0 guard CNemIDBoringSSL_CBS_get_optional_asn1( @@ -56,21 +56,23 @@ struct OCSPResponse { return } - // Parse contents of responseBytes var responseBytesChildCBS = CBS() guard CNemIDBoringSSL_CBS_get_asn1(&responseBytesCBS, &responseBytesChildCBS, CBS_ASN1_SEQUENCE) == 1 else { throw OCSPResponseError.failedToParseResponse } - // Parse responseType + // Parse responseType (OBJECT IDENTIFIER) var responseTypeCBS = CBS() guard CNemIDBoringSSL_CBS_get_asn1(&responseBytesChildCBS, &responseTypeCBS, CBS_ASN1_OBJECT) == 1 else { throw OCSPResponseError.failedToParseResponse } + let responseTypeNID = CNemIDBoringSSL_OBJ_cbs2nid(&responseTypeCBS) - guard responseTypeNID == NID_id_pkix_OCSP_basic else { throw OCSPResponseError.responseTypeWasNotOCSPBasic } + guard responseTypeNID == NID_id_pkix_OCSP_basic else { + throw OCSPResponseError.responseTypeWasNotOCSPBasic + } - // Parse response + // Parse response (OCTET STRING) var responseCBS = CBS() guard CNemIDBoringSSL_CBS_get_asn1(&responseBytesChildCBS, &responseCBS, CBS_ASN1_OCTETSTRING) == 1 else { throw OCSPResponseError.failedToParseResponse @@ -120,13 +122,13 @@ extension OCSPResponse { let certs: [X509Certificate] init(cbs: UnsafeMutablePointer) throws { - // Parse tbsResponseData + // Parse tbsResponseData (ResponseData) var tbsResponseDataCBS = CBS() guard CNemIDBoringSSL_CBS_get_asn1(cbs, &tbsResponseDataCBS, CBS_ASN1_SEQUENCE) == 1 else { throw OCSPResponseError.failedToParseResponse } - // Parse signature algorithm + // Parse signature algorithm (AlgorithmIdentifier) var signatureAlgorithmCBS = CBS() guard CNemIDBoringSSL_CBS_get_asn1(cbs, &signatureAlgorithmCBS, CBS_ASN1_SEQUENCE) == 1 else { throw OCSPResponseError.failedToParseResponse @@ -136,7 +138,7 @@ extension OCSPResponse { throw OCSPResponseError.failedToParseResponse } let algorithmNID = CNemIDBoringSSL_OBJ_cbs2nid(&algorithmCBS) - guard let algorithm = BasicOCSPResponse.SignatureAlgorithm(nid: algorithmNID) else { + guard let algorithm = SignatureAlgorithm(nid: algorithmNID) else { throw OCSPResponseError.unknownSignatureAlgorithm(algorithmNID) } @@ -152,28 +154,44 @@ extension OCSPResponse { } guard let signatureBytesPtr = signatureBytes else { throw OCSPResponseError.failedToParseResponse } defer { CNemIDBoringSSL_OPENSSL_free(signatureBytesPtr) } - let signature = [UInt8](UnsafeBufferPointer(start: signatureBytesPtr, count: signatureLength)) - // Parse certs - #warning("requires access to nemid to verify...") + // Parse certs ([0] EXPLICIT SEQUENCE OF Certificate OPTIONAL) + var _certs = [X509Certificate]() var certsCBS = CBS() var isCertsPresent: Int32 = 0 guard CNemIDBoringSSL_CBS_get_optional_asn1( - cbs, &certsCBS, + cbs, + &certsCBS, &isCertsPresent, - CBS_ASN1_SEQUENCE | CBS_ASN1_CONTEXT_SPECIFIC | 0 + CBS_ASN1_CONSTRUCTED | CBS_ASN1_CONTEXT_SPECIFIC | 0 ) == 1 else { throw OCSPResponseError.failedToParseResponse } if isCertsPresent == 1 { - print("certs is present") + // We only handle the case of 1 certificate. + var certCBS = CBS() + guard CNemIDBoringSSL_CBS_get_asn1(&certsCBS, &certCBS, CBS_ASN1_SEQUENCE) == 1 else { + throw OCSPResponseError.failedToParseResponse + } + + // Get DER representation of certificate. + var _certOutPtr: UnsafeMutablePointer? + var certOutLength = 0 + guard + CNemIDBoringSSL_CBS_stow(&certCBS, &_certOutPtr, &certOutLength) == 1, + let certOutPtr = _certOutPtr + else { + throw OCSPResponseError.failedToParseResponse + } + defer { CNemIDBoringSSL_OPENSSL_free(certOutPtr) } + try _certs.append(X509Certificate(der: [UInt8](UnsafeBufferPointer(start: certOutPtr, count: certOutLength)))) } self.tbsResponseData = try ResponseData(cbs: &tbsResponseDataCBS) self.signatureAlgorithm = algorithm - self.signature = signature - self.certs = [] + self.signature = [UInt8](UnsafeBufferPointer(start: signatureBytesPtr, count: signatureLength)) + self.certs = _certs } } } diff --git a/Sources/NemID/RSA/RSAKey.swift b/Sources/NemID/RSA/RSAKey.swift index a6c1172..56c3bdd 100644 --- a/Sources/NemID/RSA/RSAKey.swift +++ b/Sources/NemID/RSA/RSAKey.swift @@ -9,7 +9,7 @@ public final class RSAKey: BIOLoadable { public static func `private`(pem data: Data) throws -> RSAKey where Data: DataProtocol { - let privateKey = try self.load(pem: data, { bio in + let privateKey = try self.load(pem: data, { bio -> UnsafeMutablePointer in CNemIDBoringSSL_PEM_read_bio_RSAPrivateKey(bio, nil, nil, nil) }) diff --git a/Sources/NemID/RSA/RSASigner.swift b/Sources/NemID/RSA/RSASigner.swift index 1f9e82d..bc2919f 100644 --- a/Sources/NemID/RSA/RSASigner.swift +++ b/Sources/NemID/RSA/RSASigner.swift @@ -9,10 +9,24 @@ enum RSASignerError: Error { } public struct RSASigner { + public enum Algorithm { + case sha1 + case sha256 + + var _boringPointer: OpaquePointer { + switch self { + case .sha1: return CNemIDBoringSSL_EVP_sha1() + case .sha256: return CNemIDBoringSSL_EVP_sha256() + } + } + } + let key: RSAKey + let algorithm: Algorithm - public init(key: RSAKey) { + public init(key: RSAKey, algorithm: Algorithm = .sha256) { self.key = key + self.algorithm = algorithm } func sign(_ plaintext: [UInt8]) throws -> [UInt8] { @@ -21,7 +35,7 @@ public struct RSASigner { let digest = try self.digest(plaintext) guard CNemIDBoringSSL_RSA_sign( - CNemIDBoringSSL_EVP_MD_type(CNemIDBoringSSL_EVP_sha256()), + CNemIDBoringSSL_EVP_MD_type(algorithm._boringPointer), digest, numericCast(digest.count), &signature, @@ -37,7 +51,7 @@ public struct RSASigner { func verify(_ signature: [UInt8], signs plaintext: [UInt8]) throws -> Bool { let digest = try self.digest(plaintext) return CNemIDBoringSSL_RSA_verify( - CNemIDBoringSSL_EVP_MD_type(CNemIDBoringSSL_EVP_sha256()), + CNemIDBoringSSL_EVP_MD_type(algorithm._boringPointer), digest, numericCast(digest.count), signature, @@ -50,7 +64,7 @@ public struct RSASigner { let context = CNemIDBoringSSL_EVP_MD_CTX_new() defer { CNemIDBoringSSL_EVP_MD_CTX_free(context) } - guard CNemIDBoringSSL_EVP_DigestInit_ex(context, CNemIDBoringSSL_EVP_sha256(), nil) == 1 else { + guard CNemIDBoringSSL_EVP_DigestInit_ex(context, algorithm._boringPointer, nil) == 1 else { throw RSASignerError.failedToInitializeDigest } diff --git a/Sources/NemID/Response Handler/NemIDResponseHandler.swift b/Sources/NemID/Response Handler/NemIDResponseHandler.swift index d08e44a..e2f69b4 100644 --- a/Sources/NemID/Response Handler/NemIDResponseHandler.swift +++ b/Sources/NemID/Response Handler/NemIDResponseHandler.swift @@ -21,6 +21,7 @@ struct NemIDResponseHandler { } /// Verifies a response from a NemID client flow such as logging in and extratcs the user as `NemIDUser` + /// Will also check for if the response is a NemID error and return `NemIDError` /// /// Does the checks in respect to the NemID documentation p. 34: /// - Extract the certficiates from XMLDSig @@ -30,11 +31,24 @@ struct NemIDResponseHandler { /// - Check that the certficate has not been revoked /// /// - Parameters: - /// - response: The XML as XML data received from the client. + /// - response: Base64 encoded response data received from the NemID client. /// - Returns: A `EventLoopFuture` containg the verified certificate user as `NemIDUser`. - func verifyAndExtractUser(fromXML xmlData: [UInt8]) -> EventLoopFuture { + func verifyAndExtractUser(from response: Data) -> EventLoopFuture { do { - let parsedResponse = try xmlParser.parse(xmlData) + guard let base64DecodedData = Data(base64Encoded: response) else { + throw NemIDResponseHandlerError.failedToDecodeResponseAsBase64 + } + + // Check if the response is an error. + if let responseString = String(data: base64DecodedData, encoding: .utf8), + let clientError = NemIDError(rawValue: responseString) + { + throw clientError + } + + // Else parse the response as a successful XML message. + let parsedResponse = try xmlParser.parse([UInt8](base64DecodedData)) + // Extract certificate chain. let certificates = try certificateExtractor.extract(from: parsedResponse) @@ -70,10 +84,11 @@ struct NemIDResponseHandler { guard let ocspCertificate = basicResponse.certs.first else { throw NemIDResponseHandlerError.ocspCertificateNotFoundInResponse } - let signer = RSASigner(key: try ocspCertificate.publicKey()) - guard try signer.verify(basicResponse.signature, signs: basicResponse.tbsResponseData.derBytes) else { - throw NemIDResponseHandlerError.ocspSignatureWasNotSignedByCertificate - } + #warning("this fails") + let signer = RSASigner(key: try ocspCertificate.publicKey(), algorithm: basicResponse.signatureAlgorithm == .sha1 ? .sha1 : .sha256) +// guard try signer.verify(basicResponse.signature, signs: basicResponse.tbsResponseData.derBytes) else { +// throw NemIDResponseHandlerError.ocspSignatureWasNotSignedByCertificate +// } // Validate that accompanying certificate was signed by issuer. guard try ocspCertificate.isSignedBy(by: chain.intermediate) else { @@ -94,13 +109,14 @@ struct NemIDResponseHandler { } // Check hash name, key hash and serial number are the ones we sent in the request. - try chain.leaf.withSerialNumber { serialNumber in - var ptr: UnsafeMutablePointer? - ptr = nil - let leafSerialNumberSize = CNemIDBoringSSL_BN_bn2bin(serialNumber, ptr) - let leafSerialNumberBytes = [UInt8](UnsafeMutableBufferPointer(start: ptr, count: leafSerialNumberSize)) - guard certResponse.certID.serialNumber == leafSerialNumberBytes else { fatalError() } - } + #warning("crashes") +// try chain.leaf.withSerialNumber { serialNumber in +// var ptr: UnsafeMutablePointer? +// ptr = nil +// let leafSerialNumberSize = CNemIDBoringSSL_BN_bn2bin(serialNumber, ptr) +// let leafSerialNumberBytes = [UInt8](UnsafeMutableBufferPointer(start: ptr, count: leafSerialNumberSize)) +// guard certResponse.certID.serialNumber == leafSerialNumberBytes else { fatalError() } +// } guard chain.intermediate.hashedPublicKey == certResponse.certID.issuerKeyHash else { fatalError() } guard chain.intermediate.hashedSubject == certResponse.certID.issuerNameHash else { fatalError() } @@ -115,7 +131,7 @@ struct NemIDResponseHandler { } // Check OCSP extension - guard !ocspCertificate.hasOCSPNoCheckExtension() else { + guard ocspCertificate.hasOCSPNoCheckExtension() else { throw NemIDResponseHandlerError.ocspCertificateHasNoCheckExtension } } @@ -133,7 +149,7 @@ struct NemIDResponseHandler { else { throw NemIDResponseHandlerError.failedToExtractCertificateDates } - guard notAfter < Date() && notBefore > Date() else { + guard Date() < notAfter && Date() > notBefore else { throw NemIDResponseHandlerError.certificateIsOutsideValidTime } } @@ -171,7 +187,7 @@ struct NemIDResponseHandler { guard let objectToBeSignedC14N = response.objectToBeSigned.C14N() else { throw NemIDResponseHandlerError.failedToExtractObjectToBeSigned } - guard let signatureValueBase64Decoded = Data(base64Encoded: response.signatureValue) else { + guard let signatureValueBase64Decoded = Data(base64Encoded: response.signatureValue, options: .ignoreUnknownCharacters) else { throw NemIDResponseHandlerError.failedToExtractSignatureValue } diff --git a/Sources/NemID/Response Handler/NemIDResponseHandlerError.swift b/Sources/NemID/Response Handler/NemIDResponseHandlerError.swift index 21b7fbd..2c0cd36 100644 --- a/Sources/NemID/Response Handler/NemIDResponseHandlerError.swift +++ b/Sources/NemID/Response Handler/NemIDResponseHandlerError.swift @@ -1,6 +1,7 @@ import Foundation enum NemIDResponseHandlerError: Error { + case failedToDecodeResponseAsBase64 case failedToExtractSignedInfo case failedToExtractReferenceDigest case failedToExtractObjectToBeSigned diff --git a/Sources/NemID/Utilities/Bool+NemID.swift b/Sources/NemID/Utilities/Bool+NemID.swift new file mode 100644 index 0000000..ac43f79 --- /dev/null +++ b/Sources/NemID/Utilities/Bool+NemID.swift @@ -0,0 +1,10 @@ +import Foundation + +extension Bool { + var nemIDRepresentation: String { + switch self { + case true: return "TRUE" + case false: return "FALSE" + } + } +} diff --git a/Sources/NemID/X509/X509Certificate.swift b/Sources/NemID/X509/X509Certificate.swift index dfb2a2d..ee6af6d 100644 --- a/Sources/NemID/X509/X509Certificate.swift +++ b/Sources/NemID/X509/X509Certificate.swift @@ -5,22 +5,36 @@ import Crypto enum X509CertificateError: Error { case failedToRetrievePublicKey case failedToGetSerialNumber + case failedToRetrieveDERRepresentation } -final class X509Certificate: BIOLoadable { - /// Initialize a new certificate from a PEM string - convenience init(der string: String) throws { +public final class X509Certificate: BIOLoadable { + /// Initialize a new certificate from a DER string + public convenience init(der string: String) throws { try self.init(der: [UInt8](string.utf8)) } /// Initialize a new certificate from DER-encoded data. - convenience init(der data: Data) throws where Data: DataProtocol { + public convenience init(der data: Data) throws where Data: DataProtocol { let x509 = try Self.load(pem: data) { bioPtr in return CNemIDBoringSSL_d2i_X509_bio(bioPtr, nil) } self.init(x509) } + /// Initialize a new certificate from a PEM string + public convenience init(pem string: String) throws { + try self.init(pem: [UInt8](string.utf8)) + } + + /// Initialize a new certificate from DER-encoded data. + public convenience init(pem data: Data) throws where Data: DataProtocol { + let x509 = try Self.load(pem: data) { bioPtr in + return CNemIDBoringSSL_PEM_read_bio_X509(bioPtr, nil, nil, nil) + } + self.init(x509) + } + /// Extracts the public key as `RSAKey` func publicKey() throws -> RSAKey { try withPublicKey { key in @@ -173,6 +187,34 @@ final class X509Certificate: BIOLoadable { _ref.assumingMemoryBound(to: X509.self) } + func toDERBytes() throws -> [UInt8] { + return try self.withUnsafeDERCertificateBuffer { Array($0) } + } + + func toBase64EncodedDER() throws -> String { + try Data(self.toDERBytes()).base64EncodedString() + } + + private func withUnsafeDERCertificateBuffer(_ body: (UnsafeRawBufferPointer) throws -> T) throws -> T { + guard let bio = CNemIDBoringSSL_BIO_new(CNemIDBoringSSL_BIO_s_mem()) else { + fatalError("Failed to malloc for a BIO handler") + } + defer { CNemIDBoringSSL_BIO_free(bio) } + + guard CNemIDBoringSSL_i2d_X509_bio(bio, self.ref) == 1 else { + throw X509CertificateError.failedToRetrieveDERRepresentation + } + + var dataPtr: UnsafeMutablePointer? = nil + let length = CNemIDBoringSSL_BIO_get_mem_data(bio, &dataPtr) + + guard let bytes = dataPtr.map({ UnsafeRawBufferPointer(start: $0, count: length) }) else { + fatalError("Failed to map bytes from a certificate") + } + + return try body(bytes) + } + private let _ref: UnsafeMutableRawPointer private init(_ ref: UnsafeMutablePointer) { @@ -186,7 +228,7 @@ final class X509Certificate: BIOLoadable { // MARK: Equatable extension X509Certificate: Equatable { - static func ==(_ lhs: X509Certificate, _ rhs: X509Certificate) -> Bool { + public static func ==(_ lhs: X509Certificate, _ rhs: X509Certificate) -> Bool { CNemIDBoringSSL_X509_cmp(lhs.ref, rhs.ref) == 0 } } diff --git a/Sources/NemID/XMLDSig/XMLDSigParser/libxml2DSigParser.swift b/Sources/NemID/XMLDSig/XMLDSigParser/libxml2DSigParser.swift index 5e074a7..685a820 100644 --- a/Sources/NemID/XMLDSig/XMLDSigParser/libxml2DSigParser.swift +++ b/Sources/NemID/XMLDSig/XMLDSigParser/libxml2DSigParser.swift @@ -27,10 +27,10 @@ struct libxml2XMLDSigParser: XMLDSigParser { } defer { xmlXPathFreeContext(context) } - guard xmlXPathRegisterNs(context, "openoces", "http://www.openoces.org/2006/07/signature") == 0 else { + guard xmlXPathRegisterNs(context, "openoces", "http://www.openoces.org/2006/07/signature#") == 0 else { throw ParserError.failedToRegisterNamespace } - guard xmlXPathRegisterNs(context, "ds", "http://www.w3.org/2000/09/xmldsig") == 0 else { + guard xmlXPathRegisterNs(context, "ds", "http://www.w3.org/2000/09/xmldsig#") == 0 else { throw ParserError.failedToRegisterNamespace } @@ -103,7 +103,7 @@ struct libxml2XMLDSigParser: XMLDSigParser { defer { xmlBufferFree(xmlBuffer) } guard let xmlOutputBuffer = xmlOutputBufferCreateBuffer(xmlBuffer, nil) else { return nil } defer { xmlOutputBufferClose(xmlOutputBuffer) } - xmlNodeDumpOutput(xmlOutputBuffer, xmlDoc, nodePtr, 0, 1, nil) + xmlNodeDumpOutput(xmlOutputBuffer, xmlDoc, nodePtr, 0, 0, nil) guard let dataPtr = xmlOutputBufferGetContent(xmlOutputBuffer) else { return nil } let length = xmlOutputBufferGetSize(xmlOutputBuffer) guard length > 0 else { return nil } diff --git a/Tests/NemIDTests/ParameterSignerTests.swift b/Tests/NemIDTests/ParameterSignerTests.swift index e0c35e8..6e30b17 100644 --- a/Tests/NemIDTests/ParameterSignerTests.swift +++ b/Tests/NemIDTests/ParameterSignerTests.swift @@ -11,12 +11,14 @@ final class ParameterSignerTests: XCTestCase { origin: URL(string: "https://nemid.dk")!, rememberUserID: nil, rememberUserIDInitialStatus: nil, - timestamp: date) + timestamp: date, + enableAwaitingAppApprovalEvent: true + ) - let rsaKey = try RSAKey.private(pem: rsaPrivateKey) - let rsaSigner = RSASigner(key: rsaKey) - let configuration = NemIDConfiguration(spCertificate: "cert", serviceProviderID: "", environment: .preproduction) - let signer = NemIDParametersSigner(rsaSigner: rsaSigner, configuration: configuration) + let key = try RSAKey.private(pem: rsaPrivateKey) + let certificate = try X509Certificate(der: Data(base64Encoded: TestCertificates.googleLeaf, options: .ignoreUnknownCharacters)!) + let configuration = NemIDConfiguration(spCertificate: certificate, privateKey: key, serviceProviderID: "", environment: .preproduction) + let signer = NemIDParametersSigner(configuration: configuration) let signedParameters = try signer.sign(parameters) @@ -25,10 +27,11 @@ final class ParameterSignerTests: XCTestCase { XCTAssertEqual(signedParameters.origin?.absoluteString, "https://nemid.dk") XCTAssertEqual(signedParameters.rememberUserID, nil) XCTAssertEqual(signedParameters.rememberUserIDInitialStatus, nil) - XCTAssertEqual(signedParameters.SPCert, "cert") + try XCTAssertEqual(signedParameters.spCert, certificate.toBase64EncodedDER()) XCTAssertEqual(signedParameters.timestamp, date) - XCTAssertEqual(signedParameters.paramsDigest, "5xhSpQNbt1pxNVASMrdg1irRp7uWR/JkZ5wT4c4IHd0=") - XCTAssertEqual(signedParameters.digestSignature, "hUkuhANqk6oqa5+nHBZBc4/UpV0rj7iYn1d2UyBq4XxfyW6O2Qy+LcGN+ZGNVxDJklHbJjg8VbNyjaQ8kYzximmOAvUmCEL9WCw9eT50Uv+6H+uxSQYpe4NijBA2XKhkFAmYH6w2Mdnk9fdku9hq4geVSqCjqIU+8iK++b94LGw=") + XCTAssertEqual(signedParameters.paramsDigest, "DRTNpPkJnkYFEBBQGLqX20eg4/cOYEYZXtd55or/7VI=") + XCTAssertEqual(signedParameters.digestSignature, "OXtgZLfEOauywJsjsm26v6W0DF/F3VXSnibgB1oSfk58K79HwldpQ/ryUHGiJKb/OmCovKc1P2Vrz6eCy1oMee0D7i6WLGvuARDfSfVO6TOhX2KqN7w3fUEoIMu1izETBArx//FN32AlqLOh1fcP0sF0ShzzoSYYGmEeTrlhMj4=") + XCTAssertEqual(signedParameters.enableAwaitingAppApprovalEvent, true) } } diff --git a/Tests/NemIDTests/libxml2XMLDigParserTests.swift b/Tests/NemIDTests/libxml2XMLDigParserTests.swift index 296ea1e..aa1be40 100644 --- a/Tests/NemIDTests/libxml2XMLDigParserTests.swift +++ b/Tests/NemIDTests/libxml2XMLDigParserTests.swift @@ -83,7 +83,11 @@ fileprivate let exampleXMLResponse = """ cert1 + + cert2 + + cert3 @@ -119,3 +123,124 @@ fileprivate let exampleXMLResponse = """ """ + +#warning("actual response from nemid") +// +// +// +// +// +// +// +//uduKRPLbUp4KcoWA39ciGVALf4+sjXsrYH6Zl477pnk= +// +// +// +//UjZ2dH4GqV0shGyJXYRBptC+akAlL20DWnMIZ2NaQUOQBgIjopIbdgGb/ZOJkupPh0qz9aolu+kZ +//hHTYigYCn/7P3isyQ8Xl5/5ALnfpeY4iv8aNlAgLsHCVBap0NMJykEubUUpnlu3x1qBywOHVVqV8 +//MnLHIKe/eIoEm8+EZVyvjDCyzHpp4G7O6rL7aoEgEjS6G3iXAf79X6m04m6/zuykjmCnb1kuFzwL +//Glw5XpVcRmlhOnn+y1LJLeBVwmYxCWr7cgk6MG5JHMMZ2Ob33JcJWg0yd4j19WzHUX7d0WsTV3pj +//RTJCY75I2Pu7Gix4CgOJnaJ9whERKjnoHE3n5Q== +// +// +// +// +//MIIFQjCCAyqgAwIBAgIEW6o/tzANBgkqhkiG9w0BAQsFADBPMQswCQYDVQQGEwJESzESMBAGA1UE +//ChMJVFJVU1QyNDA4MSwwKgYDVQQDEyNUUlVTVDI0MDggU3lzdGVtdGVzdCBWSUkgUHJpbWFyeSBD +//QTAeFw0xOTA2MDYxMjQzMTdaFw0zNDA2MDYxMzEzMTdaMEkxCzAJBgNVBAYTAkRLMRIwEAYDVQQK +//DAlUUlVTVDI0MDgxJjAkBgNVBAMMHVRSVVNUMjQwOCBTeXN0ZW10ZXN0IFhYWElWIENBMIIBIjAN +//BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzMjlEpubMaSpLYQiTnL8WGWQDljBvFyL+mMYIdXd +//CwGbLeyLyt7KAyXC8mtExR+ax96Z9IDIjIIpw/4upjTtWHUV6Ia7sJImw7M81KeQ/ZVSCT+pXybc +//luR5HtVqBRa/kyvs49KbbBx5KQEh2t7UzXwY/yQyEvtzJ7pJdXbP/Zoievo6gJnZh/VMhquwbGQS +//UDLC6CtwYuyHtMPHF1Xgyglva76D8CD2ajzYNXZ7AqjwMc5S1bONLyDwFITvOCAMnJCG7CQat57E +//rN7qWBuDIkQGsFIFuqUtLTOk6IYrezKhoV9FwjAOwrTTSeJoa/IZlIrpyRoBAuTKFRDG34BinwID +//AQABo4IBKjCCASYwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwEQYDVR0gBAowCDAG +//BgRVHSAAMIGvBgNVHR8EgacwgaQwOqA4oDaGNGh0dHA6Ly9jcmwuc3lzdGVtdGVzdDcudHJ1c3Qy +//NDA4LmNvbS9zeXN0ZW10ZXN0Ny5jcmwwZqBkoGKkYDBeMQswCQYDVQQGEwJESzESMBAGA1UEChMJ +//VFJVU1QyNDA4MSwwKgYDVQQDEyNUUlVTVDI0MDggU3lzdGVtdGVzdCBWSUkgUHJpbWFyeSBDQTEN +//MAsGA1UEAxMEQ1JMMTAfBgNVHSMEGDAWgBQjukwxkOHTzNMbsxtatshFzdODlzAdBgNVHQ4EFgQU +//zWxolzlyGaQ1q2Tq9BGjgYf4aTswDQYJKoZIhvcNAQELBQADggIBAA9f3uvdFgbl6nmWstkoUQHD +///0JJyMQKtFhixeNH2NplFsuAd8WzKdsa+BdmO6NxbeTSptgIkjZe+hzPfJ/oikCYmvynJlJlqqnK +//qosxK7zVHd8szcKaWY1GxD9qNrcGj7u6MF3ruqaSDYPkeYHlQX/lmJatiFQ3O8MvG1iEczUheXJl +//tgCys6jOP3SVrj56DJCv0lgpiHKqChJW2nASBOxINjAS3dDn6rSJqCccDavdE+P7Zj4vMiL0IIig +//EdCscQSDcEy8Wxm03PMGJQwOKJ4Lkbsq8WbNcgm6KjXRy0EEW8Rbu/Sbf6MLl43xycfNol9vmo+M +//ctyQGgwIzK8PXSuO3Ni6YXOtXMuKXZ4f9cUxIyYWyabsnnWSWpNzjkP2RPbS0Hj2OE/S4ajthfMa +//G42ZZwUpAJbz0+gLmIZl8zpi7Mk+S51fJbWS6siagfSdfHScjKSkwMeR8SWk1c+D20B14t1m7VsA +//zGUpSh8Xrg7EPPCImKV5aE9CobPa0GvmbF1GC4G0TAkx6B7oegG6BvaP2VDKl7ZTWpPAe7lgovUw +//AlmlcxVnyUVWF71/8WZS5S72wJDpIup/EuyDHlisxc7G0TIySznnVDBy3UAZK+crK+5CzLiVhwPP +//WfIVTLN4fzSOpXT61YaVcqMl4rNG6J+ww9PcmaladZlEYmnuCcDs +// +// +// +// +//MIIGCjCCBPKgAwIBAgIEX5wySzANBgkqhkiG9w0BAQsFADBJMQswCQYDVQQGEwJESzESMBAGA1UE +//CgwJVFJVU1QyNDA4MSYwJAYDVQQDDB1UUlVTVDI0MDggU3lzdGVtdGVzdCBYWFhJViBDQTAeFw0y +//MTA0MTYxNjM4MDJaFw0yNDA0MTYxNjM2MDJaMHYxCzAJBgNVBAYTAkRLMSkwJwYDVQQKDCBJbmdl +//biBvcmdhbmlzYXRvcmlzayB0aWxrbnl0bmluZzE8MBUGA1UEAwwOVGhlcmVzaWEgS2rDpnIwIwYD +//VQQFExxQSUQ6OTIwOC0yMDAyLTItODcxMjk2MTUzNjEzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +//MIIBCgKCAQEAmTvdGCGcgSL+JJn8s6O8u/08RRL4AeMBRPxGAdnSEL4YHA7Vm6birOC6szKblkk6 +//Nk02cdsBg6OJ27PcFGNLbliUGTXRP9XsUzxxrZU1sFoRs9KwMnYt75tfRGoEqNogxlwp22idDwVE +//S4wzaHN0u4WpZYJ8QbpcYRRTNKPa8cx1mFdlOdb4/6DDbHmUpB2i6QczrWRvZNN+oq9ow++7NQ14 +//gNAn3OmOm9v3XVWbgJ+DCcfBp2nrSIvIDpzrRI8lIUs258bMRlOuHkkjka048Q5ATer1GJTII94V +//EPEHmbALu2+diectoBOKVeDmI0tF9ieEKy0mi6AU7PuUYEuzkQIDAQABo4ICyzCCAscwDgYDVR0P +//AQH/BAQDAgP4MIGVBggrBgEFBQcBAQSBiDCBhTA8BggrBgEFBQcwAYYwaHR0cDovL29jc3Auc3lz +//dGVtdGVzdDM0LnRydXN0MjQwOC5jb20vcmVzcG9uZGVyMEUGCCsGAQUFBzAChjlodHRwOi8vYWlh +//LnN5c3RlbXRlc3QzNC50cnVzdDI0MDguY29tL3N5c3RlbXRlc3QzNC1jYS5jZXIwggEgBgNVHSAE +//ggEXMIIBEzCCAQ8GDSsGAQQBgfRRAgQGAQUwgf0wLwYIKwYBBQUHAgEWI2h0dHA6Ly93d3cudHJ1 +//c3QyNDA4LmNvbS9yZXBvc2l0b3J5MIHJBggrBgEFBQcCAjCBvDAMFgVEYW5JRDADAgEBGoGrRGFu +//SUQgdGVzdCBjZXJ0aWZpa2F0ZXIgZnJhIGRlbm5lIENBIHVkc3RlZGVzIHVuZGVyIE9JRCAxLjMu +//Ni4xLjQuMS4zMTMxMy4yLjQuNi4xLjUuIERhbklEIHRlc3QgY2VydGlmaWNhdGVzIGZyb20gdGhp +//cyBDQSBhcmUgaXNzdWVkIHVuZGVyIE9JRCAxLjMuNi4xLjQuMS4zMTMxMy4yLjQuNi4xLjUuMIGt +//BgNVHR8EgaUwgaIwPKA6oDiGNmh0dHA6Ly9jcmwuc3lzdGVtdGVzdDM0LnRydXN0MjQwOC5jb20v +//c3lzdGVtdGVzdDM0LmNybDBioGCgXqRcMFoxCzAJBgNVBAYTAkRLMRIwEAYDVQQKDAlUUlVTVDI0 +//MDgxJjAkBgNVBAMMHVRSVVNUMjQwOCBTeXN0ZW10ZXN0IFhYWElWIENBMQ8wDQYDVQQDDAZDUkwx +//NDgwHwYDVR0jBBgwFoAUzWxolzlyGaQ1q2Tq9BGjgYf4aTswHQYDVR0OBBYEFH+z0jzI8XzRg0FR +//acyCkH7/71ZpMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEBAHU5GXPYPhYcJOtHG92KpzZ+ +//LsYx5mL7+k1+0nL218GKSM5EoGb0QNexpigP2ze+bf3DGsklcKP9Wu3EF8s5RzRYUgg7V97KT5cs +///aqHtltwZn6OqEkyChaCbmRPxatF2VXFQRH2UI/xZQ9o1snbjkwF/zKSaVkH4jo2PuTqobErJtnm +//tFZsHmR/6azmq60zvfGpxzBWfDP8AalcFff0rUv5QW9ZqEsWkdLRsWb+W0yYrlettEV+kymlg/t8 +//JVXzj8/GrSexK5Q5sxE7icw1hu/fpcruWtMFCYXDj3b9RojmwoqmkRKnA0wka0jCV6PN3mZe+1po +//lSwZrrZpQXeTF1I= +// +// +// +// +//MIIGSDCCBDCgAwIBAgIES+pulDANBgkqhkiG9w0BAQsFADBPMQswCQYDVQQGEwJESzESMBAGA1UE +//ChMJVFJVU1QyNDA4MSwwKgYDVQQDEyNUUlVTVDI0MDggU3lzdGVtdGVzdCBWSUkgUHJpbWFyeSBD +//QTAeFw0xMDA1MTIwODMyMTRaFw0zNzAxMTIwOTAyMTRaME8xCzAJBgNVBAYTAkRLMRIwEAYDVQQK +//EwlUUlVTVDI0MDgxLDAqBgNVBAMTI1RSVVNUMjQwOCBTeXN0ZW10ZXN0IFZJSSBQcmltYXJ5IENB +//MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEApuuMpdHu/lXhQ+9TyecthOxrg5hPgxlK +//1rpjsyBNDEmOEpmOlK8ghyZ7MnSF3ffsiY+0jA51p+AQfYYuarGgUQVO+VM6E3VUdDpgWEksetCY +//Y8L7UrpyDeYx9oywT7E+YXH0vCoug5F9vBPnky7PlfVNaXPfgjh1+66mlUD9sV3fiTjDL12GkwOL +//t35S5BkcqAEYc37HT69N88QugxtaRl8eFBRumj1Mw0LBxCwl21GdVY4EjqH1Us7YtRMRJ2nEFTCR +//WHzm2ryf7BGd80YmtJeL6RoiidwlIgzvhoFhv4XdLHwzaQbdb9s141q2s9KDPZCGcgIgeXZdqY1V +//z7UBCMiBDG7q2S2ni7wpUMBye+iYVkvJD32srGCzpWqG7203cLyZCjq2oWuLkL807/Sk4sYleMA4 +//YFqsazIfV+M0OVrJCCCkPysS10n/+ioleM0hnoxQiupujIGPcJMA8anqWueGIaKNZFA/m1IKwnn0 +//CTkEm2aGTTEwpzb0+dCATlLyv6Ss3w+D7pqWCXsAVAZmD4pncX+/ASRZQd3oSvNQxUQr8EoxEULx +//Sae0CPRyGwQwswGpqmGm8kNPHjIC5ks2mzHZAMyTz3zoU3h/QW2T2U2+pZjUeMjYhyrReWRbOIBC +//izoOaoaNcSnPGUEohGUyLPTbZLpWsm3vjbyk7yvPqoUCAwEAAaOCASowggEmMA8GA1UdEwEB/wQF +//MAMBAf8wDgYDVR0PAQH/BAQDAgEGMBEGA1UdIAQKMAgwBgYEVR0gADCBrwYDVR0fBIGnMIGkMDqg +//OKA2hjRodHRwOi8vY3JsLnN5c3RlbXRlc3Q3LnRydXN0MjQwOC5jb20vc3lzdGVtdGVzdDcuY3Js +//MGagZKBipGAwXjELMAkGA1UEBhMCREsxEjAQBgNVBAoTCVRSVVNUMjQwODEsMCoGA1UEAxMjVFJV +//U1QyNDA4IFN5c3RlbXRlc3QgVklJIFByaW1hcnkgQ0ExDTALBgNVBAMTBENSTDEwHwYDVR0jBBgw +//FoAUI7pMMZDh08zTG7MbWrbIRc3Tg5cwHQYDVR0OBBYEFCO6TDGQ4dPM0xuzG1q2yEXN04OXMA0G +//CSqGSIb3DQEBCwUAA4ICAQCRJ9TM7sISJBHQwN8xdey4rxA0qT7NZdKICcIxyIC82HIOGAouKb3o +//HjIoMgxIUhA3xbU3Putr4+Smnc1Ldrw8AofLGlFYG2ypg3cpF9pdHrVdh8QiERozLwfNPDgVeCAn +//jKPNt8mu0FWBS32tiVM5DEOUwDpoDDRF27Ku9qTFH4IYg90wLHfLi+nqc2HwVBUgDt3tXU6zK4pz +//M0CpbrbOXPJOYHMvaw/4Em2r0PZD+QOagcecxPMWI65t2h/USbyO/ah3VKnBWDkPsMKjj5jEbBVR +//nGZdv5rcJb0cHqQ802eztziA4HTbSzBE4oRaVCrhXg/g6Jj8/tZlgxRI0JGgAX2dvWQyP4xhbxLN +//CVXPdvRV0g0ehKvhom1FGjIz975/DMavkybh0gzygq4sY9Fykl4oT4rDkDvZLYIxS4u1BrUJJJaD +//zHCeXmZqOhx8She+Fj9YwVVRGfxT4FL0Qd3WAtaCVyhSQ6SkZgrPvzAmxOUruI6XhEhYGlP5O8WF +//ETiATxuZAJNuKMJtibfRhMNsQ+TVv/ZPr5Swe+3DIQtmt1MIlGlTn4k40z4s6gDGKiFwAYXjd/kI +//D32R/hJPE41o9+3nd8aHZhBy2lF0jKAmr5a6Lbhg2O7zjGq7mQ3MceNeebuWXD44AxIinryzhqnE +//WI+BxdlFaia3U7o2+HYdHw== +// +// +// +// +//RequestIssuerTG9vZmVycw== +//TimeStamp1618591111004 +//actionlogon +//identityAssuranceLevel1 +// +// From 5d9f5e04ff71dee9efa3bd87c98db4ac3b86316c Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Mon, 19 Apr 2021 19:32:45 +0200 Subject: [PATCH 14/22] Fix parsing of OCSP signaturee --- Sources/NemID/NemIDConfiguration.swift | 8 +- Sources/NemID/NemIDUser.swift | 9 +- Sources/NemID/OCSP/OCSPRequest.swift | 2 +- Sources/NemID/OCSP/OCSPResponse.swift | 43 +---- .../RSA/{RSAKey.swift => NemIDRSAKey.swift} | 6 +- Sources/NemID/RSA/RSASigner.swift | 28 +-- .../NemIDResponseHandler.swift | 12 +- Sources/NemID/Utilities/HashAlgorithm.swift | 22 +++ .../NemID/Utilities/SignatureAlgorithm.swift | 22 +++ Sources/NemID/X509/CertificateChain.swift | 12 +- ...icate.swift => NemIDX509Certificate.swift} | 18 +- .../DefaultCertificateExtractor.swift | 4 +- .../CertificateExtractorTests.swift | 12 +- Tests/NemIDTests/Helpers/Bytes+Hex.swift | 47 +++++ .../Helpers/Date+FromComponents.swift | 16 ++ ...estCertificates.swift => TestHelper.swift} | 20 ++- Tests/NemIDTests/OCSPRequestTests.swift | 4 +- Tests/NemIDTests/OCSPResponseTests.swift | 163 +++++++++++++++++- Tests/NemIDTests/ParameterSignerTests.swift | 22 +-- Tests/NemIDTests/RSASignerTests.swift | 14 ++ Tests/NemIDTests/X509CertificateTests.swift | 22 +-- 21 files changed, 369 insertions(+), 137 deletions(-) rename Sources/NemID/RSA/{RSAKey.swift => NemIDRSAKey.swift} (84%) create mode 100644 Sources/NemID/Utilities/HashAlgorithm.swift create mode 100644 Sources/NemID/Utilities/SignatureAlgorithm.swift rename Sources/NemID/X509/{X509Certificate.swift => NemIDX509Certificate.swift} (95%) create mode 100644 Tests/NemIDTests/Helpers/Bytes+Hex.swift create mode 100644 Tests/NemIDTests/Helpers/Date+FromComponents.swift rename Tests/NemIDTests/Helpers/{TestCertificates.swift => TestHelper.swift} (87%) create mode 100644 Tests/NemIDTests/RSASignerTests.swift diff --git a/Sources/NemID/NemIDConfiguration.swift b/Sources/NemID/NemIDConfiguration.swift index b3386ea..062a93d 100644 --- a/Sources/NemID/NemIDConfiguration.swift +++ b/Sources/NemID/NemIDConfiguration.swift @@ -2,17 +2,17 @@ import Foundation public struct NemIDConfiguration { /// The certificate of the OCES service-provider. - public let spCertificate: X509Certificate + public let spCertificate: NemIDX509Certificate /// The private key for the OCES service provider. - public let privateKey: RSAKey + public let privateKey: NemIDRSAKey /// The service provider ID supplied by NemID (also known as SPID) public let serviceProviderID: String /// The NemID environment to use public let environment: NemIDEnvironment public init( - spCertificate: X509Certificate, - privateKey: RSAKey, + spCertificate: NemIDX509Certificate, + privateKey: NemIDRSAKey, serviceProviderID: String, environment: NemIDEnvironment ) { diff --git a/Sources/NemID/NemIDUser.swift b/Sources/NemID/NemIDUser.swift index 71fda0b..dc6a670 100644 --- a/Sources/NemID/NemIDUser.swift +++ b/Sources/NemID/NemIDUser.swift @@ -9,12 +9,13 @@ public struct NemIDUser { /// The PID representing this user. This value can be used to verify a given CPR matches with this user. public let pid: String + /// The name of the user. For example "Bob Hansen" /// - Important: /// If the user has chosen not to share their name, this value is `nil`. public let name: String? - init(from certificate: X509Certificate) throws { + init(from certificate: NemIDX509Certificate) throws { guard let commonName = certificate.subjectCommonName else { throw InitializationError.failedToExtractCommonNameFromCertificate } @@ -30,3 +31,9 @@ public struct NemIDUser { self.name = name } } + +extension NemIDUser: CustomStringConvertible { + public var description: String { + "NemIDUser(name: \(String(describing: self.name)), PID: \(self.pid))" + } +} diff --git a/Sources/NemID/OCSP/OCSPRequest.swift b/Sources/NemID/OCSP/OCSPRequest.swift index a367307..241a2dc 100644 --- a/Sources/NemID/OCSP/OCSPRequest.swift +++ b/Sources/NemID/OCSP/OCSPRequest.swift @@ -21,7 +21,7 @@ struct OCSPRequest { /// - certificate: The `X509Certificate` the requset is for /// - issuer: The `X509Certificate` issuer of `certificate` /// - Returns: The OCSP request bytes DER-encoded. - init(certificate: X509Certificate, issuer: X509Certificate) throws { + init(certificate: NemIDX509Certificate, issuer: NemIDX509Certificate) throws { // Hash issuer name and public key guard let issuerSubjectSHA256 = issuer.hashedSubject else { throw OCSPRequestError.failedToGetIssuerSubject diff --git a/Sources/NemID/OCSP/OCSPResponse.swift b/Sources/NemID/OCSP/OCSPResponse.swift index eb3b624..6197e2b 100644 --- a/Sources/NemID/OCSP/OCSPResponse.swift +++ b/Sources/NemID/OCSP/OCSPResponse.swift @@ -119,7 +119,7 @@ extension OCSPResponse { let signatureAlgorithm: SignatureAlgorithm /// The signature as DER encoded bytes. let signature: [UInt8] - let certs: [X509Certificate] + let certs: [NemIDX509Certificate] init(cbs: UnsafeMutablePointer) throws { // Parse tbsResponseData (ResponseData) @@ -156,7 +156,7 @@ extension OCSPResponse { defer { CNemIDBoringSSL_OPENSSL_free(signatureBytesPtr) } // Parse certs ([0] EXPLICIT SEQUENCE OF Certificate OPTIONAL) - var _certs = [X509Certificate]() + var _certs = [NemIDX509Certificate]() var certsCBS = CBS() var isCertsPresent: Int32 = 0 guard CNemIDBoringSSL_CBS_get_optional_asn1( @@ -185,33 +185,20 @@ extension OCSPResponse { throw OCSPResponseError.failedToParseResponse } defer { CNemIDBoringSSL_OPENSSL_free(certOutPtr) } - try _certs.append(X509Certificate(der: [UInt8](UnsafeBufferPointer(start: certOutPtr, count: certOutLength)))) + try _certs.append(NemIDX509Certificate(der: [UInt8](UnsafeBufferPointer(start: certOutPtr, count: certOutLength)))) } self.tbsResponseData = try ResponseData(cbs: &tbsResponseDataCBS) self.signatureAlgorithm = algorithm - self.signature = [UInt8](UnsafeBufferPointer(start: signatureBytesPtr, count: signatureLength)) + var signature = [UInt8](UnsafeBufferPointer(start: signatureBytesPtr, count: signatureLength)) + // Skip first byte (unused number of bits in a BIT_STRING) + signature.removeFirst() + self.signature = signature self.certs = _certs } } } -// MARK: SignatureAlgorithm -extension OCSPResponse.BasicOCSPResponse { - enum SignatureAlgorithm { - case sha256 - case sha1 - - init?(nid: Int32) { - switch nid { - case NID_sha256WithRSAEncryption: self = .sha256 - case NID_sha1WithRSAEncryption: self = .sha1 - default: return nil - } - } - } -} - // MARK: ResponseData extension OCSPResponse.BasicOCSPResponse { struct ResponseData { @@ -412,22 +399,6 @@ extension OCSPResponse.BasicOCSPResponse.ResponseData.SingleResponse { } } -// MARK: HashAlgorithm -extension OCSPResponse.BasicOCSPResponse.ResponseData.SingleResponse.CertID { - enum HashAlgorithm { - case sha256 - case sha1 - - init?(nid: Int32) { - switch nid { - case NID_sha256: self = .sha256 - case NID_sha1: self = .sha1 - default: return nil - } - } - } -} - // MARK: CertStatus extension OCSPResponse.BasicOCSPResponse.ResponseData.SingleResponse { enum CertStatus { diff --git a/Sources/NemID/RSA/RSAKey.swift b/Sources/NemID/RSA/NemIDRSAKey.swift similarity index 84% rename from Sources/NemID/RSA/RSAKey.swift rename to Sources/NemID/RSA/NemIDRSAKey.swift index 56c3bdd..fec292f 100644 --- a/Sources/NemID/RSA/RSAKey.swift +++ b/Sources/NemID/RSA/NemIDRSAKey.swift @@ -1,12 +1,12 @@ import Foundation @_implementationOnly import CNemIDBoringSSL -public final class RSAKey: BIOLoadable { - public static func `private`(pem string: String) throws -> RSAKey { +public final class NemIDRSAKey: BIOLoadable { + public static func `private`(pem string: String) throws -> NemIDRSAKey { try .private(pem: [UInt8](string.utf8)) } - public static func `private`(pem data: Data) throws -> RSAKey + public static func `private`(pem data: Data) throws -> NemIDRSAKey where Data: DataProtocol { let privateKey = try self.load(pem: data, { bio -> UnsafeMutablePointer in diff --git a/Sources/NemID/RSA/RSASigner.swift b/Sources/NemID/RSA/RSASigner.swift index bc2919f..be80d23 100644 --- a/Sources/NemID/RSA/RSASigner.swift +++ b/Sources/NemID/RSA/RSASigner.swift @@ -8,25 +8,13 @@ enum RSASignerError: Error { case failedToGetDigest } -public struct RSASigner { - public enum Algorithm { - case sha1 - case sha256 - - var _boringPointer: OpaquePointer { - switch self { - case .sha1: return CNemIDBoringSSL_EVP_sha1() - case .sha256: return CNemIDBoringSSL_EVP_sha256() - } - } - } - - let key: RSAKey - let algorithm: Algorithm +struct RSASigner { + let key: NemIDRSAKey + let hashAlgorithm: HashAlgorithm - public init(key: RSAKey, algorithm: Algorithm = .sha256) { + init(key: NemIDRSAKey, hashAlgorithm: HashAlgorithm = .sha256) { self.key = key - self.algorithm = algorithm + self.hashAlgorithm = hashAlgorithm } func sign(_ plaintext: [UInt8]) throws -> [UInt8] { @@ -35,7 +23,7 @@ public struct RSASigner { let digest = try self.digest(plaintext) guard CNemIDBoringSSL_RSA_sign( - CNemIDBoringSSL_EVP_MD_type(algorithm._boringPointer), + CNemIDBoringSSL_EVP_MD_type(hashAlgorithm._boringPointer), digest, numericCast(digest.count), &signature, @@ -51,7 +39,7 @@ public struct RSASigner { func verify(_ signature: [UInt8], signs plaintext: [UInt8]) throws -> Bool { let digest = try self.digest(plaintext) return CNemIDBoringSSL_RSA_verify( - CNemIDBoringSSL_EVP_MD_type(algorithm._boringPointer), + CNemIDBoringSSL_EVP_MD_type(hashAlgorithm._boringPointer), digest, numericCast(digest.count), signature, @@ -64,7 +52,7 @@ public struct RSASigner { let context = CNemIDBoringSSL_EVP_MD_CTX_new() defer { CNemIDBoringSSL_EVP_MD_CTX_free(context) } - guard CNemIDBoringSSL_EVP_DigestInit_ex(context, algorithm._boringPointer, nil) == 1 else { + guard CNemIDBoringSSL_EVP_DigestInit_ex(context, hashAlgorithm._boringPointer, nil) == 1 else { throw RSASignerError.failedToInitializeDigest } diff --git a/Sources/NemID/Response Handler/NemIDResponseHandler.swift b/Sources/NemID/Response Handler/NemIDResponseHandler.swift index e2f69b4..468e1ee 100644 --- a/Sources/NemID/Response Handler/NemIDResponseHandler.swift +++ b/Sources/NemID/Response Handler/NemIDResponseHandler.swift @@ -84,11 +84,11 @@ struct NemIDResponseHandler { guard let ocspCertificate = basicResponse.certs.first else { throw NemIDResponseHandlerError.ocspCertificateNotFoundInResponse } - #warning("this fails") - let signer = RSASigner(key: try ocspCertificate.publicKey(), algorithm: basicResponse.signatureAlgorithm == .sha1 ? .sha1 : .sha256) -// guard try signer.verify(basicResponse.signature, signs: basicResponse.tbsResponseData.derBytes) else { -// throw NemIDResponseHandlerError.ocspSignatureWasNotSignedByCertificate -// } + #warning("signature is fixed now, now we need the derBytes fixed.") + let signer = RSASigner(key: try ocspCertificate.publicKey(), hashAlgorithm: basicResponse.signatureAlgorithm.hashAlgorithm) + guard try signer.verify(basicResponse.signature, signs: basicResponse.tbsResponseData.derBytes) else { + throw NemIDResponseHandlerError.ocspSignatureWasNotSignedByCertificate + } // Validate that accompanying certificate was signed by issuer. guard try ocspCertificate.isSignedBy(by: chain.intermediate) else { @@ -177,7 +177,7 @@ struct NemIDResponseHandler { } /// Verifies the signed element in the xml response - private func validateXMLSignature(_ response: ParsedXMLDSigResponse, wasSignedBy certificate: X509Certificate) throws { + private func validateXMLSignature(_ response: ParsedXMLDSigResponse, wasSignedBy certificate: NemIDX509Certificate) throws { guard let signedInfoC14N = response.signedInfo.C14N() else { throw NemIDResponseHandlerError.failedToExtractSignedInfo } diff --git a/Sources/NemID/Utilities/HashAlgorithm.swift b/Sources/NemID/Utilities/HashAlgorithm.swift new file mode 100644 index 0000000..7427be3 --- /dev/null +++ b/Sources/NemID/Utilities/HashAlgorithm.swift @@ -0,0 +1,22 @@ +import Foundation +@_implementationOnly import CNemIDBoringSSL + +enum HashAlgorithm { + case sha1 + case sha256 + + init?(nid: Int32) { + switch nid { + case NID_sha256: self = .sha256 + case NID_sha1: self = .sha1 + default: return nil + } + } + + var _boringPointer: OpaquePointer { + switch self { + case .sha1: return CNemIDBoringSSL_EVP_sha1() + case .sha256: return CNemIDBoringSSL_EVP_sha256() + } + } +} diff --git a/Sources/NemID/Utilities/SignatureAlgorithm.swift b/Sources/NemID/Utilities/SignatureAlgorithm.swift new file mode 100644 index 0000000..663aa70 --- /dev/null +++ b/Sources/NemID/Utilities/SignatureAlgorithm.swift @@ -0,0 +1,22 @@ +import Foundation +@_implementationOnly import CNemIDBoringSSL + +enum SignatureAlgorithm { + case sha256WithRSAEncryption + case sha1WithRSAEncryption + + init?(nid: Int32) { + switch nid { + case NID_sha256WithRSAEncryption: self = .sha256WithRSAEncryption + case NID_sha1WithRSAEncryption: self = .sha1WithRSAEncryption + default: return nil + } + } + + var hashAlgorithm: HashAlgorithm { + switch self { + case .sha1WithRSAEncryption: return .sha1 + case .sha256WithRSAEncryption: return .sha256 + } + } +} diff --git a/Sources/NemID/X509/CertificateChain.swift b/Sources/NemID/X509/CertificateChain.swift index 5d59a18..2e6ead5 100644 --- a/Sources/NemID/X509/CertificateChain.swift +++ b/Sources/NemID/X509/CertificateChain.swift @@ -1,9 +1,9 @@ import Foundation struct CertificateChain { - let root: X509Certificate - let intermediate: X509Certificate - let leaf: X509Certificate + let root: NemIDX509Certificate + let intermediate: NemIDX509Certificate + let leaf: NemIDX509Certificate } // MARK: Sequence @@ -15,14 +15,14 @@ extension CertificateChain: Sequence { // MARK: - CertificateChainIterator struct CertificateChainIterator: IteratorProtocol { - let certificates: [X509Certificate] + let certificates: [NemIDX509Certificate] private var currentPos = 0 - init(certificates: [X509Certificate]) { + init(certificates: [NemIDX509Certificate]) { self.certificates = certificates } - mutating func next() -> X509Certificate? { + mutating func next() -> NemIDX509Certificate? { if currentPos < certificates.count { let oldPosition = currentPos currentPos += 1 diff --git a/Sources/NemID/X509/X509Certificate.swift b/Sources/NemID/X509/NemIDX509Certificate.swift similarity index 95% rename from Sources/NemID/X509/X509Certificate.swift rename to Sources/NemID/X509/NemIDX509Certificate.swift index ee6af6d..16a3279 100644 --- a/Sources/NemID/X509/X509Certificate.swift +++ b/Sources/NemID/X509/NemIDX509Certificate.swift @@ -8,7 +8,7 @@ enum X509CertificateError: Error { case failedToRetrieveDERRepresentation } -public final class X509Certificate: BIOLoadable { +public final class NemIDX509Certificate: BIOLoadable { /// Initialize a new certificate from a DER string public convenience init(der string: String) throws { try self.init(der: [UInt8](string.utf8)) @@ -36,17 +36,17 @@ public final class X509Certificate: BIOLoadable { } /// Extracts the public key as `RSAKey` - func publicKey() throws -> RSAKey { + func publicKey() throws -> NemIDRSAKey { try withPublicKey { key in guard let rsaKey = CNemIDBoringSSL_EVP_PKEY_get1_RSA(key) else { throw X509CertificateError.failedToRetrievePublicKey } - return RSAKey(rsaKey) + return NemIDRSAKey(rsaKey) } } /// Verifies that `self` was signed with `signer`'s private key. - func isSignedBy(by signer: X509Certificate) throws -> Bool { + func isSignedBy(by signer: NemIDX509Certificate) throws -> Bool { try signer.withPublicKey { pubKey in CNemIDBoringSSL_X509_verify(self.ref, pubKey) == 1 } @@ -227,14 +227,14 @@ public final class X509Certificate: BIOLoadable { } // MARK: Equatable -extension X509Certificate: Equatable { - public static func ==(_ lhs: X509Certificate, _ rhs: X509Certificate) -> Bool { +extension NemIDX509Certificate: Equatable { + public static func ==(_ lhs: NemIDX509Certificate, _ rhs: NemIDX509Certificate) -> Bool { CNemIDBoringSSL_X509_cmp(lhs.ref, rhs.ref) == 0 } } // MARK: - KeyUsage -extension X509Certificate { +extension NemIDX509Certificate { enum KeyUsage { case digitalSignature case keyCertSign @@ -251,7 +251,7 @@ extension X509Certificate { } // MARK: - NameComponent -extension X509Certificate { +extension NemIDX509Certificate { struct NameComponent { let value: Int32 @@ -265,7 +265,7 @@ extension X509Certificate { } // MARK: ExtendedKeyUsage -extension X509Certificate { +extension NemIDX509Certificate { struct ExtendedKeyUsage { let value: Int32 diff --git a/Sources/NemID/XMLDSig/Certificates Extractor/DefaultCertificateExtractor.swift b/Sources/NemID/XMLDSig/Certificates Extractor/DefaultCertificateExtractor.swift index a9f0e8a..763bb02 100644 --- a/Sources/NemID/XMLDSig/Certificates Extractor/DefaultCertificateExtractor.swift +++ b/Sources/NemID/XMLDSig/Certificates Extractor/DefaultCertificateExtractor.swift @@ -3,11 +3,11 @@ import Foundation struct DefaultCertificateExtractor: CertificateExtrator { func extract(from xml: ParsedXMLDSigResponse) throws -> CertificateChain { let certificates = try xml.x509Certificates - .map { base64DerCertificate -> X509Certificate in + .map { base64DerCertificate -> NemIDX509Certificate in guard let decoded = Data(base64Encoded: base64DerCertificate, options: .ignoreUnknownCharacters) else { throw CertificateExtractorError.failedToDecodeCertificate } - return try X509Certificate(der: decoded) + return try NemIDX509Certificate(der: decoded) } guard certificates.count == 3 else { throw CertificateExtractorError.unexpectedCertificateCount(certificates.count) } diff --git a/Tests/NemIDTests/CertificateExtractorTests.swift b/Tests/NemIDTests/CertificateExtractorTests.swift index ed26921..8d0a60f 100644 --- a/Tests/NemIDTests/CertificateExtractorTests.swift +++ b/Tests/NemIDTests/CertificateExtractorTests.swift @@ -10,15 +10,15 @@ final class CertificateExtractorTests: XCTestCase { referenceDigestValue: "", objectToBeSigned: Data(), x509Certificates: [ - TestCertificates.googleRoot, - TestCertificates.googleIntermediate, - TestCertificates.googleLeaf, + TestHelper.googleRoot, + TestHelper.googleIntermediate, + TestHelper.googleLeaf, ] ) let chain = try sut.extract(from: response) - try XCTAssertEqual(chain.root, X509Certificate(der: Data(base64Encoded: TestCertificates.googleRoot, options: .ignoreUnknownCharacters)!)) - try XCTAssertEqual(chain.intermediate, X509Certificate(der: Data(base64Encoded: TestCertificates.googleIntermediate, options: .ignoreUnknownCharacters)!)) - try XCTAssertEqual(chain.leaf, X509Certificate(der: Data(base64Encoded: TestCertificates.googleLeaf, options: .ignoreUnknownCharacters)!)) + try XCTAssertEqual(chain.root, NemIDX509Certificate(der: Data(base64Encoded: TestHelper.googleRoot, options: .ignoreUnknownCharacters)!)) + try XCTAssertEqual(chain.intermediate, NemIDX509Certificate(der: Data(base64Encoded: TestHelper.googleIntermediate, options: .ignoreUnknownCharacters)!)) + try XCTAssertEqual(chain.leaf, NemIDX509Certificate(der: Data(base64Encoded: TestHelper.googleLeaf, options: .ignoreUnknownCharacters)!)) } } diff --git a/Tests/NemIDTests/Helpers/Bytes+Hex.swift b/Tests/NemIDTests/Helpers/Bytes+Hex.swift new file mode 100644 index 0000000..ea42384 --- /dev/null +++ b/Tests/NemIDTests/Helpers/Bytes+Hex.swift @@ -0,0 +1,47 @@ +import Foundation + +// https://github.com/vapor/vapor/blob/main/Sources/Vapor/Utilities/Bytes%2BHex.swift +extension Sequence where Element == UInt8 { + public var hex: String { + self.hexEncodedString() + } + + public func hexEncodedString(uppercase: Bool = false) -> String { + return String(decoding: self.hexEncodedBytes(uppercase: uppercase), as: Unicode.UTF8.self) + } + + public func hexEncodedBytes(uppercase: Bool = false) -> [UInt8] { + let table: [UInt8] = uppercase ? radix16table_uppercase : radix16table_lowercase + var result: [UInt8] = [] + + result.reserveCapacity(self.underestimatedCount * 2) // best guess + return self.reduce(into: result) { output, byte in + output.append(table[numericCast(byte / 16)]) + output.append(table[numericCast(byte % 16)]) + } + } +} + +extension Collection where Element == UInt8 { + public func hexEncodedBytes(uppercase: Bool = false) -> [UInt8] { + let table: [UInt8] = uppercase ? radix16table_uppercase : radix16table_lowercase + + return .init(unsafeUninitializedCapacity: self.count * 2) { buffer, outCount in + for byte in self { + let nibs = byte.quotientAndRemainder(dividingBy: 16) + + buffer[outCount + 0] = table[numericCast(nibs.quotient)] + buffer[outCount + 1] = table[numericCast(nibs.remainder)] + outCount += 2 + } + } + } +} + +fileprivate let radix16table_uppercase: [UInt8] = [ + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46 +] + +fileprivate let radix16table_lowercase: [UInt8] = [ + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66 +] diff --git a/Tests/NemIDTests/Helpers/Date+FromComponents.swift b/Tests/NemIDTests/Helpers/Date+FromComponents.swift new file mode 100644 index 0000000..d600f5f --- /dev/null +++ b/Tests/NemIDTests/Helpers/Date+FromComponents.swift @@ -0,0 +1,16 @@ +import Foundation + +extension Date { + init(year: Int, month: Int, day: Int, hour: Int, minute: Int, second: Int) { + var date = DateComponents() + date.calendar = Calendar(identifier: .gregorian) + date.year = year + date.month = month + date.day = day + date.hour = hour + date.minute = minute + date.second = second + date.timeZone = TimeZone(abbreviation: "UTC") + self = date.date! + } +} diff --git a/Tests/NemIDTests/Helpers/TestCertificates.swift b/Tests/NemIDTests/Helpers/TestHelper.swift similarity index 87% rename from Tests/NemIDTests/Helpers/TestCertificates.swift rename to Tests/NemIDTests/Helpers/TestHelper.swift index c8b3282..2f206fe 100644 --- a/Tests/NemIDTests/Helpers/TestCertificates.swift +++ b/Tests/NemIDTests/Helpers/TestHelper.swift @@ -1,7 +1,7 @@ import Foundation @testable import NemID -enum TestCertificates { +enum TestHelper { static let googleRoot = """ MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4G A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNp @@ -104,4 +104,22 @@ enum TestCertificates { QslRN0DGtMWV0ov9HcRUJ9zktAxMHsAQPZkl1VDLK9fOZiotwcASXZIwPeyHa4kh 9AIGDv5ZLD2L1fAhrmD+KyK3XqRYlN3C0wu5AASOwK2j/MS3nvTPZVCO """ + + static let rsaPrivateKey = """ + -----BEGIN RSA PRIVATE KEY----- + MIICXAIBAAKBgQDMEcwZYbKAGNYfpnGUB+mOSzU1n0VTX3Z0fdpBXscWyPn41FWK + TYXX6CBJ+BLVnLCXPnZF9d0ELfuPSXN7/a3nAmcTiHx9SUUQDlwVpHKmwimNt35j + YwbpAjWJ333XTW6yq+wvTNt8eydOHl5awLV/OlJEELfCBZgDqey/q968gwIDAQAB + AoGAM4Fmbx2ObPBX0uMylXctxqFKy77oQ3O7tQkytf8S5rhRBzGoaWDJoEXRKHo5 + XrrOg03bkirM3sowTOjwAeJ0Kn9KiEkeMlAnIVTtVv+CLsxTMd4hP52qyvNbOpK+ + rohJtn5pvuQ/mKwQOGzzzqsPzRYuOCPrTYmBP5Ac58yTB6ECQQD5aWr17AI9tTum + 6O6wpaPicPTZFjhenFl7obXuBrALimH9LJ7iZnXQkq+iunvyks6aMHYncohj3py9 + qEfOpQjTAkEA0XXDoZW0gTV0K0wHOjuClGledGe9Fmy/r9XmJOvnvdNPRbRK2SOO + 1ebG+5kK4obKeM0QgVpf7I6vcO0qYWIvkQJANwHO+0n//IgaDefVrNP7Xxe2iKJj + 8EnfWmsB6utCrGjqz6GlsR0T4tpXLjae25MRSeRiSrTx68TPIO0aWTMAzQJABw2/ + M87V0FAbhGXADI76e8L8olDoBjxNTD+Yy3+CQ1s9XSyQJLXU1pE5/DkQK8a8RMsr + FiAUAORhNh1WgwcKcQJBALlOUOsgMzxOAmCGdKQuGCGw+vk7KhAX7GbWSDbAOCpc + wHcQvVwfd8D6Vw5XG8cek7PbISE/XRcxUyTGsxblKvA= + -----END RSA PRIVATE KEY----- + """ } diff --git a/Tests/NemIDTests/OCSPRequestTests.swift b/Tests/NemIDTests/OCSPRequestTests.swift index a57344f..545ae45 100644 --- a/Tests/NemIDTests/OCSPRequestTests.swift +++ b/Tests/NemIDTests/OCSPRequestTests.swift @@ -4,8 +4,8 @@ import XCTest @_implementationOnly import CNemIDBoringSSL final class OCSPRequestTests: XCTestCase { - let certificate = try! X509Certificate(der: Data(base64Encoded: TestCertificates.googleLeaf, options: .ignoreUnknownCharacters)!) - let issuer = try! X509Certificate(der: Data(base64Encoded: TestCertificates.googleIntermediate, options: .ignoreUnknownCharacters)!) + let certificate = try! NemIDX509Certificate(der: Data(base64Encoded: TestHelper.googleLeaf, options: .ignoreUnknownCharacters)!) + let issuer = try! NemIDX509Certificate(der: Data(base64Encoded: TestHelper.googleIntermediate, options: .ignoreUnknownCharacters)!) func test_init_parsesOCSPEndpoint() throws { let request = try OCSPRequest(certificate: certificate, issuer: issuer) diff --git a/Tests/NemIDTests/OCSPResponseTests.swift b/Tests/NemIDTests/OCSPResponseTests.swift index 420bd04..f6d056e 100644 --- a/Tests/NemIDTests/OCSPResponseTests.swift +++ b/Tests/NemIDTests/OCSPResponseTests.swift @@ -9,19 +9,164 @@ final class OCSPResponseTests: XCTestCase { XCTAssertNil(response.basicOCSPResponse) } - func test_init_withSuccesfullResponse() throws { - let response = try OCSPResponse(from: [UInt8](Data(base64Encoded: successfullOcspResponse)!)) - XCTAssertEqual(response.responseStatus, .successful) + func test_init_withSuccesfulResponse() throws { + let response = try OCSPResponse(from: [UInt8](Data(base64Encoded: successfulOcspResponse)!)) let basicResponse = try XCTUnwrap(response.basicOCSPResponse) - XCTAssertEqual(basicResponse.signatureAlgorithm, .sha256) let singleResponse = try XCTUnwrap(basicResponse.tbsResponseData.responses.first) - // 2021-03-05 14:11:58Z - XCTAssertEqual(singleResponse.thisUpdate, Date(timeIntervalSince1970: 1614866718)) - // 2021-03-12 13:11:57Z - XCTAssertEqual(singleResponse.nextUpdate, Date(timeIntervalSince1970: 1615467917)) + let certificate = try XCTUnwrap(basicResponse.certs.first) + + let expectedSignature = "xo6zSJb7gemfLet4Y8OHhUMSvKv1ipdQ5HGPHeItShBSo4plJH2+8JvqbZ5ofaFJWzryaBZo2FT7xTbOZcCJ4W/GZwi1Yx/+otWVSi1WyREoqzSmSCBxN+fXOzl0X1j+9QrsaG122gd0qDWBZi1muLiRwXiLdQTX1CI8txxv7N6HlZoIf6iY+TgxoKJtExf3pH9By2BGof57ygwIUrcc+TXGbQoohPlu3RB1IqKIKc1nbajb9deIap0ZAsHfuv9n3P5epIw80VKCbSXrTUjEhP7WGR1lA6IqVTiUohXL7TYDIUKwMY6DgZ44A+1OYdRoajt4HB2KuHVZUfaW76RFEg==" + + XCTAssertEqual(response.responseStatus, .successful) + XCTAssertEqual(basicResponse.signatureAlgorithm, .sha1WithRSAEncryption) + XCTAssertEqual(basicResponse.certs.count, 1) + XCTAssertEqual(Data(basicResponse.signature).base64EncodedString(), expectedSignature) + XCTAssertEqual(singleResponse.certStatus, .good) + XCTAssertEqual(singleResponse.thisUpdate, Date(year: 2021, month: 04, day: 19, hour: 11, minute: 35, second: 08)) + XCTAssertEqual(singleResponse.nextUpdate, Date(year: 2021, month: 04, day: 20, hour: 02, minute: 50, second: 17)) + XCTAssertEqual(singleResponse.certID.hashAlgorithm, .sha256) + XCTAssertEqual(singleResponse.certID.issuerNameHash.hexEncodedString(uppercase: true), "4DF13F909A02EB818EC1C353DE918DF9A4FB4C22E1300351A86682A59CD1AC51") + XCTAssertEqual(singleResponse.certID.issuerKeyHash.hexEncodedString(uppercase: true), "4739EC449DF2CEC9AAE1D6271251C57406C24685AB4C11ABD67301A47E985144") + XCTAssertEqual(singleResponse.certID.serialNumber.hexEncodedString(uppercase: true), "5F9C324B") + XCTAssertEqual(certificate.subjectCommonName, "DANID A/S - Systemtest XXXIV OCSP Responder 02") } - private let successfullOcspResponse = "MIIB1AoBAKCCAc0wggHJBgkrBgEFBQcwAQEEggG6MIIBtjCBn6IWBBSY0fhuEOvPm+xgnxiQG6DrfQn9KxgPMjAyMTAzMDQxNDA1MTlaMHQwcjBKMAkGBSsOAwIaBQAEFEJGMMInGdvecPCP/HPlpl9mOBe8BBSY0fhuEOvPm+xgnxiQG6DrfQn9KwIRAJqpJQj6G3+pBQAAAACHSiaAABgPMjAyMTAzMDQxNDA1MThaoBEYDzIwMjEwMzExMTMwNTE3WjANBgkqhkiG9w0BAQsFAAOCAQEAEpCZ5Dnd35gUM3JRIBRlk+FZ0kZsoMDbt9KBBN2YRd3dThnmHQptOiWv+SeQyX3hpffwxkfuf3vySqo8yMPfFTkEg7QMuWw72ZQ3wLujatl4+YIujijp/nQSCEBlHqG9YQSlm3RVX3xlq07qBDk2GgIOuVwbHsS9lXNOODm8pUMNoBkIdnywcVm3vFE7Pha8UCUdFs9yRjB5Rt0+uYY4gfjAgmFN9KR2cZHjjY+GLd4TSXBD9U9I+u0ekTkS25zbhz+2lJ/Nooj/T60BLTy1ruI+dsQ/S7o9ucpNd1RMU/cB3w36Ku9ZYiGGozYPkfC/GbQJvd0SbJpMw/jq2AYmtw==" + /* + OCSP Response Data: + OCSP Response Status: successful (0x0) + Response Type: Basic OCSP Response + Version: 1 (0x0) + Responder Id: 83AC750FB200DB852BA56E4A90C8965C1F069F6D + Produced At: Apr 19 14:50:17 2021 GMT + Responses: + Certificate ID: + Hash Algorithm: sha256 + Issuer Name Hash: 4DF13F909A02EB818EC1C353DE918DF9A4FB4C22E1300351A86682A59CD1AC51 + Issuer Key Hash: 4739EC449DF2CEC9AAE1D6271251C57406C24685AB4C11ABD67301A47E985144 + Serial Number: 5F9C324B + Cert Status: good + This Update: Apr 19 11:35:08 2021 GMT + Next Update: Apr 20 02:50:17 2021 GMT + + Signature Algorithm: sha1WithRSAEncryption + c6:8e:b3:48:96:fb:81:e9:9f:2d:eb:78:63:c3:87:85:43:12: + bc:ab:f5:8a:97:50:e4:71:8f:1d:e2:2d:4a:10:52:a3:8a:65: + 24:7d:be:f0:9b:ea:6d:9e:68:7d:a1:49:5b:3a:f2:68:16:68: + d8:54:fb:c5:36:ce:65:c0:89:e1:6f:c6:67:08:b5:63:1f:fe: + a2:d5:95:4a:2d:56:c9:11:28:ab:34:a6:48:20:71:37:e7:d7: + 3b:39:74:5f:58:fe:f5:0a:ec:68:6d:76:da:07:74:a8:35:81: + 66:2d:66:b8:b8:91:c1:78:8b:75:04:d7:d4:22:3c:b7:1c:6f: + ec:de:87:95:9a:08:7f:a8:98:f9:38:31:a0:a2:6d:13:17:f7: + a4:7f:41:cb:60:46:a1:fe:7b:ca:0c:08:52:b7:1c:f9:35:c6: + 6d:0a:28:84:f9:6e:dd:10:75:22:a2:88:29:cd:67:6d:a8:db: + f5:d7:88:6a:9d:19:02:c1:df:ba:ff:67:dc:fe:5e:a4:8c:3c: + d1:52:82:6d:25:eb:4d:48:c4:84:fe:d6:19:1d:65:03:a2:2a: + 55:38:94:a2:15:cb:ed:36:03:21:42:b0:31:8e:83:81:9e:38: + 03:ed:4e:61:d4:68:6a:3b:78:1c:1d:8a:b8:75:59:51:f6:96: + ef:a4:45:12 + Certificate: + Data: + Version: 3 (0x2) + Serial Number: 1604072134 (0x5f9c32c6) + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=DK, O=TRUST2408, CN=TRUST2408 Systemtest XXXIV CA + Validity + Not Before: Apr 19 01:00:39 2021 GMT + Not After : Apr 22 01:00:38 2021 GMT + Subject: C=DK, O=DANID A/S // CVR:30808460/serialNumber=CVR:30808460-UID:OCSP01-SYSTEMTEST-XXXIV, CN=DANID A/S - Systemtest XXXIV OCSP Responder 02 + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (2048 bit) + Modulus: + 00:c8:92:0d:eb:c0:af:6a:6d:67:46:48:cb:97:0f: + 02:2d:13:0d:b9:e7:7a:26:59:dc:79:dd:8d:7d:b5: + 3a:5f:fc:57:bb:e1:00:20:e8:94:35:94:bc:6f:4c: + 38:2a:d0:fa:cb:7b:7c:4e:96:c3:ea:0a:d1:66:8d: + 22:9f:0c:a0:3a:1d:1b:31:01:71:df:45:1c:de:25: + 39:07:e5:e4:78:9b:03:30:d3:87:73:c5:77:33:4d: + b4:59:f4:20:fa:be:ab:e4:a1:c5:2b:d1:4f:be:c2: + 63:47:68:a2:56:55:5c:a1:17:c6:a5:35:08:20:b2: + 01:d2:bb:98:79:76:6f:71:3f:12:f4:a9:6f:d8:a3: + 9e:70:9f:5d:3e:64:3a:b9:71:3e:5f:64:7f:d7:c1: + 4c:a9:a1:84:0b:10:4e:d3:35:4b:fb:c0:81:c7:df: + 34:b3:70:4e:e7:a1:0d:bc:f0:2d:a4:95:ba:de:a0: + 4d:53:9d:3c:9b:a0:f4:92:34:60:de:e2:ee:67:be: + b9:f6:96:af:64:0c:b2:df:18:17:f8:eb:26:e6:c3: + 76:c6:c5:64:35:97:d4:a4:fc:cc:3a:95:56:6e:d0: + 80:30:a7:97:0a:eb:49:94:ad:1f:83:0f:b3:08:80: + 2d:44:25:36:37:a8:75:10:12:47:af:1d:40:62:b4: + 9b:35 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Key Usage: critical + Digital Signature + OCSP No Check: + + X509v3 Extended Key Usage: + OCSP Signing + X509v3 Certificate Policies: + Policy: 1.3.6.1.4.1.31313.2.4.6.3.5 + CPS: http://www.trust2408.com/repository + User Notice: + Organization: DanID + Number: 1 + Explicit Text: DanID test certifikater fra denne CA udstedes under OID 1.3.6.1.4.1.31313.2.4.6.3.5. DanID test certificates from this CA are issued under OID 1.3.6.1.4.1.31313.2.4.6.3.5. + + X509v3 Authority Key Identifier: + keyid:CD:6C:68:97:39:72:19:A4:35:AB:64:EA:F4:11:A3:81:87:F8:69:3B + + X509v3 Subject Key Identifier: + 83:AC:75:0F:B2:00:DB:85:2B:A5:6E:4A:90:C8:96:5C:1F:06:9F:6D + X509v3 Basic Constraints: + CA:FALSE + Signature Algorithm: sha256WithRSAEncryption + 96:1d:e6:5d:b1:7f:cf:3d:a7:1a:0f:f4:c9:f3:90:ab:ad:73: + d8:05:84:5f:c7:6e:8b:86:cb:50:b2:02:d7:20:c8:91:88:71: + 63:01:9c:ed:5a:33:2c:0a:5f:17:e5:6d:cd:ab:34:fb:86:94: + 86:62:e5:bf:10:81:f7:1c:58:e5:a5:37:98:0f:8e:a0:47:0e: + 3d:18:0c:8d:94:93:cb:47:38:88:5c:40:3b:5b:88:c4:80:8a: + be:19:fb:7c:c0:38:d2:e3:77:60:7d:a2:36:16:7d:c1:6d:58: + e9:27:58:cb:64:97:a4:ee:a3:06:bb:d3:f4:1a:75:ea:9b:c1: + b7:fd:d6:56:f3:2b:48:d5:4d:27:94:80:6a:95:bf:8d:02:23: + 7b:58:06:af:45:b6:18:9a:d0:ae:66:30:7b:01:6e:09:b5:d7: + 4e:e8:9c:f3:a7:be:ab:61:55:69:47:8a:f2:57:7a:04:2d:81: + 92:1f:e3:6c:0b:0e:0f:a3:87:e6:0f:50:f7:7b:bc:de:27:fe: + 4d:d4:3b:15:50:83:33:c1:4a:21:08:1a:44:94:5c:76:6e:ea: + 11:af:95:12:78:58:7f:0c:e2:a8:38:24:2b:18:fe:e5:01:be: + da:d6:32:52:49:56:e0:6a:44:91:40:1e:c0:73:78:3c:3a:85: + e2:31:86:71 + -----BEGIN CERTIFICATE----- + MIIFDjCCA/agAwIBAgIEX5wyxjANBgkqhkiG9w0BAQsFADBJMQswCQYDVQQGEwJE + SzESMBAGA1UECgwJVFJVU1QyNDA4MSYwJAYDVQQDDB1UUlVTVDI0MDggU3lzdGVt + dGVzdCBYWFhJViBDQTAeFw0yMTA0MTkwMTAwMzlaFw0yMTA0MjIwMTAwMzhaMIGb + MQswCQYDVQQGEwJESzEiMCAGA1UECgwZREFOSUQgQS9TIC8vIENWUjozMDgwODQ2 + MDFoMC8GA1UEBRMoQ1ZSOjMwODA4NDYwLVVJRDpPQ1NQMDEtU1lTVEVNVEVTVC1Y + WFhJVjA1BgNVBAMMLkRBTklEIEEvUyAtIFN5c3RlbXRlc3QgWFhYSVYgT0NTUCBS + ZXNwb25kZXIgMDIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDIkg3r + wK9qbWdGSMuXDwItEw2553omWdx53Y19tTpf/Fe74QAg6JQ1lLxvTDgq0PrLe3xO + lsPqCtFmjSKfDKA6HRsxAXHfRRzeJTkH5eR4mwMw04dzxXczTbRZ9CD6vqvkocUr + 0U++wmNHaKJWVVyhF8alNQggsgHSu5h5dm9xPxL0qW/Yo55wn10+ZDq5cT5fZH/X + wUypoYQLEE7TNUv7wIHH3zSzcE7noQ288C2klbreoE1TnTyboPSSNGDe4u5nvrn2 + lq9kDLLfGBf46ybmw3bGxWQ1l9Sk/Mw6lVZu0IAwp5cK60mUrR+DD7MIgC1EJTY3 + qHUQEkevHUBitJs1AgMBAAGjggGpMIIBpTAOBgNVHQ8BAf8EBAMCB4AwDwYJKwYB + BQUHMAEFBAIFADATBgNVHSUEDDAKBggrBgEFBQcDCTCCASAGA1UdIASCARcwggET + MIIBDwYNKwYBBAGB9FECBAYDBTCB/TAvBggrBgEFBQcCARYjaHR0cDovL3d3dy50 + cnVzdDI0MDguY29tL3JlcG9zaXRvcnkwgckGCCsGAQUFBwICMIG8MAwWBURhbklE + MAMCAQEagatEYW5JRCB0ZXN0IGNlcnRpZmlrYXRlciBmcmEgZGVubmUgQ0EgdWRz + dGVkZXMgdW5kZXIgT0lEIDEuMy42LjEuNC4xLjMxMzEzLjIuNC42LjMuNS4gRGFu + SUQgdGVzdCBjZXJ0aWZpY2F0ZXMgZnJvbSB0aGlzIENBIGFyZSBpc3N1ZWQgdW5k + ZXIgT0lEIDEuMy42LjEuNC4xLjMxMzEzLjIuNC42LjMuNS4wHwYDVR0jBBgwFoAU + zWxolzlyGaQ1q2Tq9BGjgYf4aTswHQYDVR0OBBYEFIOsdQ+yANuFK6VuSpDIllwf + Bp9tMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEBAJYd5l2xf889pxoP9Mnz + kKutc9gFhF/HbouGy1CyAtcgyJGIcWMBnO1aMywKXxflbc2rNPuGlIZi5b8Qgfcc + WOWlN5gPjqBHDj0YDI2Uk8tHOIhcQDtbiMSAir4Z+3zAONLjd2B9ojYWfcFtWOkn + WMtkl6Tuowa70/Qadeqbwbf91lbzK0jVTSeUgGqVv40CI3tYBq9Fthia0K5mMHsB + bgm1107onPOnvqthVWlHivJXegQtgZIf42wLDg+jh+YPUPd7vN4n/k3UOxVQgzPB + SiEIGkSUXHZu6hGvlRJ4WH8M4qg4JCsY/uUBvtrWwMlJJVuBqRJFAHsBzeDw6heIx + hnE= + -----END CERTIFICATE----- + */ + private let successfulOcspResponse = "MIIG/woBAKCCBvgwggb0BgkrBgEFBQcwAQEEggblMIIG4TCBsKIWBBSDrHUPsgDbhSulbkqQyJZcHwafbRgPMjAyMTA0MTkxNDUwMTdaMIGEMIGBMFkwDQYJYIZIAWUDBAIBBQAEIE3xP5CaAuuBjsHDU96Rjfmk+0wi4TADUahmgqWc0axRBCBHOexEnfLOyarh1icSUcV0BsJGhatMEavWcwGkfphRRAIEX5wyS4AAGA8yMDIxMDQxOTExMzUwOFqgERgPMjAyMTA0MjAwMjUwMTdaMA0GCSqGSIb3DQEBBQUAA4IBAQDGjrNIlvuB6Z8t63hjw4eFQxK8q/WKl1DkcY8d4i1KEFKjimUkfb7wm+ptnmh9oUlbOvJoFmjYVPvFNs5lwInhb8ZnCLVjH/6i1ZVKLVbJESirNKZIIHE359c7OXRfWP71CuxobXbaB3SoNYFmLWa4uJHBeIt1BNfUIjy3HG/s3oeVmgh/qJj5ODGgom0TF/ekf0HLYEah/nvKDAhStxz5NcZtCiiE+W7dEHUioogpzWdtqNv114hqnRkCwd+6/2fc/l6kjDzRUoJtJetNSMSE/tYZHWUDoipVOJSiFcvtNgMhQrAxjoOBnjgD7U5h1GhqO3gcHYq4dVlR9pbvpEUSoIIFFjCCBRIwggUOMIID9qADAgECAgRfnDLGMA0GCSqGSIb3DQEBCwUAMEkxCzAJBgNVBAYTAkRLMRIwEAYDVQQKDAlUUlVTVDI0MDgxJjAkBgNVBAMMHVRSVVNUMjQwOCBTeXN0ZW10ZXN0IFhYWElWIENBMB4XDTIxMDQxOTAxMDAzOVoXDTIxMDQyMjAxMDAzOFowgZsxCzAJBgNVBAYTAkRLMSIwIAYDVQQKDBlEQU5JRCBBL1MgLy8gQ1ZSOjMwODA4NDYwMWgwLwYDVQQFEyhDVlI6MzA4MDg0NjAtVUlEOk9DU1AwMS1TWVNURU1URVNULVhYWElWMDUGA1UEAwwuREFOSUQgQS9TIC0gU3lzdGVtdGVzdCBYWFhJViBPQ1NQIFJlc3BvbmRlciAwMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMiSDevAr2ptZ0ZIy5cPAi0TDbnneiZZ3HndjX21Ol/8V7vhACDolDWUvG9MOCrQ+st7fE6Ww+oK0WaNIp8MoDodGzEBcd9FHN4lOQfl5HibAzDTh3PFdzNNtFn0IPq+q+ShxSvRT77CY0doolZVXKEXxqU1CCCyAdK7mHl2b3E/EvSpb9ijnnCfXT5kOrlxPl9kf9fBTKmhhAsQTtM1S/vAgcffNLNwTuehDbzwLaSVut6gTVOdPJug9JI0YN7i7me+ufaWr2QMst8YF/jrJubDdsbFZDWX1KT8zDqVVm7QgDCnlwrrSZStH4MPswiALUQlNjeodRASR68dQGK0mzUCAwEAAaOCAakwggGlMA4GA1UdDwEB/wQEAwIHgDAPBgkrBgEFBQcwAQUEAgUAMBMGA1UdJQQMMAoGCCsGAQUFBwMJMIIBIAYDVR0gBIIBFzCCARMwggEPBg0rBgEEAYH0UQIEBgMFMIH9MC8GCCsGAQUFBwIBFiNodHRwOi8vd3d3LnRydXN0MjQwOC5jb20vcmVwb3NpdG9yeTCByQYIKwYBBQUHAgIwgbwwDBYFRGFuSUQwAwIBARqBq0RhbklEIHRlc3QgY2VydGlmaWthdGVyIGZyYSBkZW5uZSBDQSB1ZHN0ZWRlcyB1bmRlciBPSUQgMS4zLjYuMS40LjEuMzEzMTMuMi40LjYuMy41LiBEYW5JRCB0ZXN0IGNlcnRpZmljYXRlcyBmcm9tIHRoaXMgQ0EgYXJlIGlzc3VlZCB1bmRlciBPSUQgMS4zLjYuMS40LjEuMzEzMTMuMi40LjYuMy41LjAfBgNVHSMEGDAWgBTNbGiXOXIZpDWrZOr0EaOBh/hpOzAdBgNVHQ4EFgQUg6x1D7IA24UrpW5KkMiWXB8Gn20wCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAQEAlh3mXbF/zz2nGg/0yfOQq61z2AWEX8dui4bLULIC1yDIkYhxYwGc7VozLApfF+Vtzas0+4aUhmLlvxCB9xxY5aU3mA+OoEcOPRgMjZSTy0c4iFxAO1uIxICKvhn7fMA40uN3YH2iNhZ9wW1Y6SdYy2SXpO6jBrvT9Bp16pvBt/3WVvMrSNVNJ5SAapW/jQIje1gGr0W2GJrQrmYwewFuCbXXTuic86e+q2FVaUeK8ld6BC2Bkh/jbAsOD6OH5g9Q93u83if+TdQ7FVCDM8FKIQgaRJRcdm7qEa+VEnhYfwziqDgkKxj+5QG+2tYyUklW4GpEkUAewHN4PDqF4jGGcQ==" private let unauthorizedOcspResponse = "MAMKAQY=" } diff --git a/Tests/NemIDTests/ParameterSignerTests.swift b/Tests/NemIDTests/ParameterSignerTests.swift index 6e30b17..7fd65b6 100644 --- a/Tests/NemIDTests/ParameterSignerTests.swift +++ b/Tests/NemIDTests/ParameterSignerTests.swift @@ -15,8 +15,8 @@ final class ParameterSignerTests: XCTestCase { enableAwaitingAppApprovalEvent: true ) - let key = try RSAKey.private(pem: rsaPrivateKey) - let certificate = try X509Certificate(der: Data(base64Encoded: TestCertificates.googleLeaf, options: .ignoreUnknownCharacters)!) + let key = try NemIDRSAKey.private(pem: TestHelper.rsaPrivateKey) + let certificate = try NemIDX509Certificate(der: Data(base64Encoded: TestHelper.googleLeaf, options: .ignoreUnknownCharacters)!) let configuration = NemIDConfiguration(spCertificate: certificate, privateKey: key, serviceProviderID: "", environment: .preproduction) let signer = NemIDParametersSigner(configuration: configuration) @@ -34,21 +34,3 @@ final class ParameterSignerTests: XCTestCase { XCTAssertEqual(signedParameters.enableAwaitingAppApprovalEvent, true) } } - -let rsaPrivateKey = """ ------BEGIN RSA PRIVATE KEY----- -MIICXAIBAAKBgQDMEcwZYbKAGNYfpnGUB+mOSzU1n0VTX3Z0fdpBXscWyPn41FWK -TYXX6CBJ+BLVnLCXPnZF9d0ELfuPSXN7/a3nAmcTiHx9SUUQDlwVpHKmwimNt35j -YwbpAjWJ333XTW6yq+wvTNt8eydOHl5awLV/OlJEELfCBZgDqey/q968gwIDAQAB -AoGAM4Fmbx2ObPBX0uMylXctxqFKy77oQ3O7tQkytf8S5rhRBzGoaWDJoEXRKHo5 -XrrOg03bkirM3sowTOjwAeJ0Kn9KiEkeMlAnIVTtVv+CLsxTMd4hP52qyvNbOpK+ -rohJtn5pvuQ/mKwQOGzzzqsPzRYuOCPrTYmBP5Ac58yTB6ECQQD5aWr17AI9tTum -6O6wpaPicPTZFjhenFl7obXuBrALimH9LJ7iZnXQkq+iunvyks6aMHYncohj3py9 -qEfOpQjTAkEA0XXDoZW0gTV0K0wHOjuClGledGe9Fmy/r9XmJOvnvdNPRbRK2SOO -1ebG+5kK4obKeM0QgVpf7I6vcO0qYWIvkQJANwHO+0n//IgaDefVrNP7Xxe2iKJj -8EnfWmsB6utCrGjqz6GlsR0T4tpXLjae25MRSeRiSrTx68TPIO0aWTMAzQJABw2/ -M87V0FAbhGXADI76e8L8olDoBjxNTD+Yy3+CQ1s9XSyQJLXU1pE5/DkQK8a8RMsr -FiAUAORhNh1WgwcKcQJBALlOUOsgMzxOAmCGdKQuGCGw+vk7KhAX7GbWSDbAOCpc -wHcQvVwfd8D6Vw5XG8cek7PbISE/XRcxUyTGsxblKvA= ------END RSA PRIVATE KEY----- -""" diff --git a/Tests/NemIDTests/RSASignerTests.swift b/Tests/NemIDTests/RSASignerTests.swift new file mode 100644 index 0000000..8b33003 --- /dev/null +++ b/Tests/NemIDTests/RSASignerTests.swift @@ -0,0 +1,14 @@ +import XCTest +@testable import NemID +@_implementationOnly import CNemIDBoringSSL + +final class RSASignerTests: XCTestCase { + func test_sign() throws { + let plaintext = [UInt8]("hello".utf8) + let signer = try RSASigner(key: .private(pem: TestHelper.rsaPrivateKey), hashAlgorithm: .sha256) + let ciphertext = try signer.sign(plaintext) + + try XCTAssertTrue(signer.verify(ciphertext, signs: plaintext)) + } +} + diff --git a/Tests/NemIDTests/X509CertificateTests.swift b/Tests/NemIDTests/X509CertificateTests.swift index 0f8b22a..2095539 100644 --- a/Tests/NemIDTests/X509CertificateTests.swift +++ b/Tests/NemIDTests/X509CertificateTests.swift @@ -4,59 +4,59 @@ import XCTest final class X509CertificateTests: XCTestCase { func test_notBefore() throws { - let certificate = try X509Certificate(der: Data(base64Encoded: TestCertificates.googleRoot, options: .ignoreUnknownCharacters)!) + let certificate = try NemIDX509Certificate(der: Data(base64Encoded: TestHelper.googleRoot, options: .ignoreUnknownCharacters)!) // 15/12/2006 08:00:00 GMT XCTAssertEqual(certificate.notBefore(), Date(timeIntervalSince1970: 1166169600)) } func test_notAfter() throws { - let certificate = try X509Certificate(der: Data(base64Encoded: TestCertificates.googleRoot, options: .ignoreUnknownCharacters)!) + let certificate = try NemIDX509Certificate(der: Data(base64Encoded: TestHelper.googleRoot, options: .ignoreUnknownCharacters)!) // 15/12/2021 08:00:00 GMT XCTAssertEqual(certificate.notAfter(), Date(timeIntervalSince1970: 1639555200)) } func test_hasCAFlag_withRoot_returnsTrue() throws { - let certificate = try X509Certificate(der: Data(base64Encoded: TestCertificates.googleRoot, options: .ignoreUnknownCharacters)!) + let certificate = try NemIDX509Certificate(der: Data(base64Encoded: TestHelper.googleRoot, options: .ignoreUnknownCharacters)!) XCTAssertTrue(certificate.hasCAFlag()) } func test_hasCAFlag_withLeaf_returnsFalse() throws { - let certificate = try X509Certificate(der: Data(base64Encoded: TestCertificates.googleLeaf, options: .ignoreUnknownCharacters)!) + let certificate = try NemIDX509Certificate(der: Data(base64Encoded: TestHelper.googleLeaf, options: .ignoreUnknownCharacters)!) XCTAssertFalse(certificate.hasCAFlag()) } func test_hasKeyUsage_digitalSignature_withRoot_returnsFalse() throws { - let certificate = try X509Certificate(der: Data(base64Encoded: TestCertificates.googleRoot, options: .ignoreUnknownCharacters)!) + let certificate = try NemIDX509Certificate(der: Data(base64Encoded: TestHelper.googleRoot, options: .ignoreUnknownCharacters)!) XCTAssertFalse(certificate.hasKeyUsage(.digitalSignature)) } func test_hasKeyUsage_digitalSignature_withLeaf_returnsTrue() throws { - let certificate = try X509Certificate(der: Data(base64Encoded: TestCertificates.googleLeaf, options: .ignoreUnknownCharacters)!) + let certificate = try NemIDX509Certificate(der: Data(base64Encoded: TestHelper.googleLeaf, options: .ignoreUnknownCharacters)!) XCTAssertTrue(certificate.hasKeyUsage(.digitalSignature)) } func test_hasKeyUsage_keyCertSign_withRoot_returnsTrue() throws { - let certificate = try X509Certificate(der: Data(base64Encoded: TestCertificates.googleRoot, options: .ignoreUnknownCharacters)!) + let certificate = try NemIDX509Certificate(der: Data(base64Encoded: TestHelper.googleRoot, options: .ignoreUnknownCharacters)!) XCTAssertTrue(certificate.hasKeyUsage(.keyCertSign)) } func test_hasKeyUsage_keyCertSign_withLeaf_returnFalse() throws { - let certificate = try X509Certificate(der: Data(base64Encoded: TestCertificates.googleLeaf, options: .ignoreUnknownCharacters)!) + let certificate = try NemIDX509Certificate(der: Data(base64Encoded: TestHelper.googleLeaf, options: .ignoreUnknownCharacters)!) XCTAssertFalse(certificate.hasKeyUsage(.keyCertSign)) } func test_commonName_withLeaf_returnsCorrectCommonName() throws { - let certificate = try X509Certificate(der: Data(base64Encoded: TestCertificates.googleLeaf, options: .ignoreUnknownCharacters)!) + let certificate = try NemIDX509Certificate(der: Data(base64Encoded: TestHelper.googleLeaf, options: .ignoreUnknownCharacters)!) XCTAssertEqual(certificate.subjectCommonName, "*.google.com") } func test_hasExtendedKeyUsage_ocspSinging_withGoogleCert_returnsFalse() throws { - let certificate = try X509Certificate(der: Data(base64Encoded: TestCertificates.googleLeaf, options: .ignoreUnknownCharacters)!) + let certificate = try NemIDX509Certificate(der: Data(base64Encoded: TestHelper.googleLeaf, options: .ignoreUnknownCharacters)!) XCTAssertEqual(certificate.hasExtendedKeyUsage(.ocspSigning), false) } func test_hasOCSPNoCheckExtension_withGoogleCert_returnsFalse() throws { - let certificate = try X509Certificate(der: Data(base64Encoded: TestCertificates.googleLeaf, options: .ignoreUnknownCharacters)!) + let certificate = try NemIDX509Certificate(der: Data(base64Encoded: TestHelper.googleLeaf, options: .ignoreUnknownCharacters)!) XCTAssertEqual(certificate.hasOCSPNoCheckExtension(), false) } From 7bd7be8c68d7cdb0b8435fa5842bae36b2a4e848 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Tue, 20 Apr 2021 21:03:45 +0200 Subject: [PATCH 15/22] Fix serial number checking crash --- Sources/NemID/OCSP/OCSPRequest.swift | 5 +-- .../NemIDResponseHandler.swift | 33 ++++++++-------- Sources/NemID/X509/NemIDX509Certificate.swift | 38 +++++++++---------- 3 files changed, 33 insertions(+), 43 deletions(-) diff --git a/Sources/NemID/OCSP/OCSPRequest.swift b/Sources/NemID/OCSP/OCSPRequest.swift index 241a2dc..7a72820 100644 --- a/Sources/NemID/OCSP/OCSPRequest.swift +++ b/Sources/NemID/OCSP/OCSPRequest.swift @@ -26,9 +26,6 @@ struct OCSPRequest { guard let issuerSubjectSHA256 = issuer.hashedSubject else { throw OCSPRequestError.failedToGetIssuerSubject } - guard let publicKeySHA256 = issuer.hashedPublicKey else { - throw OCSPRequestError.failedToGetIssuerPublicKey - } // Create OCSPRequest ASN1 sequence var cbb = CBB() @@ -79,7 +76,7 @@ struct OCSPRequest { } // Set issuerKeyHash - guard CNemIDBoringSSL_CBB_add_asn1_octet_string(&certID, publicKeySHA256, publicKeySHA256.count) == 1 else { + guard CNemIDBoringSSL_CBB_add_asn1_octet_string(&certID, issuer.hashedPublicKey, issuer.hashedPublicKey.count) == 1 else { throw OCSPRequestError.failedToGenerateRequest } diff --git a/Sources/NemID/Response Handler/NemIDResponseHandler.swift b/Sources/NemID/Response Handler/NemIDResponseHandler.swift index 468e1ee..a061f57 100644 --- a/Sources/NemID/Response Handler/NemIDResponseHandler.swift +++ b/Sources/NemID/Response Handler/NemIDResponseHandler.swift @@ -84,11 +84,12 @@ struct NemIDResponseHandler { guard let ocspCertificate = basicResponse.certs.first else { throw NemIDResponseHandlerError.ocspCertificateNotFoundInResponse } - #warning("signature is fixed now, now we need the derBytes fixed.") + #warning("der bytes has correct length, so I think it's the signer failing?s") let signer = RSASigner(key: try ocspCertificate.publicKey(), hashAlgorithm: basicResponse.signatureAlgorithm.hashAlgorithm) - guard try signer.verify(basicResponse.signature, signs: basicResponse.tbsResponseData.derBytes) else { - throw NemIDResponseHandlerError.ocspSignatureWasNotSignedByCertificate - } + +// guard try signer.verify(basicResponse.signature, signs: basicResponse.tbsResponseData.derBytes) else { +// throw NemIDResponseHandlerError.ocspSignatureWasNotSignedByCertificate +// } // Validate that accompanying certificate was signed by issuer. guard try ocspCertificate.isSignedBy(by: chain.intermediate) else { @@ -108,15 +109,16 @@ struct NemIDResponseHandler { throw NemIDResponseHandlerError.ocspCertificateWrongHashAlgorithm } + #warning("todo: remove fatalerror") // Check hash name, key hash and serial number are the ones we sent in the request. - #warning("crashes") -// try chain.leaf.withSerialNumber { serialNumber in -// var ptr: UnsafeMutablePointer? -// ptr = nil -// let leafSerialNumberSize = CNemIDBoringSSL_BN_bn2bin(serialNumber, ptr) -// let leafSerialNumberBytes = [UInt8](UnsafeMutableBufferPointer(start: ptr, count: leafSerialNumberSize)) -// guard certResponse.certID.serialNumber == leafSerialNumberBytes else { fatalError() } -// } + try chain.leaf.withSerialNumber { serialNumber in + var output = [UInt8](repeating: 0, count: numericCast(CNemIDBoringSSL_BN_num_bytes(serialNumber))) + let leafSerialNumberSize = CNemIDBoringSSL_BN_bn2bin(serialNumber, &output) + let leafSerialNumberBytes = [UInt8](output[0.. notBefore else { + guard Date() < certificate.notAfter() && Date() > certificate.notBefore() else { throw NemIDResponseHandlerError.certificateIsOutsideValidTime } } diff --git a/Sources/NemID/X509/NemIDX509Certificate.swift b/Sources/NemID/X509/NemIDX509Certificate.swift index 16a3279..b8fe37e 100644 --- a/Sources/NemID/X509/NemIDX509Certificate.swift +++ b/Sources/NemID/X509/NemIDX509Certificate.swift @@ -53,14 +53,14 @@ public final class NemIDX509Certificate: BIOLoadable { } /// Returns the certificate notBefore as a `Date` - func notBefore() -> Date? { - guard let notBefore = CNemIDBoringSSL_X509_get0_notBefore(self.ref) else { return nil } + func notBefore() -> Date { + let notBefore = CNemIDBoringSSL_X509_get0_notBefore(self.ref)! return Date(timeIntervalSince1970: TimeInterval(notBefore.timeSinceEpoch)) } /// Returns the certificate notAfter as a `Date` - func notAfter() -> Date? { - guard let notAfter = CNemIDBoringSSL_X509_get0_notAfter(self.ref) else { return nil } + func notAfter() -> Date { + let notAfter = CNemIDBoringSSL_X509_get0_notAfter(self.ref)! return Date(timeIntervalSince1970: TimeInterval(notAfter.timeSinceEpoch)) } @@ -97,13 +97,11 @@ public final class NemIDX509Certificate: BIOLoadable { /// Returns the subject as ASN.1/DER encoded bytes. var subject: [UInt8]? { let _subjectName = CNemIDBoringSSL_X509_get_subject_name(ref) - var subjectNameBytes: UnsafeMutablePointer? let length = CNemIDBoringSSL_i2d_X509_NAME(_subjectName, &subjectNameBytes) - guard let subjectNamePointer = subjectNameBytes else { return nil } + defer { CNemIDBoringSSL_OPENSSL_free(subjectNamePointer) } let asn1Bytes = [UInt8](UnsafeBufferPointer(start: subjectNamePointer, count: numericCast(length))) - CNemIDBoringSSL_OPENSSL_free(subjectNamePointer) return asn1Bytes } @@ -115,8 +113,8 @@ public final class NemIDX509Certificate: BIOLoadable { let length = CNemIDBoringSSL_i2d_X509_NAME(_name, &nameBytes) guard let namePointer = nameBytes else { return nil } + defer { CNemIDBoringSSL_OPENSSL_free(namePointer) } let asn1Bytes = [UInt8](UnsafeBufferPointer(start: namePointer, count: numericCast(length))) - CNemIDBoringSSL_OPENSSL_free(namePointer) return asn1Bytes } @@ -132,25 +130,23 @@ public final class NemIDX509Certificate: BIOLoadable { /// Allows access to the certificate's serial number as `BIGNUM` func withSerialNumber(_ closure: (UnsafeMutablePointer) throws -> Void) throws { - guard let serialNumberASN1 = CNemIDBoringSSL_X509_get0_serialNumber(self.ref) else { + let serialNumberASN1 = CNemIDBoringSSL_X509_get_serialNumber(self.ref)! + guard let bn = CNemIDBoringSSL_ASN1_INTEGER_to_BN(serialNumberASN1, nil) else { throw X509CertificateError.failedToGetSerialNumber } - var bn = BIGNUM() - CNemIDBoringSSL_BN_init(&bn) - CNemIDBoringSSL_ASN1_INTEGER_to_BN(serialNumberASN1, &bn) - try closure(&bn) - CNemIDBoringSSL_BN_clear(&bn) + defer { CNemIDBoringSSL_BN_free(bn) } + try closure(bn) } /// Returns the certificate's public key as SHA256 encoded DER bytes. - var hashedPublicKey: [UInt8]? { - guard let pubKeyASN1 = CNemIDBoringSSL_X509_get0_pubkey_bitstr(self.ref) else { - return nil - } - + var hashedPublicKey: [UInt8] { + let pubKeyASN1 = CNemIDBoringSSL_X509_get0_pubkey_bitstr(self.ref)! // No need to copy data - let pubKeyData = Data(bytesNoCopy: CNemIDBoringSSL_ASN1_STRING_data(pubKeyASN1), count: numericCast(CNemIDBoringSSL_ASN1_STRING_length(pubKeyASN1)), deallocator: .none) - + let pubKeyData = Data( + bytesNoCopy: CNemIDBoringSSL_ASN1_STRING_data(pubKeyASN1), + count: numericCast(CNemIDBoringSSL_ASN1_STRING_length(pubKeyASN1)), + deallocator: .none + ) return [UInt8](SHA256.hash(data: pubKeyData)) } From a9ee98ad27830e6b8438f523308196f69a120ef9 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Wed, 21 Apr 2021 18:37:41 +0200 Subject: [PATCH 16/22] clean up error handling --- Sources/NemID/NemIDError.swift | 280 +----------------- Sources/NemID/NemIDLoginService.swift | 1 - Sources/NemID/NemIDResponseError.swift | 280 ++++++++++++++++++ Sources/NemID/NemIDUser.swift | 15 +- Sources/NemID/RSA/NemIDRSAKey.swift | 4 +- .../CertificateChainValidationError.swift | 10 + .../NemIDResponseHandler.swift | 67 ++--- .../NemIDResponseHandlerError.swift | 23 -- .../OCSPValidationError.swift | 18 ++ .../Response Handler/XMLValidationError.swift | 10 + Sources/NemID/Utilities/BIOLoadable.swift | 14 +- Sources/NemID/X509/NemIDX509Certificate.swift | 24 +- .../X509/NemIDX509CertificateError.swift | 7 + ...rror.swift => CertificateChainError.swift} | 2 +- .../CertificateExtractor.swift | 5 - .../DefaultCertificateExtractor.swift | 50 ---- .../NemID/XMLDSig/ParsedXMLDSigResponse.swift | 49 +++ .../XMLDSigParser/libxml2DSigParser.swift | 50 ++-- ...swift => ParsedXMLDSigResponseTests.swift} | 5 +- .../NemIDTests/libxml2XMLDigParserTests.swift | 8 +- 20 files changed, 466 insertions(+), 456 deletions(-) create mode 100644 Sources/NemID/NemIDResponseError.swift create mode 100644 Sources/NemID/Response Handler/CertificateChainValidationError.swift create mode 100644 Sources/NemID/Response Handler/OCSPValidationError.swift create mode 100644 Sources/NemID/Response Handler/XMLValidationError.swift create mode 100644 Sources/NemID/X509/NemIDX509CertificateError.swift rename Sources/NemID/XMLDSig/{Certificates Extractor/CertificateExtractorError.swift => CertificateChainError.swift} (89%) delete mode 100644 Sources/NemID/XMLDSig/Certificates Extractor/CertificateExtractor.swift delete mode 100644 Sources/NemID/XMLDSig/Certificates Extractor/DefaultCertificateExtractor.swift rename Tests/NemIDTests/{CertificateExtractorTests.swift => ParsedXMLDSigResponseTests.swift} (85%) diff --git a/Sources/NemID/NemIDError.swift b/Sources/NemID/NemIDError.swift index edffc08..178b41d 100644 --- a/Sources/NemID/NemIDError.swift +++ b/Sources/NemID/NemIDError.swift @@ -1,280 +1,6 @@ import Foundation -/// See a full list at [NemID Errors](https://www.nets.eu/dk-da/kundeservice/nemid-tjenesteudbyder/NemID-tjenesteudbyderpakken/Documents/NemID%20Error%20Codes.pdf) -public enum NemIDError: String { - case APP001 - case APP002 - case APP003 - case APP004 - case APP007 - case APP008 - case APP009 - case APP010 - - case AUTH001 - case AUTH004 - case AUTH005 - case AUTH006 - case AUTH007 - case AUTH008 - case AUTH009 - case AUTH010 - case AUTH011 - case AUTH012 - case AUTH013 - case AUTH017 - case AUTH018 - case AUTH019 - case AUTH020 - case AUTH021 - - case CAN001 - case CAN002 - case CAN003 - case CAN004 - case CAN005 - case CAN007 - case CAN008 - - case LOCK001 - case LOCK002 - case LOCK003 - - case SRV001 - case SRV002 - case SRV003 - case SRV004 - case SRV005 - case SRV006 - case SRV007 - case SRV008 - case SRV010 - case SRV011 - case SRV012 - - case OCES001 - case OCES002 - case OCES003 - case OCES004 - case OCES005 - case OCES006 - - /// Returns a text suitable for showing the user in English. - public var englishDescrption: String { self.metadata.englishDescription } - - /// Returns a text suitable for showing the user in Danish. - public var danishDescription: String { self.metadata.danishDescription } - - // MARK: Private - private struct Metadata { - let englishDescription: String - let danishDescription: String - - static var `default`: Self = .init( - englishDescription: "A technical error has occured. Contact the service provider if the problem persists.", - danishDescription: "Der er opstået en teknisk fejl. Kontakt tjenesteudbyder hvis problemet forsætter." - ) - } - - private var metadata: Metadata { - switch self { - case .APP001: - return .default - case .APP002: - return .default - case .APP003: - return .default - case .APP004: - return .default - case .APP007: - return .default - case .APP008: - return .default - case .APP009: - return .default - case .APP010: - return .default - case .AUTH001: - return .init( - englishDescription: "Your NemID is blocked. Please contact NemID support.", - danishDescription: "Dit NemID er spærret. Kontakt NemID support." - ) - case .AUTH004: - return .init( - englishDescription: "Your NemID is temporarily locked and you cannot log on until the 8 hour time lock has been lifted.", - danishDescription: "Dit NemID er midlertidigt låst i 8 timer og du kan ikke logge på før spærringen er ophævet." - ) - case .AUTH005: - return .init( - englishDescription: "Your NemID is blocked. Please contact NemID support.", - danishDescription: "Dit NemID er spærret. Kontakt NemID support." - ) - case .AUTH006: - return .init( - englishDescription: "You have used all the codes on your code card.", - danishDescription: "Du har brugt alle nøgler på nøglekortet." - ) - case .AUTH007: - return .init( - englishDescription: "Your NemID password is blocked due to too many failed password attempts.", - danishDescription: "Din NemID-adgangskode er spærret på grund af for mange fejlede forsøg." - ) - case .AUTH008: - return .init( - englishDescription: "Your NemID is not active and you need support to issue a new activation password to activate.", - danishDescription: "Dit NemID er ikke aktivt og du skal bestille en ny midlertidig adgangskode til aktivering hos support." - ) - case .AUTH009: - return .default - case .AUTH010: - return .default - case .AUTH011: - return .init( - englishDescription: "NemID login on mobile does not support authentication using a temporary password.", - danishDescription: "NemID på mobil understøtter ikke brug af midlertidig adgangskode." - ) - case .AUTH012: - return .default - case .AUTH013: - return .default - case .AUTH017: - return .init( - englishDescription: "Something in the browser environment has caused NemID to stop working. This could be because of an incompatible plug- in, too restrictive privacy settings or other environment factors.", - danishDescription: "En teknisk fejl i browseren gør at NemID ikke kan starte." - ) - case .AUTH018: - return .init( - englishDescription: "Your code app is revoked. To use it again please reactivate it.", - danishDescription: "Din nøgleapp er spærret. For at bruge den igen skal den genaktiveres." - ) - case .AUTH019: - return .init( - englishDescription: "It is not possible to login with a code card, please use a code app or code token.", - danishDescription: "Det er ikke muligt at logge ind med nøglekort, brug anden løsning nøgleapp eller nøgleviser." - ) - case .AUTH020: - return .init( - englishDescription: "Unable to login with 1-factor, please try with 2-factor login.", - danishDescription: "Kunne ikke logge ind med 1- faktor, prøv med 2-faktor login." - ) - case .AUTH021: - return .init( - englishDescription: "This NemID is no longer valid due to insufficient identification of the user", - danishDescription: "Det er ikke længere muligt at logge ind med dette NemID pga. manglende opdatering af identitetsoplysninger." - ) - case .CAN001: - return .init( - englishDescription: "You have cancelled the activation of NemID after submitting the activation password.", - danishDescription: "Du har afbrudt aktiveringen efter du har brugt den midlertidige adgangskode." - ) - case .CAN002: - return .init(englishDescription: "You have canelled the login.", danishDescription: "Du har afbrudt login.") - case .CAN003: - return .init( - englishDescription: "The connection to the application has timed out or has been interrupted by another app", - danishDescription: "Forbindelsen til applikationen er timet ud eller er blevet afbrudt af en anden app." - ) - case .CAN004: - return .init(englishDescription: "The session is cancelled", danishDescription: "Session er afbrudt") - case .CAN005: - return .init( - englishDescription: "You took too long to authenticate the request you had sent to your code app.", - danishDescription: "Det tog for lang tid, før du godkendte den anmodning, du havde sendt til din nøgleapp" - ) - case .CAN007: - return .init( - englishDescription: "You rejected your code app authentication request. If this was incorrect, you can submit a new request after clicking “OK” to finish.", - danishDescription: "Du har afvist din anmodning om godkendelse i din nøgleapp. Hvis det var en fejl, kan du sende en ny anmodning, når du har afsluttet ved at klikke på ”Ok”." - ) - case .CAN008: - return .init( - englishDescription: "You sent a new authentication request to your code app overwriting an existing one.", - danishDescription: "Du har sendt en ny anmodning til godkendelse i din nøgleapp, som overskriver en eksisterende." - ) - case .LOCK001: - return .init( - englishDescription: "You have used the wrong user ID or password too many times. Your NemID is now blocked for 8 hours after which you can try again.", - danishDescription: "Du har angivet forkert bruger- id eller adgangskode for mange gange. NemID er nu spærret i 8 timer, hvorefter du kan forsøge igen" - ) - case .LOCK002: - return .init( - englishDescription: "You have used a wrong password too many times. Your NemID is blocked and cannot be used.", - danishDescription: "Du har angivet en forkert adgangskode for mange gange. Dit NemID er spærret." - ) - case .LOCK003: - return .init( - englishDescription: "You have entered a wrong NemID key too many times. Your NemID is blocked and cannot be used.", - danishDescription: "Du har angivet forkert NemID nøgle for mange gange. Dit NemID er spærret." - ) - case .SRV001: - return .default - case .SRV002: - return .default - case .SRV003: - return .default - case .SRV004: - return .default - case .SRV005: - return .default - case .SRV006: - return .init(englishDescription: "Time limit exceeded", danishDescription: "Tidsgrænse er overskredet.") - case .SRV007: - return .init( - englishDescription: "Please update to the most recent version of the application", - danishDescription: "Opdater venligst til den nyeste version af applikationen." - ) - case .SRV008: - return .default - case .SRV010: - return .default - case .SRV011: - return .default - case .SRV012: - return .init(englishDescription: "IP address changed in flow", danishDescription: "IP adresse ændredes under transkationen.") - case .OCES001: - return .init(englishDescription: "You only have NemID for online banking.", danishDescription: "Du har kun NemID til netbank.") - case .OCES002: - return .init( - englishDescription: "If you wish to use NemID for other services than online banking, you have to affiliate a public digital signature to your NemID.", - danishDescription: "Ønsker du at bruge NemID til andet end netbank, skal du først tilknytte en offentlig digital signatur." - ) - case .OCES003: - return .init( - englishDescription: "You have attempted to log on using a NemID with no public digital signature", - danishDescription: "Der er ikke tilknyttet en offentlig digital signatur til det NemID du har forsøgt at logge på med." - ) - case .OCES004: - return .init( - englishDescription: "You can only use this NemID for your online banking service.", - danishDescription: "Du kan kun bruge dette NemID til netbank." - ) - case .OCES005: - return .init( - englishDescription: "Issuing your public digital signature failed.", - danishDescription: "Udstedelsen af din offentlige digitale signatur mislykkedes." - ) - case .OCES006: - return .init( - englishDescription: "You currently don’t have an active public digital signature (OCES certificate)affiliated with your NemID.", - danishDescription: "Du har ikke en aktiv offentlig digital signatur tilknyttet NemID i øjeblikket." - ) - } - } -} - -// MARK: - Codable -extension NemIDError: Codable {} - -// MARK: - CustomStringConvertible -extension NemIDError: CustomStringConvertible { - public var description: String { - "NemIDError.\(self.rawValue): \(self.englishDescrption)" - } -} - -// MARK: - LocalizedError -extension NemIDError: LocalizedError { - public var errorDescription: String? { - self.englishDescrption - } +public enum NemIDError: Error { + case failedToLoadCertificate + case failedToLoadPrivateKey } diff --git a/Sources/NemID/NemIDLoginService.swift b/Sources/NemID/NemIDLoginService.swift index 2f29d5c..fe53134 100644 --- a/Sources/NemID/NemIDLoginService.swift +++ b/Sources/NemID/NemIDLoginService.swift @@ -32,7 +32,6 @@ public struct LiveNemIDLoginService: NemIDLoginService { public func validateAndExtractUser(fromResponse response: Data) -> EventLoopFuture { let responseHandler = NemIDResponseHandler( xmlParser: libxml2XMLDSigParser(), - certificateExtractor: DefaultCertificateExtractor(), ocspClient: HTTPOCSPClient(client: httpClient, eventLoop: eventLoop, logger: logger), eventLoop: self.eventLoop ) diff --git a/Sources/NemID/NemIDResponseError.swift b/Sources/NemID/NemIDResponseError.swift new file mode 100644 index 0000000..a0ec2ed --- /dev/null +++ b/Sources/NemID/NemIDResponseError.swift @@ -0,0 +1,280 @@ +import Foundation + +/// See a full list at [NemID Errors](https://www.nets.eu/dk-da/kundeservice/nemid-tjenesteudbyder/NemID-tjenesteudbyderpakken/Documents/NemID%20Error%20Codes.pdf) +public enum NemIDResponseError: String, Error { + case APP001 + case APP002 + case APP003 + case APP004 + case APP007 + case APP008 + case APP009 + case APP010 + + case AUTH001 + case AUTH004 + case AUTH005 + case AUTH006 + case AUTH007 + case AUTH008 + case AUTH009 + case AUTH010 + case AUTH011 + case AUTH012 + case AUTH013 + case AUTH017 + case AUTH018 + case AUTH019 + case AUTH020 + case AUTH021 + + case CAN001 + case CAN002 + case CAN003 + case CAN004 + case CAN005 + case CAN007 + case CAN008 + + case LOCK001 + case LOCK002 + case LOCK003 + + case SRV001 + case SRV002 + case SRV003 + case SRV004 + case SRV005 + case SRV006 + case SRV007 + case SRV008 + case SRV010 + case SRV011 + case SRV012 + + case OCES001 + case OCES002 + case OCES003 + case OCES004 + case OCES005 + case OCES006 + + /// Returns a text suitable for showing the user in English. + public var englishDescrption: String { self.metadata.englishDescription } + + /// Returns a text suitable for showing the user in Danish. + public var danishDescription: String { self.metadata.danishDescription } + + // MARK: Private + private struct Metadata { + let englishDescription: String + let danishDescription: String + + static var `default`: Self = .init( + englishDescription: "A technical error has occured. Contact the service provider if the problem persists.", + danishDescription: "Der er opstået en teknisk fejl. Kontakt tjenesteudbyder hvis problemet forsætter." + ) + } + + private var metadata: Metadata { + switch self { + case .APP001: + return .default + case .APP002: + return .default + case .APP003: + return .default + case .APP004: + return .default + case .APP007: + return .default + case .APP008: + return .default + case .APP009: + return .default + case .APP010: + return .default + case .AUTH001: + return .init( + englishDescription: "Your NemID is blocked. Please contact NemID support.", + danishDescription: "Dit NemID er spærret. Kontakt NemID support." + ) + case .AUTH004: + return .init( + englishDescription: "Your NemID is temporarily locked and you cannot log on until the 8 hour time lock has been lifted.", + danishDescription: "Dit NemID er midlertidigt låst i 8 timer og du kan ikke logge på før spærringen er ophævet." + ) + case .AUTH005: + return .init( + englishDescription: "Your NemID is blocked. Please contact NemID support.", + danishDescription: "Dit NemID er spærret. Kontakt NemID support." + ) + case .AUTH006: + return .init( + englishDescription: "You have used all the codes on your code card.", + danishDescription: "Du har brugt alle nøgler på nøglekortet." + ) + case .AUTH007: + return .init( + englishDescription: "Your NemID password is blocked due to too many failed password attempts.", + danishDescription: "Din NemID-adgangskode er spærret på grund af for mange fejlede forsøg." + ) + case .AUTH008: + return .init( + englishDescription: "Your NemID is not active and you need support to issue a new activation password to activate.", + danishDescription: "Dit NemID er ikke aktivt og du skal bestille en ny midlertidig adgangskode til aktivering hos support." + ) + case .AUTH009: + return .default + case .AUTH010: + return .default + case .AUTH011: + return .init( + englishDescription: "NemID login on mobile does not support authentication using a temporary password.", + danishDescription: "NemID på mobil understøtter ikke brug af midlertidig adgangskode." + ) + case .AUTH012: + return .default + case .AUTH013: + return .default + case .AUTH017: + return .init( + englishDescription: "Something in the browser environment has caused NemID to stop working. This could be because of an incompatible plug- in, too restrictive privacy settings or other environment factors.", + danishDescription: "En teknisk fejl i browseren gør at NemID ikke kan starte." + ) + case .AUTH018: + return .init( + englishDescription: "Your code app is revoked. To use it again please reactivate it.", + danishDescription: "Din nøgleapp er spærret. For at bruge den igen skal den genaktiveres." + ) + case .AUTH019: + return .init( + englishDescription: "It is not possible to login with a code card, please use a code app or code token.", + danishDescription: "Det er ikke muligt at logge ind med nøglekort, brug anden løsning nøgleapp eller nøgleviser." + ) + case .AUTH020: + return .init( + englishDescription: "Unable to login with 1-factor, please try with 2-factor login.", + danishDescription: "Kunne ikke logge ind med 1- faktor, prøv med 2-faktor login." + ) + case .AUTH021: + return .init( + englishDescription: "This NemID is no longer valid due to insufficient identification of the user", + danishDescription: "Det er ikke længere muligt at logge ind med dette NemID pga. manglende opdatering af identitetsoplysninger." + ) + case .CAN001: + return .init( + englishDescription: "You have cancelled the activation of NemID after submitting the activation password.", + danishDescription: "Du har afbrudt aktiveringen efter du har brugt den midlertidige adgangskode." + ) + case .CAN002: + return .init(englishDescription: "You have canelled the login.", danishDescription: "Du har afbrudt login.") + case .CAN003: + return .init( + englishDescription: "The connection to the application has timed out or has been interrupted by another app", + danishDescription: "Forbindelsen til applikationen er timet ud eller er blevet afbrudt af en anden app." + ) + case .CAN004: + return .init(englishDescription: "The session is cancelled", danishDescription: "Session er afbrudt") + case .CAN005: + return .init( + englishDescription: "You took too long to authenticate the request you had sent to your code app.", + danishDescription: "Det tog for lang tid, før du godkendte den anmodning, du havde sendt til din nøgleapp" + ) + case .CAN007: + return .init( + englishDescription: "You rejected your code app authentication request. If this was incorrect, you can submit a new request after clicking “OK” to finish.", + danishDescription: "Du har afvist din anmodning om godkendelse i din nøgleapp. Hvis det var en fejl, kan du sende en ny anmodning, når du har afsluttet ved at klikke på ”Ok”." + ) + case .CAN008: + return .init( + englishDescription: "You sent a new authentication request to your code app overwriting an existing one.", + danishDescription: "Du har sendt en ny anmodning til godkendelse i din nøgleapp, som overskriver en eksisterende." + ) + case .LOCK001: + return .init( + englishDescription: "You have used the wrong user ID or password too many times. Your NemID is now blocked for 8 hours after which you can try again.", + danishDescription: "Du har angivet forkert bruger- id eller adgangskode for mange gange. NemID er nu spærret i 8 timer, hvorefter du kan forsøge igen" + ) + case .LOCK002: + return .init( + englishDescription: "You have used a wrong password too many times. Your NemID is blocked and cannot be used.", + danishDescription: "Du har angivet en forkert adgangskode for mange gange. Dit NemID er spærret." + ) + case .LOCK003: + return .init( + englishDescription: "You have entered a wrong NemID key too many times. Your NemID is blocked and cannot be used.", + danishDescription: "Du har angivet forkert NemID nøgle for mange gange. Dit NemID er spærret." + ) + case .SRV001: + return .default + case .SRV002: + return .default + case .SRV003: + return .default + case .SRV004: + return .default + case .SRV005: + return .default + case .SRV006: + return .init(englishDescription: "Time limit exceeded", danishDescription: "Tidsgrænse er overskredet.") + case .SRV007: + return .init( + englishDescription: "Please update to the most recent version of the application", + danishDescription: "Opdater venligst til den nyeste version af applikationen." + ) + case .SRV008: + return .default + case .SRV010: + return .default + case .SRV011: + return .default + case .SRV012: + return .init(englishDescription: "IP address changed in flow", danishDescription: "IP adresse ændredes under transkationen.") + case .OCES001: + return .init(englishDescription: "You only have NemID for online banking.", danishDescription: "Du har kun NemID til netbank.") + case .OCES002: + return .init( + englishDescription: "If you wish to use NemID for other services than online banking, you have to affiliate a public digital signature to your NemID.", + danishDescription: "Ønsker du at bruge NemID til andet end netbank, skal du først tilknytte en offentlig digital signatur." + ) + case .OCES003: + return .init( + englishDescription: "You have attempted to log on using a NemID with no public digital signature", + danishDescription: "Der er ikke tilknyttet en offentlig digital signatur til det NemID du har forsøgt at logge på med." + ) + case .OCES004: + return .init( + englishDescription: "You can only use this NemID for your online banking service.", + danishDescription: "Du kan kun bruge dette NemID til netbank." + ) + case .OCES005: + return .init( + englishDescription: "Issuing your public digital signature failed.", + danishDescription: "Udstedelsen af din offentlige digitale signatur mislykkedes." + ) + case .OCES006: + return .init( + englishDescription: "You currently don’t have an active public digital signature (OCES certificate)affiliated with your NemID.", + danishDescription: "Du har ikke en aktiv offentlig digital signatur tilknyttet NemID i øjeblikket." + ) + } + } +} + +// MARK: - Codable +extension NemIDResponseError: Codable {} + +// MARK: - CustomStringConvertible +extension NemIDResponseError: CustomStringConvertible { + public var description: String { + "NemIDResponseError.\(self.rawValue)" + } +} + +// MARK: - LocalizedError +extension NemIDResponseError: LocalizedError { + public var errorDescription: String? { + self.englishDescrption + } +} diff --git a/Sources/NemID/NemIDUser.swift b/Sources/NemID/NemIDUser.swift index dc6a670..fd106f0 100644 --- a/Sources/NemID/NemIDUser.swift +++ b/Sources/NemID/NemIDUser.swift @@ -1,12 +1,12 @@ import Foundation +enum NemIDUserError: Error { + case failedToExtractCommonName + case failedToExtractSerialNumber +} + /// A model that represents a private NemID user (POCES certificate) with their PID and their name. public struct NemIDUser { - enum InitializationError: Error { - case failedToExtractCommonNameFromCertificate - case failedToExtractPIDFromCertificate - } - /// The PID representing this user. This value can be used to verify a given CPR matches with this user. public let pid: String @@ -17,12 +17,11 @@ public struct NemIDUser { init(from certificate: NemIDX509Certificate) throws { guard let commonName = certificate.subjectCommonName else { - throw InitializationError.failedToExtractCommonNameFromCertificate + throw NemIDUserError.failedToExtractCommonName } guard let pid = certificate.subjectSerialNumber?.components(separatedBy: "PID:").last else { - throw InitializationError.failedToExtractPIDFromCertificate + throw NemIDUserError.failedToExtractSerialNumber } - self.init(pid: pid, name: commonName != "Pseudonym" ? commonName : nil) } diff --git a/Sources/NemID/RSA/NemIDRSAKey.swift b/Sources/NemID/RSA/NemIDRSAKey.swift index fec292f..038352b 100644 --- a/Sources/NemID/RSA/NemIDRSAKey.swift +++ b/Sources/NemID/RSA/NemIDRSAKey.swift @@ -9,9 +9,9 @@ public final class NemIDRSAKey: BIOLoadable { public static func `private`(pem data: Data) throws -> NemIDRSAKey where Data: DataProtocol { - let privateKey = try self.load(pem: data, { bio -> UnsafeMutablePointer in + guard let privateKey = self.load(pem: data, { bio -> UnsafeMutablePointer in CNemIDBoringSSL_PEM_read_bio_RSAPrivateKey(bio, nil, nil, nil) - }) + }) else { throw NemIDError.failedToLoadPrivateKey } return self.init(privateKey) } diff --git a/Sources/NemID/Response Handler/CertificateChainValidationError.swift b/Sources/NemID/Response Handler/CertificateChainValidationError.swift new file mode 100644 index 0000000..cff8b16 --- /dev/null +++ b/Sources/NemID/Response Handler/CertificateChainValidationError.swift @@ -0,0 +1,10 @@ +import Foundation + +enum CertificateChainValidationError: Error { + case certificateWasNotSignedByCorrectCertificate + case failedToExtractCertificateDates + case certificateIsOutsideValidTime + case issuerDidNotHaveCAFlag + case leafDidNotHaveDigitalSignatureKeyUsage + case issuerDidNotHaveKeyCertSignKeyUsage +} diff --git a/Sources/NemID/Response Handler/NemIDResponseHandler.swift b/Sources/NemID/Response Handler/NemIDResponseHandler.swift index a061f57..67cf40e 100644 --- a/Sources/NemID/Response Handler/NemIDResponseHandler.swift +++ b/Sources/NemID/Response Handler/NemIDResponseHandler.swift @@ -9,13 +9,11 @@ import NIO */ struct NemIDResponseHandler { private let xmlParser: XMLDSigParser - private let certificateExtractor: CertificateExtrator private let ocspClient: OCSPClient private let eventLoop: EventLoop - init(xmlParser: XMLDSigParser, certificateExtractor: CertificateExtrator, ocspClient: OCSPClient, eventLoop: EventLoop) { + init(xmlParser: XMLDSigParser, ocspClient: OCSPClient, eventLoop: EventLoop) { self.xmlParser = xmlParser - self.certificateExtractor = certificateExtractor self.ocspClient = ocspClient self.eventLoop = eventLoop } @@ -41,7 +39,7 @@ struct NemIDResponseHandler { // Check if the response is an error. if let responseString = String(data: base64DecodedData, encoding: .utf8), - let clientError = NemIDError(rawValue: responseString) + let clientError = NemIDResponseError(rawValue: responseString) { throw clientError } @@ -50,7 +48,7 @@ struct NemIDResponseHandler { let parsedResponse = try xmlParser.parse([UInt8](base64DecodedData)) // Extract certificate chain. - let certificates = try certificateExtractor.extract(from: parsedResponse) + let certificates = try parsedResponse.verifiedCertificateChain() // Validate XML signature with leaf certificate try validateXMLSignature(parsedResponse, wasSignedBy: certificates.leaf) @@ -74,91 +72,94 @@ struct NemIDResponseHandler { private func validateOCSPResponse(_ response: OCSPResponse, chain: CertificateChain) throws { // Check response status guard response.responseStatus == .successful else { - throw NemIDResponseHandlerError.ocspRequestWasNotSuccessful + throw OCSPValidationError.requestWasNotSuccessful } guard let basicResponse = response.basicOCSPResponse else { - throw NemIDResponseHandlerError.ocspBasicResponseIsNotPresent + throw OCSPValidationError.basicResponseIsNotPresent } // Validate that signature is tbsResponseData signed by accompanying certificate guard let ocspCertificate = basicResponse.certs.first else { - throw NemIDResponseHandlerError.ocspCertificateNotFoundInResponse + throw OCSPValidationError.certificateNotFoundInResponse } #warning("der bytes has correct length, so I think it's the signer failing?s") let signer = RSASigner(key: try ocspCertificate.publicKey(), hashAlgorithm: basicResponse.signatureAlgorithm.hashAlgorithm) // guard try signer.verify(basicResponse.signature, signs: basicResponse.tbsResponseData.derBytes) else { -// throw NemIDResponseHandlerError.ocspSignatureWasNotSignedByCertificate +// throw OCSPValidationError.signatureWasNotSignedByCertificate // } // Validate that accompanying certificate was signed by issuer. guard try ocspCertificate.isSignedBy(by: chain.intermediate) else { - throw NemIDResponseHandlerError.ocspCertificateWasNotSignedByIssuer + throw OCSPValidationError.certificateWasNotSignedByIssuer } // Validate certificate recovation status guard let certResponse = basicResponse.tbsResponseData.responses.first else { - throw NemIDResponseHandlerError.ocspCertificateResponseNotPresent + throw OCSPValidationError.certificateResponseNotPresent } guard certResponse.certStatus == .good else { - throw NemIDResponseHandlerError.ocspCertificateStatusIsNotGood + throw OCSPValidationError.certificateStatusIsNotGood } // Check hash algorithm guard certResponse.certID.hashAlgorithm == .sha256 else { - throw NemIDResponseHandlerError.ocspCertificateWrongHashAlgorithm + throw OCSPValidationError.certificateWrongHashAlgorithm } - #warning("todo: remove fatalerror") // Check hash name, key hash and serial number are the ones we sent in the request. try chain.leaf.withSerialNumber { serialNumber in var output = [UInt8](repeating: 0, count: numericCast(CNemIDBoringSSL_BN_num_bytes(serialNumber))) let leafSerialNumberSize = CNemIDBoringSSL_BN_bn2bin(serialNumber, &output) let leafSerialNumberBytes = [UInt8](output[0..= Date() && certResponse.thisUpdate <= Date() else { - throw NemIDResponseHandlerError.ocspResponseIsOutsideAllowedTime + throw OCSPValidationError.responseIsOutsideAllowedTime } // Check OCSP signing key usage guard ocspCertificate.hasExtendedKeyUsage(.ocspSigning) else { - throw NemIDResponseHandlerError.ocspCertificateDidNotHaveOCSPSigningExtendedKeyUsage + throw OCSPValidationError.certificateDidNotHaveOCSPSigningExtendedKeyUsage } - // Check OCSP extension + // Check OCSP NoCheck extension is present (should be null) guard ocspCertificate.hasOCSPNoCheckExtension() else { - throw NemIDResponseHandlerError.ocspCertificateHasNoCheckExtension + throw OCSPValidationError.certificateNoCheckExtensionNotFound } } private func validateCertificateChain(_ chain: CertificateChain) throws { // Verify that leaf certificate has digitalSignature key usage guard chain.leaf.hasKeyUsage(.digitalSignature) else { - throw NemIDResponseHandlerError.leafDidNotHaveDigitalSignatureKeyUsage + throw CertificateChainValidationError.leafDidNotHaveDigitalSignatureKeyUsage } + // Verify dates for certificate in chain { - // Verify dates guard Date() < certificate.notAfter() && Date() > certificate.notBefore() else { - throw NemIDResponseHandlerError.certificateIsOutsideValidTime + throw CertificateChainValidationError.certificateIsOutsideValidTime } } // Verify that intermediate and root has cA constraint guard chain.root.hasCAFlag() && chain.intermediate.hasCAFlag() else { - throw NemIDResponseHandlerError.issuerDidNotHaveCAFlag + throw CertificateChainValidationError.issuerDidNotHaveCAFlag } // Verify that intermediate and root has keyCertSign usage guard chain.intermediate.hasKeyUsage(.keyCertSign) && chain.root.hasKeyUsage(.keyCertSign) else { - throw NemIDResponseHandlerError.issuerDidNotHaveKeyCertSignKeyUsage + throw CertificateChainValidationError.issuerDidNotHaveKeyCertSignKeyUsage } // Verify the actual chain signing. @@ -166,7 +167,7 @@ struct NemIDResponseHandler { try chain.intermediate.isSignedBy(by: chain.root), try chain.root.isSignedBy(by: chain.root) else { - throw NemIDResponseHandlerError.certificateWasNotSignedByCorrectCertificate + throw CertificateChainValidationError.certificateWasNotSignedByCorrectCertificate } // Verify that root certificate is a trusted OCES certificate. @@ -176,27 +177,27 @@ struct NemIDResponseHandler { /// Verifies the signed element in the xml response private func validateXMLSignature(_ response: ParsedXMLDSigResponse, wasSignedBy certificate: NemIDX509Certificate) throws { guard let signedInfoC14N = response.signedInfo.C14N() else { - throw NemIDResponseHandlerError.failedToExtractSignedInfo + throw XMLValidationError.failedToExtractSignedInfo } guard let referenceDigestBase64Decoded = Data(base64Encoded: response.referenceDigestValue) else { - throw NemIDResponseHandlerError.failedToExtractReferenceDigest + throw XMLValidationError.failedToExtractReferenceDigest } guard let objectToBeSignedC14N = response.objectToBeSigned.C14N() else { - throw NemIDResponseHandlerError.failedToExtractObjectToBeSigned + throw XMLValidationError.failedToExtractObjectToBeSigned } guard let signatureValueBase64Decoded = Data(base64Encoded: response.signatureValue, options: .ignoreUnknownCharacters) else { - throw NemIDResponseHandlerError.failedToExtractSignatureValue + throw XMLValidationError.failedToExtractSignatureValue } // Verify reference object digest was made from ToBeSigned object. guard SHA256.hash(data: objectToBeSignedC14N) == referenceDigestBase64Decoded else { - throw NemIDResponseHandlerError.digestDidNotMatchSignedObject + throw XMLValidationError.digestDidNotMatchSignedObject } // Verify that signedInfo was signed with certificate let signer = RSASigner(key: try certificate.publicKey()) guard try signer.verify([UInt8](signatureValueBase64Decoded), signs: signedInfoC14N) else { - throw NemIDResponseHandlerError.signedInfoWasNotSignedByCertificate + throw XMLValidationError.signedInfoWasNotSignedByCertificate } } } diff --git a/Sources/NemID/Response Handler/NemIDResponseHandlerError.swift b/Sources/NemID/Response Handler/NemIDResponseHandlerError.swift index 2c0cd36..84266ee 100644 --- a/Sources/NemID/Response Handler/NemIDResponseHandlerError.swift +++ b/Sources/NemID/Response Handler/NemIDResponseHandlerError.swift @@ -2,27 +2,4 @@ import Foundation enum NemIDResponseHandlerError: Error { case failedToDecodeResponseAsBase64 - case failedToExtractSignedInfo - case failedToExtractReferenceDigest - case failedToExtractObjectToBeSigned - case failedToExtractSignatureValue - case digestDidNotMatchSignedObject - case signedInfoWasNotSignedByCertificate - case certificateWasNotSignedByCorrectCertificate - case failedToExtractCertificateDates - case certificateIsOutsideValidTime - case issuerDidNotHaveCAFlag - case leafDidNotHaveDigitalSignatureKeyUsage - case issuerDidNotHaveKeyCertSignKeyUsage - case ocspRequestWasNotSuccessful - case ocspBasicResponseIsNotPresent - case ocspCertificateNotFoundInResponse - case ocspSignatureWasNotSignedByCertificate - case ocspCertificateWasNotSignedByIssuer - case ocspCertificateResponseNotPresent - case ocspCertificateStatusIsNotGood - case ocspCertificateWrongHashAlgorithm - case ocspResponseIsOutsideAllowedTime - case ocspCertificateDidNotHaveOCSPSigningExtendedKeyUsage - case ocspCertificateHasNoCheckExtension } diff --git a/Sources/NemID/Response Handler/OCSPValidationError.swift b/Sources/NemID/Response Handler/OCSPValidationError.swift new file mode 100644 index 0000000..da7f479 --- /dev/null +++ b/Sources/NemID/Response Handler/OCSPValidationError.swift @@ -0,0 +1,18 @@ +import Foundation + +enum OCSPValidationError: Error { + case requestWasNotSuccessful + case basicResponseIsNotPresent + case certificateNotFoundInResponse + case signatureWasNotSignedByCertificate + case certificateWasNotSignedByIssuer + case certificateResponseNotPresent + case certificateStatusIsNotGood + case certificateWrongHashAlgorithm + case responseIsOutsideAllowedTime + case certificateDidNotHaveOCSPSigningExtendedKeyUsage + case certificateNoCheckExtensionNotFound + case serialNumberDidNotMatchRequest + case issuerKeyHashDidNotMatchRequest + case issuerNameHashDidNotMatchRequest +} diff --git a/Sources/NemID/Response Handler/XMLValidationError.swift b/Sources/NemID/Response Handler/XMLValidationError.swift new file mode 100644 index 0000000..440513f --- /dev/null +++ b/Sources/NemID/Response Handler/XMLValidationError.swift @@ -0,0 +1,10 @@ +import Foundation + +enum XMLValidationError: Error { + case failedToExtractSignedInfo + case failedToExtractReferenceDigest + case failedToExtractObjectToBeSigned + case failedToExtractSignatureValue + case digestDidNotMatchSignedObject + case signedInfoWasNotSignedByCertificate +} diff --git a/Sources/NemID/Utilities/BIOLoadable.swift b/Sources/NemID/Utilities/BIOLoadable.swift index 4a51e28..0e3a883 100644 --- a/Sources/NemID/Utilities/BIOLoadable.swift +++ b/Sources/NemID/Utilities/BIOLoadable.swift @@ -3,23 +3,17 @@ import Foundation protocol BIOLoadable { } -enum BIOLoadableError: Error { - case failedToLoadBIO -} - extension BIOLoadable { - static func load(pem data: Data, _ closure: (UnsafeMutablePointer) -> T?) throws -> T + static func load(pem data: Data, _ closure: (UnsafeMutablePointer) throws -> T?) rethrows -> T? where Data: DataProtocol { precondition(data.regions.count <= 1, "There is no such thing as data that has discontiguous regions") - guard let region = data.regions.first else { throw BIOLoadableError.failedToLoadBIO } + guard let region = data.regions.first else { return nil } return try region.withUnsafeBytes { ptr in - let bio = CNemIDBoringSSL_BIO_new_mem_buf(ptr.baseAddress, numericCast(ptr.count)) - guard let bioPtr = bio else { throw BIOLoadableError.failedToLoadBIO } + let bio = CNemIDBoringSSL_BIO_new_mem_buf(ptr.baseAddress, numericCast(ptr.count))! defer { CNemIDBoringSSL_BIO_free(bio) } - guard let result = closure(bioPtr) else { throw BIOLoadableError.failedToLoadBIO } - return result + return try closure(bio) } } } diff --git a/Sources/NemID/X509/NemIDX509Certificate.swift b/Sources/NemID/X509/NemIDX509Certificate.swift index b8fe37e..4dd4bee 100644 --- a/Sources/NemID/X509/NemIDX509Certificate.swift +++ b/Sources/NemID/X509/NemIDX509Certificate.swift @@ -2,12 +2,6 @@ import Foundation import Crypto @_implementationOnly import CNemIDBoringSSL -enum X509CertificateError: Error { - case failedToRetrievePublicKey - case failedToGetSerialNumber - case failedToRetrieveDERRepresentation -} - public final class NemIDX509Certificate: BIOLoadable { /// Initialize a new certificate from a DER string public convenience init(der string: String) throws { @@ -16,9 +10,10 @@ public final class NemIDX509Certificate: BIOLoadable { /// Initialize a new certificate from DER-encoded data. public convenience init(der data: Data) throws where Data: DataProtocol { - let x509 = try Self.load(pem: data) { bioPtr in + guard let x509 = Self.load(pem: data, { bioPtr in return CNemIDBoringSSL_d2i_X509_bio(bioPtr, nil) - } + }) else { throw NemIDError.failedToLoadCertificate } + self.init(x509) } @@ -29,9 +24,10 @@ public final class NemIDX509Certificate: BIOLoadable { /// Initialize a new certificate from DER-encoded data. public convenience init(pem data: Data) throws where Data: DataProtocol { - let x509 = try Self.load(pem: data) { bioPtr in + guard let x509 = Self.load(pem: data, { bioPtr in return CNemIDBoringSSL_PEM_read_bio_X509(bioPtr, nil, nil, nil) - } + }) else { throw NemIDError.failedToLoadCertificate } + self.init(x509) } @@ -39,7 +35,7 @@ public final class NemIDX509Certificate: BIOLoadable { func publicKey() throws -> NemIDRSAKey { try withPublicKey { key in guard let rsaKey = CNemIDBoringSSL_EVP_PKEY_get1_RSA(key) else { - throw X509CertificateError.failedToRetrievePublicKey + throw NemIDX509CertificateError.failedToRetrievePublicKey } return NemIDRSAKey(rsaKey) } @@ -89,7 +85,7 @@ public final class NemIDX509Certificate: BIOLoadable { /// Returns a pointer to the public key, which is only valid for the lifetime of the closure func withPublicKey(_ handler: (UnsafeMutablePointer?) throws -> T) throws -> T { - guard let pubKey = CNemIDBoringSSL_X509_get_pubkey(self.ref) else { throw X509CertificateError.failedToRetrievePublicKey } + guard let pubKey = CNemIDBoringSSL_X509_get_pubkey(self.ref) else { throw NemIDX509CertificateError.failedToRetrievePublicKey } defer { CNemIDBoringSSL_EVP_PKEY_free(pubKey) } return try handler(pubKey) } @@ -132,7 +128,7 @@ public final class NemIDX509Certificate: BIOLoadable { func withSerialNumber(_ closure: (UnsafeMutablePointer) throws -> Void) throws { let serialNumberASN1 = CNemIDBoringSSL_X509_get_serialNumber(self.ref)! guard let bn = CNemIDBoringSSL_ASN1_INTEGER_to_BN(serialNumberASN1, nil) else { - throw X509CertificateError.failedToGetSerialNumber + throw NemIDX509CertificateError.failedToGetSerialNumber } defer { CNemIDBoringSSL_BN_free(bn) } try closure(bn) @@ -198,7 +194,7 @@ public final class NemIDX509Certificate: BIOLoadable { defer { CNemIDBoringSSL_BIO_free(bio) } guard CNemIDBoringSSL_i2d_X509_bio(bio, self.ref) == 1 else { - throw X509CertificateError.failedToRetrieveDERRepresentation + throw NemIDX509CertificateError.failedToRetrieveDERRepresentation } var dataPtr: UnsafeMutablePointer? = nil diff --git a/Sources/NemID/X509/NemIDX509CertificateError.swift b/Sources/NemID/X509/NemIDX509CertificateError.swift new file mode 100644 index 0000000..c5ef0ec --- /dev/null +++ b/Sources/NemID/X509/NemIDX509CertificateError.swift @@ -0,0 +1,7 @@ +import Foundation + +enum NemIDX509CertificateError: Error { + case failedToRetrievePublicKey + case failedToGetSerialNumber + case failedToRetrieveDERRepresentation +} diff --git a/Sources/NemID/XMLDSig/Certificates Extractor/CertificateExtractorError.swift b/Sources/NemID/XMLDSig/CertificateChainError.swift similarity index 89% rename from Sources/NemID/XMLDSig/Certificates Extractor/CertificateExtractorError.swift rename to Sources/NemID/XMLDSig/CertificateChainError.swift index 8a70093..eddada4 100644 --- a/Sources/NemID/XMLDSig/Certificates Extractor/CertificateExtractorError.swift +++ b/Sources/NemID/XMLDSig/CertificateChainError.swift @@ -1,6 +1,6 @@ import Foundation -enum CertificateExtractorError: Error { +enum CertificateChainError: Error { case failedToDecodeCertificate case unexpectedCertificateCount(Int) case failedToLocateLeafCertificate diff --git a/Sources/NemID/XMLDSig/Certificates Extractor/CertificateExtractor.swift b/Sources/NemID/XMLDSig/Certificates Extractor/CertificateExtractor.swift deleted file mode 100644 index 45daedb..0000000 --- a/Sources/NemID/XMLDSig/Certificates Extractor/CertificateExtractor.swift +++ /dev/null @@ -1,5 +0,0 @@ -import Foundation - -protocol CertificateExtrator { - func extract(from xml: ParsedXMLDSigResponse) throws -> CertificateChain -} diff --git a/Sources/NemID/XMLDSig/Certificates Extractor/DefaultCertificateExtractor.swift b/Sources/NemID/XMLDSig/Certificates Extractor/DefaultCertificateExtractor.swift deleted file mode 100644 index 763bb02..0000000 --- a/Sources/NemID/XMLDSig/Certificates Extractor/DefaultCertificateExtractor.swift +++ /dev/null @@ -1,50 +0,0 @@ -import Foundation - -struct DefaultCertificateExtractor: CertificateExtrator { - func extract(from xml: ParsedXMLDSigResponse) throws -> CertificateChain { - let certificates = try xml.x509Certificates - .map { base64DerCertificate -> NemIDX509Certificate in - guard let decoded = Data(base64Encoded: base64DerCertificate, options: .ignoreUnknownCharacters) else { - throw CertificateExtractorError.failedToDecodeCertificate - } - return try NemIDX509Certificate(der: decoded) - } - - guard certificates.count == 3 else { throw CertificateExtractorError.unexpectedCertificateCount(certificates.count) } - - // Count how many times each certificate is used as subject or issuer. - let certificatesUsageCount = certificates.reduce(into: [[UInt8]: Int]()) { res, certificate in - guard let subject = certificate.subject, - let issuer = certificate.issuer - else { return } - res[issuer] = (res[issuer] ?? 0) + 1 - res[subject] = (res[subject] ?? 0) + 1 - } - - // Leaf certificate should only have one usage (as subject). - guard let leafCertificateName = certificatesUsageCount.first(where: { $0.value == 1 })?.key, - let leafCertificate = certificates.first(where: { $0.subject == leafCertificateName }) - else { throw CertificateExtractorError.failedToLocateLeafCertificate } - - // Intermediate should be used twice (subject and issuer of leaf) - guard let intermediateCertificateName = certificatesUsageCount.first(where: { $0.value == 2 })?.key, - let intermediateCertificate = certificates.first(where: { $0.subject == intermediateCertificateName }) - else { throw CertificateExtractorError.failedToLocateIntermediateCertificate } - - // Intermediate should be used three time (subject, issuer of intermediate and self) - guard let rootCertificateName = certificatesUsageCount.first(where: { $0.value == 3 })?.key, - let rootCertificate = certificates.first(where: { $0.subject == rootCertificateName }) - else { throw CertificateExtractorError.failedToLocateRootCertificate } - - // Verify issuers (chain) is correct - guard leafCertificate.issuer == intermediateCertificate.subject else { throw CertificateExtractorError.leafIssuerWasNotIntermediate } - guard intermediateCertificate.issuer == rootCertificate.subject else { throw CertificateExtractorError.intermediateIssuerWasNotRoot } - guard rootCertificate.issuer == rootCertificate.subject else { throw CertificateExtractorError.rootWasNotSelfSigned } - - return CertificateChain( - root: rootCertificate, - intermediate: intermediateCertificate, - leaf: leafCertificate) - } -} - diff --git a/Sources/NemID/XMLDSig/ParsedXMLDSigResponse.swift b/Sources/NemID/XMLDSig/ParsedXMLDSigResponse.swift index e1631c6..b71f715 100644 --- a/Sources/NemID/XMLDSig/ParsedXMLDSigResponse.swift +++ b/Sources/NemID/XMLDSig/ParsedXMLDSigResponse.swift @@ -11,4 +11,53 @@ struct ParsedXMLDSigResponse { let objectToBeSigned: Data /// Returns an array of X509 certificates let x509Certificates: [String] + + func verifiedCertificateChain() throws -> CertificateChain { + let certificates = try x509Certificates + .map { base64DerCertificate -> NemIDX509Certificate in + guard let decoded = Data(base64Encoded: base64DerCertificate, options: .ignoreUnknownCharacters) else { + throw CertificateChainError.failedToDecodeCertificate + } + return try NemIDX509Certificate(der: decoded) + } + + guard certificates.count == 3 else { + throw CertificateChainError.unexpectedCertificateCount(certificates.count) + } + + // Count how many times each certificate is used as subject or issuer. + let certificatesUsageCount = certificates.reduce(into: [[UInt8]: Int]()) { res, certificate in + guard let subject = certificate.subject, + let issuer = certificate.issuer + else { return } + res[issuer] = (res[issuer] ?? 0) + 1 + res[subject] = (res[subject] ?? 0) + 1 + } + + // Leaf certificate should only have one usage (as subject). + guard let leafCertificateName = certificatesUsageCount.first(where: { $0.value == 1 })?.key, + let leafCertificate = certificates.first(where: { $0.subject == leafCertificateName }) + else { throw CertificateChainError.failedToLocateLeafCertificate } + + // Intermediate should be used twice (subject and issuer of leaf) + guard let intermediateCertificateName = certificatesUsageCount.first(where: { $0.value == 2 })?.key, + let intermediateCertificate = certificates.first(where: { $0.subject == intermediateCertificateName }) + else { throw CertificateChainError.failedToLocateIntermediateCertificate } + + // Intermediate should be used three time (subject, issuer of intermediate and self) + guard let rootCertificateName = certificatesUsageCount.first(where: { $0.value == 3 })?.key, + let rootCertificate = certificates.first(where: { $0.subject == rootCertificateName }) + else { throw CertificateChainError.failedToLocateRootCertificate } + + // Verify issuers (chain) is correct + guard leafCertificate.issuer == intermediateCertificate.subject else { throw CertificateChainError.leafIssuerWasNotIntermediate } + guard intermediateCertificate.issuer == rootCertificate.subject else { throw CertificateChainError.intermediateIssuerWasNotRoot } + guard rootCertificate.issuer == rootCertificate.subject else { throw CertificateChainError.rootWasNotSelfSigned } + + return CertificateChain( + root: rootCertificate, + intermediate: intermediateCertificate, + leaf: leafCertificate + ) + } } diff --git a/Sources/NemID/XMLDSig/XMLDSigParser/libxml2DSigParser.swift b/Sources/NemID/XMLDSig/XMLDSigParser/libxml2DSigParser.swift index 685a820..61e2a9e 100644 --- a/Sources/NemID/XMLDSig/XMLDSigParser/libxml2DSigParser.swift +++ b/Sources/NemID/XMLDSig/XMLDSigParser/libxml2DSigParser.swift @@ -1,37 +1,37 @@ import Foundation import Clibxml2 +enum libxml2DSigParserError: Error { + case failedToAllocateXMLDoc + case failedToCreateXPathContext + case failedToRegisterNamespace + case failedToParseCertificates + case failedToParseSignedElement + case failedToParseDigestValue + case failedToParseSignatureValue + case failedToParseSignedInfo +} + struct libxml2XMLDSigParser: XMLDSigParser { - enum ParserError: Error { - case failedToAllocateXMLDoc - case failedToCreateXPathContext - case failedToRegisterNamespace - case failedToParseCertificates - case failedToParseSignedElement - case failedToParseDigestValue - case failedToParseSignatureValue - case failedToParseSignedInfo - } - func parse(_ xml: [UInt8]) throws -> ParsedXMLDSigResponse { guard let xmlDoc = xml.withUnsafeBytes({ bytes -> xmlDocPtr? in let buf = bytes.bindMemory(to: Int8.self) return xmlReadMemory(buf.baseAddress, numericCast(buf.count), "noname.xml", nil, 0) }) else { - throw ParserError.failedToAllocateXMLDoc + throw libxml2DSigParserError.failedToAllocateXMLDoc } defer { xmlFreeDoc(xmlDoc) } guard let context = xmlXPathNewContext(xmlDoc) else { - throw ParserError.failedToCreateXPathContext + throw libxml2DSigParserError.failedToCreateXPathContext } defer { xmlXPathFreeContext(context) } - guard xmlXPathRegisterNs(context, "openoces", "http://www.openoces.org/2006/07/signature#") == 0 else { - throw ParserError.failedToRegisterNamespace - } - guard xmlXPathRegisterNs(context, "ds", "http://www.w3.org/2000/09/xmldsig#") == 0 else { - throw ParserError.failedToRegisterNamespace + guard + xmlXPathRegisterNs(context, "openoces", "http://www.openoces.org/2006/07/signature#") == 0, + xmlXPathRegisterNs(context, "ds", "http://www.w3.org/2000/09/xmldsig#") == 0 + else { + throw libxml2DSigParserError.failedToRegisterNamespace } // Parse signed element @@ -40,7 +40,7 @@ struct libxml2XMLDSigParser: XMLDSigParser { context: context, in: xmlDoc ) else { - throw ParserError.failedToParseSignedElement + throw libxml2DSigParserError.failedToParseSignedElement } // Parse singedInfo @@ -49,7 +49,7 @@ struct libxml2XMLDSigParser: XMLDSigParser { context: context, in: xmlDoc ) else { - throw ParserError.failedToParseSignedInfo + throw libxml2DSigParserError.failedToParseSignedInfo } // Parse digestValue @@ -57,28 +57,28 @@ struct libxml2XMLDSigParser: XMLDSigParser { query: "/openoces:signature/ds:Signature/ds:SignedInfo/ds:Reference/ds:DigestValue", context: context ) else { - throw ParserError.failedToParseDigestValue + throw libxml2DSigParserError.failedToParseDigestValue } // Parse signatureValue guard let signatureValue = parseXPathValue(query: "/openoces:signature/ds:Signature/ds:SignatureValue", context: context) else { - throw ParserError.failedToParseSignatureValue + throw libxml2DSigParserError.failedToParseSignatureValue } // Parse certificates guard let certsXMLObject = xmlXPathEvalExpression("/openoces:signature/ds:Signature/ds:KeyInfo/ds:X509Data/ds:X509Certificate", context) else { - throw ParserError.failedToParseCertificates + throw libxml2DSigParserError.failedToParseCertificates } defer { xmlXPathFreeObject(certsXMLObject) } guard let nodeSetPtr = certsXMLObject.pointee.nodesetval else { - throw ParserError.failedToParseCertificates + throw libxml2DSigParserError.failedToParseCertificates } let certificates = try (0.. String in guard let nodePtr = nodeSetPtr.pointee.nodeTab[numericCast(index)], let content = xmlNodeGetContent(nodePtr) else { - throw ParserError.failedToParseCertificates + throw libxml2DSigParserError.failedToParseCertificates } defer { xmlFree(content) } return String(cString: content) diff --git a/Tests/NemIDTests/CertificateExtractorTests.swift b/Tests/NemIDTests/ParsedXMLDSigResponseTests.swift similarity index 85% rename from Tests/NemIDTests/CertificateExtractorTests.swift rename to Tests/NemIDTests/ParsedXMLDSigResponseTests.swift index 8d0a60f..72dac34 100644 --- a/Tests/NemIDTests/CertificateExtractorTests.swift +++ b/Tests/NemIDTests/ParsedXMLDSigResponseTests.swift @@ -1,9 +1,8 @@ import XCTest @testable import NemID -final class CertificateExtractorTests: XCTestCase { +final class ParsedXMLDSigResponseTests: XCTestCase { func test_extract_extractsChain() throws { - let sut = DefaultCertificateExtractor() let response = ParsedXMLDSigResponse( signatureValue: "", signedInfo: Data(), @@ -16,7 +15,7 @@ final class CertificateExtractorTests: XCTestCase { ] ) - let chain = try sut.extract(from: response) + let chain = try response.verifiedCertificateChain() try XCTAssertEqual(chain.root, NemIDX509Certificate(der: Data(base64Encoded: TestHelper.googleRoot, options: .ignoreUnknownCharacters)!)) try XCTAssertEqual(chain.intermediate, NemIDX509Certificate(der: Data(base64Encoded: TestHelper.googleIntermediate, options: .ignoreUnknownCharacters)!)) try XCTAssertEqual(chain.leaf, NemIDX509Certificate(der: Data(base64Encoded: TestHelper.googleLeaf, options: .ignoreUnknownCharacters)!)) diff --git a/Tests/NemIDTests/libxml2XMLDigParserTests.swift b/Tests/NemIDTests/libxml2XMLDigParserTests.swift index aa1be40..26771c0 100644 --- a/Tests/NemIDTests/libxml2XMLDigParserTests.swift +++ b/Tests/NemIDTests/libxml2XMLDigParserTests.swift @@ -45,8 +45,8 @@ final class libxml2XMLDigParserTests: XCTestCase { } fileprivate let exampleXMLStructure = """ - - + + digest-value @@ -66,8 +66,8 @@ fileprivate let exampleXMLStructure = """ """ fileprivate let exampleXMLResponse = """ - - + + From 878fea4aecfe09b596929baa5b5138d1a82bdbda Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Fri, 23 Apr 2021 10:25:50 +0200 Subject: [PATCH 17/22] Add OCES root certification validation --- Package.swift | 2 +- Sources/NemID/NemIDEnvironment.swift | 12 +++++++++--- Sources/NemID/NemIDLoginService.swift | 3 ++- Sources/NemID/OCSP/OCSPResponse.swift | 1 - .../CertificateChainValidationError.swift | 1 + .../Response Handler/NemIDResponseHandler.swift | 8 ++++++-- .../NemID/Utilities}/Bytes+Hex.swift | 8 ++++---- Sources/NemID/X509/NemIDX509Certificate.swift | 5 +++++ 8 files changed, 28 insertions(+), 12 deletions(-) rename {Tests/NemIDTests/Helpers => Sources/NemID/Utilities}/Bytes+Hex.swift (86%) diff --git a/Package.swift b/Package.swift index 876b2bb..c7a9e21 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.2 +// swift-tools-version:5.3 import PackageDescription let package = Package( diff --git a/Sources/NemID/NemIDEnvironment.swift b/Sources/NemID/NemIDEnvironment.swift index ff597d6..9f8ad0e 100644 --- a/Sources/NemID/NemIDEnvironment.swift +++ b/Sources/NemID/NemIDEnvironment.swift @@ -1,17 +1,23 @@ import Foundation public struct NemIDEnvironment { + /// The endpoint for the PID-CPR match service. let pidCPRMatchEndpoint: String + /// The SHA256 fingerprint of the OCES root certificate. + let ocesCertificateFingerprint: String - init(pidCPRMatchEndpoint: String) { + init(pidCPRMatchEndpoint: String, ocesCertificateFingerprint: String) { self.pidCPRMatchEndpoint = pidCPRMatchEndpoint + self.ocesCertificateFingerprint = ocesCertificateFingerprint } public static let production = NemIDEnvironment( - pidCPRMatchEndpoint: "https://pidws.certifikat.dk/pid_serviceprovider_server/pidxml/" + pidCPRMatchEndpoint: "https://pidws.certifikat.dk/pid_serviceprovider_server/pidxml/", + ocesCertificateFingerprint: "92d8092ee77bc9208f0897dc05271894e63ef27933ae537fb983eef0eae3eec8" ) public static let preproduction = NemIDEnvironment( - pidCPRMatchEndpoint: "https://pidws.pp.certifikat.dk/pid_serviceprovider_server/pidxml/" + pidCPRMatchEndpoint: "https://pidws.pp.certifikat.dk/pid_serviceprovider_server/pidxml/", + ocesCertificateFingerprint: "0e2fd1fda36a4bf3995e28619704d60e3382c91e44a2b458ab891316380b1d50" ) } diff --git a/Sources/NemID/NemIDLoginService.swift b/Sources/NemID/NemIDLoginService.swift index fe53134..69ca3fc 100644 --- a/Sources/NemID/NemIDLoginService.swift +++ b/Sources/NemID/NemIDLoginService.swift @@ -33,7 +33,8 @@ public struct LiveNemIDLoginService: NemIDLoginService { let responseHandler = NemIDResponseHandler( xmlParser: libxml2XMLDSigParser(), ocspClient: HTTPOCSPClient(client: httpClient, eventLoop: eventLoop, logger: logger), - eventLoop: self.eventLoop + eventLoop: self.eventLoop, + configuration: self.configuration ) return responseHandler.verifyAndExtractUser(from: response) diff --git a/Sources/NemID/OCSP/OCSPResponse.swift b/Sources/NemID/OCSP/OCSPResponse.swift index 6197e2b..7c9c564 100644 --- a/Sources/NemID/OCSP/OCSPResponse.swift +++ b/Sources/NemID/OCSP/OCSPResponse.swift @@ -376,7 +376,6 @@ extension OCSPResponse.BasicOCSPResponse.ResponseData.SingleResponse { } defer { CNemIDBoringSSL_OPENSSL_free(issuerKeyHashPtr) } - #warning("check if this works with NemID") // Parse serial number var serialNumberCBS = CBS() guard CNemIDBoringSSL_CBS_get_asn1(cbs, &serialNumberCBS, CBS_ASN1_INTEGER) == 1 else { diff --git a/Sources/NemID/Response Handler/CertificateChainValidationError.swift b/Sources/NemID/Response Handler/CertificateChainValidationError.swift index cff8b16..6368d3f 100644 --- a/Sources/NemID/Response Handler/CertificateChainValidationError.swift +++ b/Sources/NemID/Response Handler/CertificateChainValidationError.swift @@ -7,4 +7,5 @@ enum CertificateChainValidationError: Error { case issuerDidNotHaveCAFlag case leafDidNotHaveDigitalSignatureKeyUsage case issuerDidNotHaveKeyCertSignKeyUsage + case failedToVerifyRootAsOCES } diff --git a/Sources/NemID/Response Handler/NemIDResponseHandler.swift b/Sources/NemID/Response Handler/NemIDResponseHandler.swift index 67cf40e..b321f0f 100644 --- a/Sources/NemID/Response Handler/NemIDResponseHandler.swift +++ b/Sources/NemID/Response Handler/NemIDResponseHandler.swift @@ -11,11 +11,13 @@ struct NemIDResponseHandler { private let xmlParser: XMLDSigParser private let ocspClient: OCSPClient private let eventLoop: EventLoop + private let configuration: NemIDConfiguration - init(xmlParser: XMLDSigParser, ocspClient: OCSPClient, eventLoop: EventLoop) { + init(xmlParser: XMLDSigParser, ocspClient: OCSPClient, eventLoop: EventLoop, configuration: NemIDConfiguration) { self.xmlParser = xmlParser self.ocspClient = ocspClient self.eventLoop = eventLoop + self.configuration = configuration } /// Verifies a response from a NemID client flow such as logging in and extratcs the user as `NemIDUser` @@ -171,7 +173,9 @@ struct NemIDResponseHandler { } // Verify that root certificate is a trusted OCES certificate. - #warning("todo") + guard try chain.root.fingerprint() == configuration.environment.ocesCertificateFingerprint else { + throw CertificateChainValidationError.failedToVerifyRootAsOCES + } } /// Verifies the signed element in the xml response diff --git a/Tests/NemIDTests/Helpers/Bytes+Hex.swift b/Sources/NemID/Utilities/Bytes+Hex.swift similarity index 86% rename from Tests/NemIDTests/Helpers/Bytes+Hex.swift rename to Sources/NemID/Utilities/Bytes+Hex.swift index ea42384..b03fcaa 100644 --- a/Tests/NemIDTests/Helpers/Bytes+Hex.swift +++ b/Sources/NemID/Utilities/Bytes+Hex.swift @@ -2,15 +2,15 @@ import Foundation // https://github.com/vapor/vapor/blob/main/Sources/Vapor/Utilities/Bytes%2BHex.swift extension Sequence where Element == UInt8 { - public var hex: String { + var hex: String { self.hexEncodedString() } - public func hexEncodedString(uppercase: Bool = false) -> String { + func hexEncodedString(uppercase: Bool = false) -> String { return String(decoding: self.hexEncodedBytes(uppercase: uppercase), as: Unicode.UTF8.self) } - public func hexEncodedBytes(uppercase: Bool = false) -> [UInt8] { + func hexEncodedBytes(uppercase: Bool = false) -> [UInt8] { let table: [UInt8] = uppercase ? radix16table_uppercase : radix16table_lowercase var result: [UInt8] = [] @@ -23,7 +23,7 @@ extension Sequence where Element == UInt8 { } extension Collection where Element == UInt8 { - public func hexEncodedBytes(uppercase: Bool = false) -> [UInt8] { + func hexEncodedBytes(uppercase: Bool = false) -> [UInt8] { let table: [UInt8] = uppercase ? radix16table_uppercase : radix16table_lowercase return .init(unsafeUninitializedCapacity: self.count * 2) { buffer, outCount in diff --git a/Sources/NemID/X509/NemIDX509Certificate.swift b/Sources/NemID/X509/NemIDX509Certificate.swift index 4dd4bee..8909a5c 100644 --- a/Sources/NemID/X509/NemIDX509Certificate.swift +++ b/Sources/NemID/X509/NemIDX509Certificate.swift @@ -90,6 +90,11 @@ public final class NemIDX509Certificate: BIOLoadable { return try handler(pubKey) } + /// Returns the SHA256 fingerprint of the certificate as a hex string + func fingerprint() throws -> String { + try SHA256.hash(data: self.toDERBytes()).hex + } + /// Returns the subject as ASN.1/DER encoded bytes. var subject: [UInt8]? { let _subjectName = CNemIDBoringSSL_X509_get_subject_name(ref) From 18752e4a6de3549ee48cf90f50844a89d9da4f34 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Fri, 23 Apr 2021 12:19:01 +0200 Subject: [PATCH 18/22] Fix validation of OCSP signature --- Sources/NemID/OCSP/OCSPResponse.swift | 17 ++++++++++++----- .../Response Handler/NemIDResponseHandler.swift | 16 ++++++---------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/Sources/NemID/OCSP/OCSPResponse.swift b/Sources/NemID/OCSP/OCSPResponse.swift index 7c9c564..d376518 100644 --- a/Sources/NemID/OCSP/OCSPResponse.swift +++ b/Sources/NemID/OCSP/OCSPResponse.swift @@ -123,8 +123,9 @@ extension OCSPResponse { init(cbs: UnsafeMutablePointer) throws { // Parse tbsResponseData (ResponseData) + // asn1_element: We need the header bytes as well to validate the signature of the response var tbsResponseDataCBS = CBS() - guard CNemIDBoringSSL_CBS_get_asn1(cbs, &tbsResponseDataCBS, CBS_ASN1_SEQUENCE) == 1 else { + guard CNemIDBoringSSL_CBS_get_asn1_element(cbs, &tbsResponseDataCBS, CBS_ASN1_SEQUENCE) == 1 else { throw OCSPResponseError.failedToParseResponse } @@ -217,20 +218,26 @@ extension OCSPResponse.BasicOCSPResponse { } defer { CNemIDBoringSSL_OPENSSL_free(tbsResponseDataPtr) } + // Since `cbs` contains the header bytes as well, we need to unwrap it into a sequence to continue parsing. + var responseDataCBS = CBS() + guard CNemIDBoringSSL_CBS_get_asn1(cbs, &responseDataCBS, CBS_ASN1_SEQUENCE) == 1 else { + throw OCSPResponseError.failedToParseResponse + } + // Ignore responderID - var responderID = CBS() - guard CNemIDBoringSSL_CBS_get_any_asn1(cbs, &responderID, nil) == 1 else { + var responderCBS = CBS() + guard CNemIDBoringSSL_CBS_get_any_asn1(&responseDataCBS, &responderCBS, nil) == 1 else { throw OCSPResponseError.failedToParseResponse } // Ignore producedAt var producedAt = CBS() - guard CNemIDBoringSSL_CBS_get_asn1(cbs, &producedAt, CBS_ASN1_GENERALIZEDTIME) == 1 else { + guard CNemIDBoringSSL_CBS_get_asn1(&responseDataCBS, &producedAt, CBS_ASN1_GENERALIZEDTIME) == 1 else { throw OCSPResponseError.failedToParseResponse } var responsesCBS = CBS() - guard CNemIDBoringSSL_CBS_get_asn1(cbs, &responsesCBS, CBS_ASN1_SEQUENCE) == 1 else { + guard CNemIDBoringSSL_CBS_get_asn1(&responseDataCBS, &responsesCBS, CBS_ASN1_SEQUENCE) == 1 else { throw OCSPResponseError.failedToParseResponse } diff --git a/Sources/NemID/Response Handler/NemIDResponseHandler.swift b/Sources/NemID/Response Handler/NemIDResponseHandler.swift index b321f0f..5b15171 100644 --- a/Sources/NemID/Response Handler/NemIDResponseHandler.swift +++ b/Sources/NemID/Response Handler/NemIDResponseHandler.swift @@ -3,10 +3,6 @@ import Crypto import NIO @_implementationOnly import CNemIDBoringSSL -#warning("if something fails it might be releated to this:") -/* - //remember to skip the first byte it is the number of unused bits and it is always 0 for keys and certificates from nodes-php - */ struct NemIDResponseHandler { private let xmlParser: XMLDSigParser private let ocspClient: OCSPClient @@ -46,7 +42,7 @@ struct NemIDResponseHandler { throw clientError } - // Else parse the response as a successful XML message. + // Parse the response as a successful XML message. let parsedResponse = try xmlParser.parse([UInt8](base64DecodedData)) // Extract certificate chain. @@ -84,12 +80,12 @@ struct NemIDResponseHandler { guard let ocspCertificate = basicResponse.certs.first else { throw OCSPValidationError.certificateNotFoundInResponse } - #warning("der bytes has correct length, so I think it's the signer failing?s") - let signer = RSASigner(key: try ocspCertificate.publicKey(), hashAlgorithm: basicResponse.signatureAlgorithm.hashAlgorithm) -// guard try signer.verify(basicResponse.signature, signs: basicResponse.tbsResponseData.derBytes) else { -// throw OCSPValidationError.signatureWasNotSignedByCertificate -// } + // Validate OCSP signature was made from tbsResponseData. + let signer = RSASigner(key: try ocspCertificate.publicKey(), hashAlgorithm: basicResponse.signatureAlgorithm.hashAlgorithm) + guard try signer.verify(basicResponse.signature, signs: basicResponse.tbsResponseData.derBytes) else { + throw OCSPValidationError.signatureWasNotSignedByCertificate + } // Validate that accompanying certificate was signed by issuer. guard try ocspCertificate.isSignedBy(by: chain.intermediate) else { From dda90df3840cc9e6a50dbad77eea447c2b3759f0 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Fri, 23 Apr 2021 14:48:56 +0200 Subject: [PATCH 19/22] Handle PID-CPR match response --- .../HTTPNemIDPIDCPRMatchClient.swift | 76 +++++++++++++++---- .../Models/PIDCPRMatchRequest.swift | 61 +++++++-------- .../Models/PIDCPRMatchResponse.swift | 56 ++++++++++++++ .../NemIDPIDCPRMatchClient.swift | 7 +- .../PID-CPR Match/PIDCPRServiceError.swift | 20 +++++ .../NemIDTests/PIDCPRMatchResponseTests.swift | 32 ++++++++ 6 files changed, 202 insertions(+), 50 deletions(-) create mode 100644 Sources/NemID/PID-CPR Match/Models/PIDCPRMatchResponse.swift create mode 100644 Sources/NemID/PID-CPR Match/PIDCPRServiceError.swift create mode 100644 Tests/NemIDTests/PIDCPRMatchResponseTests.swift diff --git a/Sources/NemID/PID-CPR Match/HTTPNemIDPIDCPRMatchClient.swift b/Sources/NemID/PID-CPR Match/HTTPNemIDPIDCPRMatchClient.swift index b7e5bed..2eeb34b 100644 --- a/Sources/NemID/PID-CPR Match/HTTPNemIDPIDCPRMatchClient.swift +++ b/Sources/NemID/PID-CPR Match/HTTPNemIDPIDCPRMatchClient.swift @@ -4,18 +4,21 @@ import AsyncHTTPClient import XMLCoder import Logging -struct HTTPNemIDPIDCPRMatchClient: NemIDPIDCPRMatchClient { - enum HTTPPIDCPRMatchClientError: Error { - case failedToBuildRequest - } - +enum HTTPPIDCPRMatchClientError: Error { + case failedToBuildRequest + case badStatusCode + case invalidResponseBody +} + +public struct HTTPNemIDPIDCPRMatchClient: NemIDPIDCPRMatchClient { private let configuration: NemIDConfiguration private let eventLoop: EventLoop private let xmlEncoder = XMLEncoder() + private let xmlDecoder = XMLDecoder() private let httpClient: HTTPClient private let logger: Logger - init( + public init( configuration: NemIDConfiguration, eventLoop: EventLoop, httpClient: HTTPClient, @@ -27,16 +30,14 @@ struct HTTPNemIDPIDCPRMatchClient: NemIDPIDCPRMatchClient { self.logger = logger } - func verifyPID(_ pid: String, matches cpr: String) -> EventLoopFuture { + public func verifyPID(_ pid: String, matches cpr: String) -> EventLoopFuture { do { let request = PIDCPRMatchRequest( - method: .init( - request: .init( - id: UUID().uuidString, - serviceProviderID: configuration.serviceProviderID, - pid: pid, - cpr: cpr - ) + request: .init( + id: UUID().uuidString, + serviceProviderID: configuration.serviceProviderID, + pid: pid, + cpr: cpr ) ) @@ -44,6 +45,7 @@ struct HTTPNemIDPIDCPRMatchClient: NemIDPIDCPRMatchClient { guard let requestString = String(data: requestData, encoding: .utf8) else { throw HTTPPIDCPRMatchClientError.failedToBuildRequest } + #warning("urlEncoded might not be neccessary according to postman") let formEncodedRequest = try "PID_REQUEST=\(requestString)".urlEncoded() var httpRequest = try HTTPClient.Request(url: configuration.environment.pidCPRMatchEndpoint, method: .POST) @@ -51,10 +53,52 @@ struct HTTPNemIDPIDCPRMatchClient: NemIDPIDCPRMatchClient { httpRequest.body = .string(formEncodedRequest) #warning("need to add certificate to request") - fatalError() -// return httpClient.execute(request: httpRequest, eventLoop: .delegate(on: eventLoop), logger: logger) + return httpClient.execute(request: httpRequest, eventLoop: .delegate(on: eventLoop), logger: logger) + .flatMapThrowing { response -> PIDCPRMatchResponse in + guard (200...299).contains(response.status.code) else { + throw HTTPPIDCPRMatchClientError.badStatusCode + } + guard let body = response.body else { + throw HTTPPIDCPRMatchClientError.invalidResponseBody + } + return try xmlDecoder.decode(PIDCPRMatchResponse.self, from: Data(buffer: body)) + } + .flatMapThrowing { decodedResponse in + switch decodedResponse.response.status.statusCode { + case 0: + return true + case 1: + return false + default: + let reason = decodedResponse.response.status.statusText + .first(where: { $0.language == "UK" })? + .value + throw PIDCPRServiceError( + statusCode: decodedResponse.response.status.statusCode, + reason: reason + ) + } + } } catch { return eventLoop.makeFailedFuture(error) } } + + public func delegating(to eventLoop: EventLoop) -> HTTPNemIDPIDCPRMatchClient { + HTTPNemIDPIDCPRMatchClient( + configuration: self.configuration, + eventLoop: eventLoop, + httpClient: self.httpClient, + logger: self.logger + ) + } + + public func logging(to logger: Logger) -> HTTPNemIDPIDCPRMatchClient { + HTTPNemIDPIDCPRMatchClient( + configuration: self.configuration, + eventLoop: self.eventLoop, + httpClient: self.httpClient, + logger: logger + ) + } } diff --git a/Sources/NemID/PID-CPR Match/Models/PIDCPRMatchRequest.swift b/Sources/NemID/PID-CPR Match/Models/PIDCPRMatchRequest.swift index 7f9d4aa..4daa4fe 100644 --- a/Sources/NemID/PID-CPR Match/Models/PIDCPRMatchRequest.swift +++ b/Sources/NemID/PID-CPR Match/Models/PIDCPRMatchRequest.swift @@ -1,47 +1,44 @@ import Foundation import XMLCoder -struct PIDCPRMatchRequest: Encodable { - struct Method: Encodable, DynamicNodeEncoding { - struct Request: Encodable, DynamicNodeEncoding { - let id: String - let serviceProviderID: String - let pid: String - let cpr: String - - enum CodingKeys: String, CodingKey { - case id - case serviceProviderID = "serviceId" - case pid - case cpr - } - - static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding { - switch key { - case CodingKeys.id: return .attribute - default: return .element - } - } - } - - let name: String = "pidCprRequest" - let version: String = "1.0" - let request: Request +/// Encodes as +struct PIDCPRMatchRequest: Encodable, DynamicNodeEncoding { + struct Request: Encodable, DynamicNodeEncoding { + let id: String + let serviceProviderID: String + let pid: String + let cpr: String enum CodingKeys: String, CodingKey { - case name - case version - case request + case id + case serviceProviderID = "serviceId" + case pid + case cpr } static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding { switch key { - case CodingKeys.name: return .attribute - case CodingKeys.version: return .attribute + case CodingKeys.id: return .attribute default: return .element } } } - let method: Method + let name: String = "pidCprRequest" + let version: String = "1.0" + let request: Request + + enum CodingKeys: String, CodingKey { + case name + case version + case request + } + + static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding { + switch key { + case CodingKeys.name: return .attribute + case CodingKeys.version: return .attribute + default: return .element + } + } } diff --git a/Sources/NemID/PID-CPR Match/Models/PIDCPRMatchResponse.swift b/Sources/NemID/PID-CPR Match/Models/PIDCPRMatchResponse.swift new file mode 100644 index 0000000..f55a338 --- /dev/null +++ b/Sources/NemID/PID-CPR Match/Models/PIDCPRMatchResponse.swift @@ -0,0 +1,56 @@ +import Foundation +import XMLCoder + +/// Decodes as +struct PIDCPRMatchResponse: Decodable { + struct Response: Decodable { + struct Status: Decodable, DynamicNodeDecoding { + struct StatusText: Decodable, DynamicNodeDecoding { + let language: String + let value: String + + enum CodingKeys: String, CodingKey { + case language + } + + static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding { + switch key { + case CodingKeys.language: + return .attribute + default: + return .element + } + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let valueContainer = try decoder.singleValueContainer() + + self.language = try container.decode(String.self, forKey: .language) + self.value = try valueContainer.decode(String.self) + } + } + + let statusCode: Int + let statusText: [StatusText] + + enum CodingKeys: String, CodingKey { + case statusCode + case statusText + } + + static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding { + switch key { + case CodingKeys.statusCode: + return .attribute + default: + return .element + } + } + } + + let status: Status + } + + let response: Response +} diff --git a/Sources/NemID/PID-CPR Match/NemIDPIDCPRMatchClient.swift b/Sources/NemID/PID-CPR Match/NemIDPIDCPRMatchClient.swift index 30138c2..1694347 100644 --- a/Sources/NemID/PID-CPR Match/NemIDPIDCPRMatchClient.swift +++ b/Sources/NemID/PID-CPR Match/NemIDPIDCPRMatchClient.swift @@ -1,7 +1,10 @@ -import Foundation import NIO +import Logging public protocol NemIDPIDCPRMatchClient { /// Verifies that a given User PID`pid` matches a danish CPR number `cpr`. - func verifyPID(_ pid: String, matches cpr: String) -> EventLoopFuture + func verifyPID(_ pid: String, matches cpr: String) -> EventLoopFuture + + func delegating(to eventLoop: EventLoop) -> Self + func logging(to logger: Logger) -> Self } diff --git a/Sources/NemID/PID-CPR Match/PIDCPRServiceError.swift b/Sources/NemID/PID-CPR Match/PIDCPRServiceError.swift new file mode 100644 index 0000000..4b46021 --- /dev/null +++ b/Sources/NemID/PID-CPR Match/PIDCPRServiceError.swift @@ -0,0 +1,20 @@ +import Foundation + +struct PIDCPRServiceError: LocalizedError, CustomStringConvertible { + let statusCode: Int + let reason: String? + + var description: String { + var desc = "PIDCPRServiceError(status: \(statusCode)" + if let reason = reason { + desc += ", message: \(reason))" + } else { + desc += ")" + } + return desc + } + + var errorDescription: String? { + description + } +} diff --git a/Tests/NemIDTests/PIDCPRMatchResponseTests.swift b/Tests/NemIDTests/PIDCPRMatchResponseTests.swift new file mode 100644 index 0000000..40d6baf --- /dev/null +++ b/Tests/NemIDTests/PIDCPRMatchResponseTests.swift @@ -0,0 +1,32 @@ +import XCTest +import XMLCoder +@testable import NemID + +final class PIDCPRMatchResponseTests: XCTestCase { + let xml = """ + + + + + CPR svarer ikke til PID + CPR does not match PID + + 9208-2002-2-871296153613 + 0112160831 + Not implemented + + + """ + + func test_decode() throws { + let xmlDecoder = XMLDecoder() + let decoded = try xmlDecoder.decode(PIDCPRMatchResponse.self, from: xml.data(using: .utf8)!) + + XCTAssertEqual(decoded.response.status.statusCode, 1) + XCTAssertEqual(decoded.response.status.statusText.count, 2) + XCTAssertEqual(decoded.response.status.statusText.first?.language, "DK") + XCTAssertEqual(decoded.response.status.statusText.first?.value, "CPR svarer ikke til PID") + XCTAssertEqual(decoded.response.status.statusText.last?.language, "UK") + XCTAssertEqual(decoded.response.status.statusText.last?.value, "CPR does not match PID") + } +} From 76d3a6f51f86fe8ce6556b2ca4eaafba71e8d101 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Wed, 5 May 2021 17:59:15 +0200 Subject: [PATCH 20/22] add certificate to pid request --- Package.swift | 4 +++- Sources/NemID/NemIDLoginService.swift | 2 +- .../HTTPNemIDPIDCPRMatchClient.swift | 14 ++++++++--- Sources/NemID/RSA/NemIDRSAKey.swift | 24 +++++++++++++++++++ Sources/NemID/RSA/NemIDRSAKeyError.swift | 5 ++++ 5 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 Sources/NemID/RSA/NemIDRSAKeyError.swift diff --git a/Package.swift b/Package.swift index c7a9e21..39e2330 100644 --- a/Package.swift +++ b/Package.swift @@ -16,8 +16,10 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/apple/swift-crypto.git", from: "1.1.3"), - .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.0.0"), +// .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.0.0"), .package(url: "https://github.com/MaxDesiatov/XMLCoder.git", from: "0.12.0"), + .package(url: "https://github.com/apple/swift-nio-ssl.git", .branch("main")), + .package(url: "https://github.com/madsodgaard/async-http-client.git", .branch("mo-request-tls")), ], targets: [ .target( diff --git a/Sources/NemID/NemIDLoginService.swift b/Sources/NemID/NemIDLoginService.swift index 69ca3fc..34ca1fc 100644 --- a/Sources/NemID/NemIDLoginService.swift +++ b/Sources/NemID/NemIDLoginService.swift @@ -9,7 +9,7 @@ public protocol NemIDLoginService { /// Validates a XMLDSig message from client, according to the NemID documentation, and returns the certificate user. /// - Parameters: - /// - response: The XMLDSig xml-document as UTF-8 encoded bytes + /// - response: The NemID response as base64 encoded `Data` func validateAndExtractUser(fromResponse response: Data) -> EventLoopFuture func delegating(to eventLoop: EventLoop) -> Self diff --git a/Sources/NemID/PID-CPR Match/HTTPNemIDPIDCPRMatchClient.swift b/Sources/NemID/PID-CPR Match/HTTPNemIDPIDCPRMatchClient.swift index 2eeb34b..c841625 100644 --- a/Sources/NemID/PID-CPR Match/HTTPNemIDPIDCPRMatchClient.swift +++ b/Sources/NemID/PID-CPR Match/HTTPNemIDPIDCPRMatchClient.swift @@ -3,6 +3,7 @@ import NIO import AsyncHTTPClient import XMLCoder import Logging +import NIOSSL enum HTTPPIDCPRMatchClientError: Error { case failedToBuildRequest @@ -45,15 +46,22 @@ public struct HTTPNemIDPIDCPRMatchClient: NemIDPIDCPRMatchClient { guard let requestString = String(data: requestData, encoding: .utf8) else { throw HTTPPIDCPRMatchClientError.failedToBuildRequest } - #warning("urlEncoded might not be neccessary according to postman") let formEncodedRequest = try "PID_REQUEST=\(requestString)".urlEncoded() var httpRequest = try HTTPClient.Request(url: configuration.environment.pidCPRMatchEndpoint, method: .POST) httpRequest.headers.add(name: "Content-Type", value: "application/x-www-form-urlencoded") httpRequest.body = .string(formEncodedRequest) - #warning("need to add certificate to request") - return httpClient.execute(request: httpRequest, eventLoop: .delegate(on: eventLoop), logger: logger) + // Configure TLS + let certificate = try NIOSSLCertificate(bytes: configuration.spCertificate.toDERBytes(), format: .der) + let privateKey = try NIOSSLPrivateKey(bytes: configuration.privateKey.toDERBytes(), format: .der) + httpRequest.tlsConfiguration = .forClient( + certificateChain: [.certificate(certificate)], + privateKey: .privateKey(privateKey) + ) + + return httpClient + .execute(request: httpRequest, eventLoop: .delegate(on: eventLoop), logger: logger) .flatMapThrowing { response -> PIDCPRMatchResponse in guard (200...299).contains(response.status.code) else { throw HTTPPIDCPRMatchClientError.badStatusCode diff --git a/Sources/NemID/RSA/NemIDRSAKey.swift b/Sources/NemID/RSA/NemIDRSAKey.swift index 038352b..12eac29 100644 --- a/Sources/NemID/RSA/NemIDRSAKey.swift +++ b/Sources/NemID/RSA/NemIDRSAKey.swift @@ -16,6 +16,30 @@ public final class NemIDRSAKey: BIOLoadable { return self.init(privateKey) } + func toDERBytes() throws -> [UInt8] { + return try self.withUnsafeDERCertificateBuffer { Array($0) } + } + + private func withUnsafeDERCertificateBuffer(_ body: (UnsafeRawBufferPointer) throws -> T) throws -> T { + guard let bio = CNemIDBoringSSL_BIO_new(CNemIDBoringSSL_BIO_s_mem()) else { + fatalError("Failed to malloc for a BIO handler") + } + defer { CNemIDBoringSSL_BIO_free(bio) } + + guard CNemIDBoringSSL_i2d_RSAPrivateKey_bio(bio, self.ref) == 1 else { + throw NemIDX509CertificateError.failedToRetrieveDERRepresentation + } + + var dataPtr: UnsafeMutablePointer? = nil + let length = CNemIDBoringSSL_BIO_get_mem_data(bio, &dataPtr) + + guard let bytes = dataPtr.map({ UnsafeRawBufferPointer(start: $0, count: length) }) else { + fatalError("Failed to map bytes from a certificate") + } + + return try body(bytes) + } + var ref: UnsafeMutablePointer { _ref.assumingMemoryBound(to: RSA.self) } diff --git a/Sources/NemID/RSA/NemIDRSAKeyError.swift b/Sources/NemID/RSA/NemIDRSAKeyError.swift new file mode 100644 index 0000000..83d49d5 --- /dev/null +++ b/Sources/NemID/RSA/NemIDRSAKeyError.swift @@ -0,0 +1,5 @@ +import Foundation + +enum NemIDRSAKeyError: Error { + case failedToRetrieveDERRepresentation +} From cc4ffc936d3c044adf40defd0c5a25c2f8008adc Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Sat, 15 May 2021 13:13:09 +0200 Subject: [PATCH 21/22] Update AHC --- Package.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Package.swift b/Package.swift index 39e2330..2a9e54b 100644 --- a/Package.swift +++ b/Package.swift @@ -16,10 +16,8 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/apple/swift-crypto.git", from: "1.1.3"), -// .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.0.0"), + .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.3.0"), .package(url: "https://github.com/MaxDesiatov/XMLCoder.git", from: "0.12.0"), - .package(url: "https://github.com/apple/swift-nio-ssl.git", .branch("main")), - .package(url: "https://github.com/madsodgaard/async-http-client.git", .branch("mo-request-tls")), ], targets: [ .target( From a200f3acff6a1dbec73c5537246e1b126f38cc0b Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Mon, 1 Nov 2021 14:18:35 +0100 Subject: [PATCH 22/22] Depend on specific NIO modules --- Package.resolved | 39 ++++++++++++------- Package.swift | 5 +++ Sources/NemID/NemIDLoginService.swift | 2 +- Sources/NemID/OCSP/HTTPOCSPClient.swift | 2 +- Sources/NemID/OCSP/OCSPClient.swift | 2 +- .../HTTPNemIDPIDCPRMatchClient.swift | 11 +++--- .../NemIDPIDCPRMatchClient.swift | 2 +- .../NemIDResponseHandler.swift | 2 +- 8 files changed, 40 insertions(+), 25 deletions(-) diff --git a/Package.resolved b/Package.resolved index 34cf9a8..8dbba70 100644 --- a/Package.resolved +++ b/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/swift-server/async-http-client.git", "state": { "branch": null, - "revision": "ba845ee93d6ea10671e829ebf273b6f7a0c92ef0", - "version": "1.2.4" + "revision": "1081b0b0541f535ca088acdb56f5ca5598bc6247", + "version": "1.6.3" } }, { @@ -15,8 +15,8 @@ "repositoryURL": "https://github.com/apple/swift-crypto.git", "state": { "branch": null, - "revision": "296d3308b4b2fa355cfe0de4ca411bf7a1cd8cf8", - "version": "1.1.4" + "revision": "3bea268b223651c4ab7b7b9ad62ef9b2d4143eb6", + "version": "1.1.6" } }, { @@ -33,8 +33,8 @@ "repositoryURL": "https://github.com/apple/swift-nio.git", "state": { "branch": null, - "revision": "6d3ca7e54e06a69d0f2612c2ce8bb8b7319085a4", - "version": "2.26.0" + "revision": "6aa9347d9bc5bbfe6a84983aec955c17ffea96ef", + "version": "2.33.0" } }, { @@ -42,8 +42,17 @@ "repositoryURL": "https://github.com/apple/swift-nio-extras.git", "state": { "branch": null, - "revision": "de1c80ad1fdff1ba772bcef6b392c3ef735f39a6", - "version": "1.8.0" + "revision": "f73ca5ee9c6806800243f1ac415fcf82de9a4c91", + "version": "1.10.2" + } + }, + { + "package": "swift-nio-http2", + "repositoryURL": "https://github.com/apple/swift-nio-http2.git", + "state": { + "branch": null, + "revision": "326f7f9a8c8c8402e3691adac04911cac9f9d87f", + "version": "1.18.4" } }, { @@ -51,8 +60,8 @@ "repositoryURL": "https://github.com/apple/swift-nio-ssl.git", "state": { "branch": null, - "revision": "bbb38fbcbbe9dc4665b2c638dfa5681b01079bfb", - "version": "2.10.4" + "revision": "5e68c1ded15619bb281b273fa8c2d8fd7f7b2b7d", + "version": "2.16.1" } }, { @@ -60,17 +69,17 @@ "repositoryURL": "https://github.com/apple/swift-nio-transport-services.git", "state": { "branch": null, - "revision": "1d28d48e071727f4558a8a4bb1894472abc47a58", - "version": "1.9.2" + "revision": "e7f5278a26442dc46783ba7e063643d524e414a0", + "version": "1.11.3" } }, { "package": "XMLCoder", - "repositoryURL": "https://github.com/MaxDesiatov/XMLCoder", + "repositoryURL": "https://github.com/MaxDesiatov/XMLCoder.git", "state": { "branch": null, - "revision": "b8fa7fe4c0849f8a64cc246e71c5d976809222e4", - "version": "0.12.0" + "revision": "887de88b37b2d691d67db950770e09776229cf6d", + "version": "0.13.0" } } ] diff --git a/Package.swift b/Package.swift index 2a9e54b..25b1ab6 100644 --- a/Package.swift +++ b/Package.swift @@ -15,6 +15,8 @@ let package = Package( MANGLE_END */ ], dependencies: [ + .package(url: "https://github.com/apple/swift-nio.git", from: "2.33.0"), + .package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.16.1"), .package(url: "https://github.com/apple/swift-crypto.git", from: "1.1.3"), .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.3.0"), .package(url: "https://github.com/MaxDesiatov/XMLCoder.git", from: "0.12.0"), @@ -26,6 +28,9 @@ let package = Package( "Clibxml2", "CNemIDBoringSSL", .product(name: "Crypto", package: "swift-crypto"), + .product(name: "NIOCore", package: "swift-nio"), + .product(name: "NIOFoundationCompat", package: "swift-nio"), + .product(name: "NIOSSL", package: "swift-nio-ssl"), .product(name: "AsyncHTTPClient", package: "async-http-client"), "XMLCoder", ] diff --git a/Sources/NemID/NemIDLoginService.swift b/Sources/NemID/NemIDLoginService.swift index 34ca1fc..ea1d3d9 100644 --- a/Sources/NemID/NemIDLoginService.swift +++ b/Sources/NemID/NemIDLoginService.swift @@ -1,5 +1,5 @@ import Foundation -import NIO +import NIOCore import Logging import AsyncHTTPClient diff --git a/Sources/NemID/OCSP/HTTPOCSPClient.swift b/Sources/NemID/OCSP/HTTPOCSPClient.swift index 6847b67..62cf902 100644 --- a/Sources/NemID/OCSP/HTTPOCSPClient.swift +++ b/Sources/NemID/OCSP/HTTPOCSPClient.swift @@ -1,5 +1,5 @@ import Foundation -import NIO +import NIOCore import AsyncHTTPClient import Logging diff --git a/Sources/NemID/OCSP/OCSPClient.swift b/Sources/NemID/OCSP/OCSPClient.swift index c119693..fb53737 100644 --- a/Sources/NemID/OCSP/OCSPClient.swift +++ b/Sources/NemID/OCSP/OCSPClient.swift @@ -1,5 +1,5 @@ import Foundation -import NIO +import NIOCore protocol OCSPClient { func send(request: OCSPRequest) -> EventLoopFuture diff --git a/Sources/NemID/PID-CPR Match/HTTPNemIDPIDCPRMatchClient.swift b/Sources/NemID/PID-CPR Match/HTTPNemIDPIDCPRMatchClient.swift index c841625..d99f210 100644 --- a/Sources/NemID/PID-CPR Match/HTTPNemIDPIDCPRMatchClient.swift +++ b/Sources/NemID/PID-CPR Match/HTTPNemIDPIDCPRMatchClient.swift @@ -1,5 +1,6 @@ import Foundation -import NIO +import NIOCore +import NIOFoundationCompat import AsyncHTTPClient import XMLCoder import Logging @@ -55,10 +56,10 @@ public struct HTTPNemIDPIDCPRMatchClient: NemIDPIDCPRMatchClient { // Configure TLS let certificate = try NIOSSLCertificate(bytes: configuration.spCertificate.toDERBytes(), format: .der) let privateKey = try NIOSSLPrivateKey(bytes: configuration.privateKey.toDERBytes(), format: .der) - httpRequest.tlsConfiguration = .forClient( - certificateChain: [.certificate(certificate)], - privateKey: .privateKey(privateKey) - ) + var tlsConfiguration = TLSConfiguration.makeClientConfiguration() + tlsConfiguration.certificateChain = [.certificate(certificate)] + tlsConfiguration.privateKey = .privateKey(privateKey) + httpRequest.tlsConfiguration = tlsConfiguration return httpClient .execute(request: httpRequest, eventLoop: .delegate(on: eventLoop), logger: logger) diff --git a/Sources/NemID/PID-CPR Match/NemIDPIDCPRMatchClient.swift b/Sources/NemID/PID-CPR Match/NemIDPIDCPRMatchClient.swift index 1694347..94081d9 100644 --- a/Sources/NemID/PID-CPR Match/NemIDPIDCPRMatchClient.swift +++ b/Sources/NemID/PID-CPR Match/NemIDPIDCPRMatchClient.swift @@ -1,4 +1,4 @@ -import NIO +import NIOCore import Logging public protocol NemIDPIDCPRMatchClient { diff --git a/Sources/NemID/Response Handler/NemIDResponseHandler.swift b/Sources/NemID/Response Handler/NemIDResponseHandler.swift index 5b15171..61030fe 100644 --- a/Sources/NemID/Response Handler/NemIDResponseHandler.swift +++ b/Sources/NemID/Response Handler/NemIDResponseHandler.swift @@ -1,6 +1,6 @@ import Foundation import Crypto -import NIO +import NIOCore @_implementationOnly import CNemIDBoringSSL struct NemIDResponseHandler {