From fb89843395e5e22bfe0daf9f661dbdf47ef31d51 Mon Sep 17 00:00:00 2001 From: Matthieu Barthelemy Date: Fri, 26 Apr 2024 07:00:49 +1000 Subject: [PATCH 01/30] WIP attestation --- .../AttestationConveyancePreference.swift | 4 +- .../Registration/AttestationObject.swift | 18 +- .../Ceremonies/Registration/Credential.swift | 2 + .../Formats/PackedAttestation.swift | 222 +++++++++--------- Sources/WebAuthn/WebAuthnManager.swift | 1 + 5 files changed, 126 insertions(+), 121 deletions(-) diff --git a/Sources/WebAuthn/Ceremonies/Registration/AttestationConveyancePreference.swift b/Sources/WebAuthn/Ceremonies/Registration/AttestationConveyancePreference.swift index cba6e10a..69de1e0d 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/AttestationConveyancePreference.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/AttestationConveyancePreference.swift @@ -18,7 +18,7 @@ public enum AttestationConveyancePreference: String, Encodable { /// Indicates the Relying Party is not interested in authenticator attestation. case none - // case indirect - // case direct + //case indirect + case direct // case enterprise } diff --git a/Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift b/Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift index d03bb646..be5242dc 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift @@ -56,21 +56,21 @@ public struct AttestationObject { throw WebAuthnError.unsupportedCredentialPublicKeyAlgorithm } - // let pemRootCertificates = pemRootCertificatesByFormat[format] ?? [] + let pemRootCertificates = pemRootCertificatesByFormat[format] ?? [] switch format { case .none: // if format is `none` statement must be empty guard attestationStatement == .map([:]) else { throw WebAuthnError.attestationStatementMustBeEmpty } - // case .packed: - // try await PackedAttestation.verify( - // attStmt: attestationStatement, - // authenticatorData: rawAuthenticatorData, - // clientDataHash: Data(clientDataHash), - // credentialPublicKey: credentialPublicKey, - // pemRootCertificates: pemRootCertificates - // ) + case .packed: + try await PackedAttestation.verify( + attStmt: attestationStatement, + authenticatorData: Data(rawAuthenticatorData), + clientDataHash: Data(clientDataHash), + credentialPublicKey: credentialPublicKey, + pemRootCertificates: pemRootCertificates + ) // case .tpm: // try TPMAttestation.verify( // attStmt: attestationStatement, diff --git a/Sources/WebAuthn/Ceremonies/Registration/Credential.swift b/Sources/WebAuthn/Ceremonies/Registration/Credential.swift index 1c656f4d..32288d9f 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Credential.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Credential.swift @@ -41,6 +41,8 @@ public struct Credential { // MARK: Optional content + public let aaguid: [UInt8]? + public let attestationObject: AttestationObject public let attestationClientDataJSON: CollectedClientData diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedAttestation.swift index 4e835994..28e2c0c9 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedAttestation.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedAttestation.swift @@ -12,124 +12,126 @@ // //===----------------------------------------------------------------------===// -// // 🚨 WIP +import Foundation +import SwiftCBOR +import SwiftASN1 +import X509 +import Crypto +import _CryptoExtras -// import Foundation -// import SwiftCBOR -// import X509 -// import Crypto +struct PackedAttestation { + enum PackedAttestationError: Error { + case invalidAlg + case invalidSig + case invalidX5C + case invalidLeafCertificate + case invalidLeafCertificatePublicKey + case missingAttestationCertificate + case algDoesNotMatch + case missingAttestedCredential + // Authenticator data cannot be verified + case invalidVerificationData + case notImplemented + } -// /// 🚨 WIP -// struct PackedAttestation { -// enum PackedAttestationError: Error { -// case invalidAlg -// case invalidSig -// case invalidX5C -// case invalidLeafCertificate -// case missingAttestationCertificate -// case algDoesNotMatch -// case missingAttestedCredential -// case notImplemented -// } + static func verify( + attStmt: CBOR, + authenticatorData: Data, + clientDataHash: Data, + credentialPublicKey: CredentialPublicKey, + pemRootCertificates: [Data] + ) async throws { + guard let algCBOR = attStmt["alg"], + case let .negativeInt(algorithmNegative) = algCBOR, + let alg = COSEAlgorithmIdentifier(rawValue: -1 - Int(algorithmNegative)) else { + throw PackedAttestationError.invalidAlg + } + guard let sigCBOR = attStmt["sig"], case let .byteString(sig) = sigCBOR else { + throw PackedAttestationError.invalidSig + } + + let verificationData = authenticatorData + clientDataHash -// static func verify( -// attStmt: CBOR, -// authenticatorData: Data, -// clientDataHash: Data, -// credentialPublicKey: CredentialPublicKey, -// pemRootCertificates: [Data] -// ) async throws { -// guard let algCBOR = attStmt["alg"], -// case let .negativeInt(algorithmNegative) = algCBOR, -// let alg = COSEAlgorithmIdentifier(rawValue: -1 - Int(algorithmNegative)) else { -// throw PackedAttestationError.invalidAlg -// } -// guard let sigCBOR = attStmt["sig"], case let .byteString(sig) = sigCBOR else { -// throw PackedAttestationError.invalidSig -// } + if let x5cCBOR = attStmt["x5c"] { + guard case let .array(x5cCBOR) = x5cCBOR else { + throw PackedAttestationError.invalidX5C + } -// let verificationData = authenticatorData + clientDataHash + let x5c: [Certificate] = try x5cCBOR.map { + guard case let .byteString(certificate) = $0 else { + throw PackedAttestationError.invalidX5C + } + return try Certificate(derEncoded: certificate) + } + guard let leafCertificate = x5c.first else { throw PackedAttestationError.invalidX5C } + let intermediates = CertificateStore(x5c[1...]) + let rootCertificates = CertificateStore( + try pemRootCertificates.map { try Certificate(derEncoded: [UInt8]($0)) } + ) -// if let x5cCBOR = attStmt["x5c"] { -// guard case let .array(x5cCBOR) = x5cCBOR else { -// throw PackedAttestationError.invalidX5C -// } + var verifier = Verifier(rootCertificates: rootCertificates) { + // TODO: do we really want to validate a cert expiry for devices that cannot be updated? + // An expired device cert just means that the device is "old". + RFC5280Policy(validationTime: Date()) + } + let verifierResult: VerificationResult = await verifier.validate( + leafCertificate: leafCertificate, + intermediates: intermediates + ) + guard case .validCertificate = verifierResult else { + throw PackedAttestationError.invalidLeafCertificate + } -// let x5c: [Certificate] = try x5cCBOR.map { -// guard case let .byteString(certificate) = $0 else { -// throw PackedAttestationError.invalidX5C -// } -// return try Certificate(derEncoded: certificate) -// } -// guard let leafCertificate = x5c.first else { throw PackedAttestationError.invalidX5C } -// let intermediates = CertificateStore(x5c[1...]) -// let rootCertificates = CertificateStore( -// try pemRootCertificates.map { try Certificate(derEncoded: [UInt8]($0)) } -// ) + // 2. Verify signature + // 2.1 Determine key type (with new Swift ASN.1/ Certificates library) + // 2.2 Create corresponding public key object (EC2PublicKey/RSAPublicKey/OKPPublicKey) + // 2.3 Call verify method on public key with signature + data + let leafCertificatePublicKey: Certificate.PublicKey = leafCertificate.publicKey + guard try leafCertificatePublicKey.verifySignature(Data(sig), algorithm: leafCertificate.signatureAlgorithm, data: verificationData) else { + throw PackedAttestationError.invalidVerificationData + } -// var verifier = Verifier(rootCertificates: rootCertificates, policy: .init(policies: [])) -// let verifierResult: VerificationResult = await verifier.validate( -// leafCertificate: leafCertificate, -// intermediates: intermediates -// ) -// guard case .validCertificate = verifierResult else { -// throw PackedAttestationError.invalidLeafCertificate -// } + } else { // self attestation is in use + guard credentialPublicKey.key.algorithm == alg else { + throw PackedAttestationError.algDoesNotMatch + } -// // 2. Verify signature -// // let leafCertificatePublicKey: Certificate.PublicKey = leafCertificate.publicKey + try credentialPublicKey.verify(signature: Data(sig), data: verificationData) + } + } +} -// // 2.1 Determine key type (with new Swift ASN.1/ Certificates library) -// // 2.2 Create corresponding public key object (EC2PublicKey/RSAPublicKey/OKPPublicKey) -// // 2.3 Call verify method on public key with signature + data -// throw PackedAttestationError.notImplemented -// } else { // self attestation is in use -// guard credentialPublicKey.key.algorithm == alg else { -// throw PackedAttestationError.algDoesNotMatch -// } -// try credentialPublicKey.verify(signature: Data(sig), data: verificationData) -// } -// } -// } +extension Certificate.PublicKey { + func verifySignature(_ signature: Data, algorithm: Certificate.SignatureAlgorithm, data: Data) throws -> Bool { + switch algorithm { -// extension Certificate.PublicKey { -// // func verifySignature(_ signature: Data, algorithm: COSEAlgorithmIdentifier, data: Data) throws -> Bool { -// // switch algorithm { -// // -// // case .algES256: -// // guard case let .p256(key) = backing else { return false } -// // let signature = try P256.Signing.ECDSASignature(derRepresentation: signature) -// // return key.isValidSignature(signature, for: data) -// // case .algES384: -// // guard case let .p384(key) = backing else { return false } -// // let signature = try P384.Signing.ECDSASignature(derRepresentation: signature) -// // return key.isValidSignature(signature, for: data) -// // case .algES512: -// // guard case let .p521(key) = backing else { return false } -// // let signature = try P521.Signing.ECDSASignature(derRepresentation: signature) -// // return key.isValidSignature(signature, for: data) -// // case .algPS256: -// // case .algPS384: -// // case .algPS512: -// // case .algRS1: -// // case .algRS256: -// // case .algRS384: -// // case .algRS512: -// //} -// // switch backing { -// // case let .p256(key): -// // try EC2PublicKey(rawRepresentation: key.rawRepresentation, algorithm: algorithm) -// // .verify(signature: signature, data: data) -// // case let .p384(key): -// // try EC2PublicKey(rawRepresentation: key.rawRepresentation, algorithm: algorithm) -// // .verify(signature: signature, data: data) -// // case let .p521(key): -// // try EC2PublicKey(rawRepresentation: key.rawRepresentation, algorithm: algorithm) -// // .verify(signature: signature, data: data) -// // case let .rsa(key): -// // try RSAPublicKeyData(rawRepresentation: key.derRepresentation, algorithm: algorithm) -// // .verify(signature: signature, data: data) -// // } -// // } -// } + case .ecdsaWithSHA256: + guard let key = P256.Signing.PublicKey(self) else { + return false + } + let signature = try P256.Signing.ECDSASignature(derRepresentation: signature) + return key.isValidSignature(signature, for: data) + case .ecdsaWithSHA384: + guard let key = P384.Signing.PublicKey(self) else { + return false + } + let signature = try P384.Signing.ECDSASignature(derRepresentation: signature) + return key.isValidSignature(signature, for: data) + case .ecdsaWithSHA512: + guard let key = P521.Signing.PublicKey(self) else { + return false + } + let signature = try P521.Signing.ECDSASignature(derRepresentation: signature) + return key.isValidSignature(signature, for: data) + case .sha1WithRSAEncryption, .sha256WithRSAEncryption, .sha384WithRSAEncryption, .sha512WithRSAEncryption: + guard let key = _RSA.Signing.PublicKey(self) else { + return false + } + let signature = _RSA.Signing.RSASignature(rawRepresentation: signature) + return key.isValidSignature(signature, for: data) + default: // Should we return more explicit info (signature alg not supported) in that case? + return false + } + } +} diff --git a/Sources/WebAuthn/WebAuthnManager.swift b/Sources/WebAuthn/WebAuthnManager.swift index b2597982..695ffba3 100644 --- a/Sources/WebAuthn/WebAuthnManager.swift +++ b/Sources/WebAuthn/WebAuthnManager.swift @@ -119,6 +119,7 @@ public struct WebAuthnManager { signCount: parsedData.response.attestationObject.authenticatorData.counter, backupEligible: parsedData.response.attestationObject.authenticatorData.flags.isBackupEligible, isBackedUp: parsedData.response.attestationObject.authenticatorData.flags.isCurrentlyBackedUp, + aaguid: parsedData.response.attestationObject.authenticatorData.attestedData?.aaguid, attestationObject: parsedData.response.attestationObject, attestationClientDataJSON: parsedData.response.clientData ) From d40d26f0a42e93f47cba7dbe5ef953dd6cd05611 Mon Sep 17 00:00:00 2001 From: Matthieu Barthelemy Date: Sat, 27 Apr 2024 13:44:31 +1000 Subject: [PATCH 02/30] WIP --- .../Registration/AttestationObject.swift | 10 ++ .../Formats/FidoU2FAttestation.swift | 107 ++++++++++++++++++ .../Formats/PackedAttestation.swift | 3 + 3 files changed, 120 insertions(+) create mode 100644 Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2FAttestation.swift diff --git a/Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift b/Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift index be5242dc..1025af10 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift @@ -30,6 +30,7 @@ public struct AttestationObject { supportedPublicKeyAlgorithms: [PublicKeyCredentialParameters], pemRootCertificatesByFormat: [AttestationFormat: [Data]] = [:] ) async throws -> AttestedCredentialData { + print("\n•••••••• \(Self.self).verify(): format=\(format) ***\n") let relyingPartyIDHash = SHA256.hash(data: relyingPartyID.data(using: .utf8)!) guard relyingPartyIDHash == authenticatorData.relyingPartyIDHash else { @@ -80,6 +81,15 @@ public struct AttestationObject { // credentialPublicKey: credentialPublicKey, // pemRootCertificates: pemRootCertificates // ) + + case .fidoU2F: + try await FidoU2FAttestation.verify( + attStmt: attestationStatement, + authenticatorData: Data(rawAuthenticatorData), + clientDataHash: Data(clientDataHash), + credentialPublicKey: credentialPublicKey, + pemRootCertificates: pemRootCertificates + ) default: throw WebAuthnError.attestationVerificationNotSupported } diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2FAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2FAttestation.swift new file mode 100644 index 00000000..ba8b2ea8 --- /dev/null +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2FAttestation.swift @@ -0,0 +1,107 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the WebAuthn Swift open source project +// +// Copyright (c) 2023 the WebAuthn Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of WebAuthn Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation +import SwiftCBOR +import SwiftASN1 +import X509 +import Crypto + +struct FidoU2FAttestation { + enum FidoU2FAttestationError: Error { + case invalidAlg + case invalidSig + case invalidX5C + case invalidLeafCertificate + case invalidLeafCertificatePublicKey + case missingAttestationCertificate + case algDoesNotMatch + case missingAttestedCredential + // Authenticator data cannot be verified + case invalidVerificationData + case notImplemented + } + + static func verify( + attStmt: CBOR, + authenticatorData: Data, + clientDataHash: Data, + credentialPublicKey: CredentialPublicKey, + pemRootCertificates: [Data] + ) async throws { + print("\n••••••• attStmt[alg]=\(attStmt["alg"])") + guard let algCBOR = attStmt["alg"] else { + throw FidoU2FAttestationError.invalidAlg + //case let .negativeInt(algorithmNegative) = algCBOR, + //let alg = COSEAlgorithmIdentifier(rawValue: -1 - Int(algorithmNegative)) else { + + } + guard let sigCBOR = attStmt["sig"], case let .byteString(sig) = sigCBOR else { + throw FidoU2FAttestationError.invalidSig + } + + let verificationData = authenticatorData + clientDataHash + + if let x5cCBOR = attStmt["x5c"] { + print("\n ••••••• Full attestation!!!!!! ••••••• \n") + guard case let .array(x5cCBOR) = x5cCBOR else { + throw FidoU2FAttestationError.invalidX5C + } + + let x5c: [Certificate] = try x5cCBOR.map { + guard case let .byteString(certificate) = $0 else { + throw FidoU2FAttestationError.invalidX5C + } + return try Certificate(derEncoded: certificate) + } + guard let leafCertificate = x5c.first else { throw FidoU2FAttestationError.invalidX5C } + let intermediates = CertificateStore(x5c[1...]) + let rootCertificates = CertificateStore( + try pemRootCertificates.map { try Certificate(derEncoded: [UInt8]($0)) } + ) + + var verifier = Verifier(rootCertificates: rootCertificates) { + // TODO: do we really want to validate a cert expiry for devices that cannot be updated? + // An expired device cert just means that the device is "old". + RFC5280Policy(validationTime: Date()) + } + let verifierResult: VerificationResult = await verifier.validate( + leafCertificate: leafCertificate, + intermediates: intermediates + ) + + guard case .validCertificate = verifierResult else { + throw FidoU2FAttestationError.invalidLeafCertificate + } + + // 2. Verify signature + // 2.1 Determine key type (with new Swift ASN.1/ Certificates library) + // 2.2 Create corresponding public key object (EC2PublicKey/RSAPublicKey/OKPPublicKey) + // 2.3 Call verify method on public key with signature + data + let leafCertificatePublicKey: Certificate.PublicKey = leafCertificate.publicKey + guard try leafCertificatePublicKey.verifySignature(Data(sig), algorithm: leafCertificate.signatureAlgorithm, data: verificationData) else { + throw FidoU2FAttestationError.invalidVerificationData + } + + } else { // self attestation is in use + print("\n ••••••• Self attestation!!!!!! ••••••• \n") + /*guard credentialPublicKey.key.algorithm == alg else { + throw PackedAttestationError.algDoesNotMatch + }*/ + + try credentialPublicKey.verify(signature: Data(sig), data: verificationData) + } + } +} + diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedAttestation.swift index 28e2c0c9..3dc06f61 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedAttestation.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedAttestation.swift @@ -53,6 +53,7 @@ struct PackedAttestation { let verificationData = authenticatorData + clientDataHash if let x5cCBOR = attStmt["x5c"] { + print("\n ••••••• Full attestation!!!!!! ••••••• \n") guard case let .array(x5cCBOR) = x5cCBOR else { throw PackedAttestationError.invalidX5C } @@ -78,6 +79,7 @@ struct PackedAttestation { leafCertificate: leafCertificate, intermediates: intermediates ) + guard case .validCertificate = verifierResult else { throw PackedAttestationError.invalidLeafCertificate } @@ -92,6 +94,7 @@ struct PackedAttestation { } } else { // self attestation is in use + print("\n ••••••• Self attestation!!!!!! ••••••• \n") guard credentialPublicKey.key.algorithm == alg else { throw PackedAttestationError.algDoesNotMatch } From f399a3f6b29bad869e0e62f5e0fa9afe30b698d7 Mon Sep 17 00:00:00 2001 From: Matthieu Barthelemy Date: Sat, 27 Apr 2024 18:11:20 +1000 Subject: [PATCH 03/30] FidoU2FAttestation --- Package.swift | 2 + .../Registration/AttestationObject.swift | 2 +- .../Formats/FidoU2FAttestation.swift | 99 +++++++++---------- 3 files changed, 51 insertions(+), 52 deletions(-) diff --git a/Package.swift b/Package.swift index 53962bc7..0590044f 100644 --- a/Package.swift +++ b/Package.swift @@ -26,6 +26,7 @@ let package = Package( dependencies: [ .package(url: "https://github.com/unrelentingtech/SwiftCBOR.git", from: "0.4.7"), .package(url: "https://github.com/apple/swift-crypto.git", "2.0.0" ..< "4.0.0"), + .package(url: "https://github.com/apple/swift-certificates.git", from: "1.0.0"), .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"), .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.1.0") ], @@ -36,6 +37,7 @@ let package = Package( "SwiftCBOR", .product(name: "Crypto", package: "swift-crypto"), .product(name: "_CryptoExtras", package: "swift-crypto"), + .product(name: "X509", package: "swift-certificates"), .product(name: "Logging", package: "swift-log"), ] ), diff --git a/Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift b/Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift index 1025af10..10ff6efa 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift @@ -85,7 +85,7 @@ public struct AttestationObject { case .fidoU2F: try await FidoU2FAttestation.verify( attStmt: attestationStatement, - authenticatorData: Data(rawAuthenticatorData), + authenticatorData: authenticatorData, clientDataHash: Data(clientDataHash), credentialPublicKey: credentialPublicKey, pemRootCertificates: pemRootCertificates diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2FAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2FAttestation.swift index ba8b2ea8..0a557ff5 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2FAttestation.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2FAttestation.swift @@ -27,6 +27,7 @@ struct FidoU2FAttestation { case invalidLeafCertificatePublicKey case missingAttestationCertificate case algDoesNotMatch + case invalidAttestationKeyType case missingAttestedCredential // Authenticator data cannot be verified case invalidVerificationData @@ -35,73 +36,69 @@ struct FidoU2FAttestation { static func verify( attStmt: CBOR, - authenticatorData: Data, + authenticatorData: AuthenticatorData, clientDataHash: Data, credentialPublicKey: CredentialPublicKey, pemRootCertificates: [Data] ) async throws { - print("\n••••••• attStmt[alg]=\(attStmt["alg"])") - guard let algCBOR = attStmt["alg"] else { - throw FidoU2FAttestationError.invalidAlg - //case let .negativeInt(algorithmNegative) = algCBOR, - //let alg = COSEAlgorithmIdentifier(rawValue: -1 - Int(algorithmNegative)) else { - - } guard let sigCBOR = attStmt["sig"], case let .byteString(sig) = sigCBOR else { throw FidoU2FAttestationError.invalidSig } + + let credentialId = authenticatorData.attestedData!.credentialID + let publicKeyBytes = authenticatorData.attestedData!.publicKey - let verificationData = authenticatorData + clientDataHash + guard case let .ec2(key) = credentialPublicKey else { + throw FidoU2FAttestationError.invalidAttestationKeyType + } + + // With U2F, the public key format used when calculating the signature (`sig`) was encoded in ANSI X9.62 format + let ansiPublicKey = [0x04] + key.xCoordinate + key.yCoordinate + // https://fidoalliance.org/specs/fido-u2f-v1.1-id-20160915/fido-u2f-raw-message-formats-v1.1-id-20160915.html#registration-response-message-success + let verificationData = Data([0x00] + authenticatorData.relyingPartyIDHash + Array(clientDataHash) + credentialId + ansiPublicKey) - if let x5cCBOR = attStmt["x5c"] { - print("\n ••••••• Full attestation!!!!!! ••••••• \n") - guard case let .array(x5cCBOR) = x5cCBOR else { + guard let x5cCBOR = attStmt["x5c"], case let .array(x5cCBOR) = x5cCBOR else { throw FidoU2FAttestationError.invalidX5C - } - - let x5c: [Certificate] = try x5cCBOR.map { - guard case let .byteString(certificate) = $0 else { - throw FidoU2FAttestationError.invalidX5C - } - return try Certificate(derEncoded: certificate) - } - guard let leafCertificate = x5c.first else { throw FidoU2FAttestationError.invalidX5C } - let intermediates = CertificateStore(x5c[1...]) - let rootCertificates = CertificateStore( - try pemRootCertificates.map { try Certificate(derEncoded: [UInt8]($0)) } - ) - - var verifier = Verifier(rootCertificates: rootCertificates) { - // TODO: do we really want to validate a cert expiry for devices that cannot be updated? - // An expired device cert just means that the device is "old". - RFC5280Policy(validationTime: Date()) - } - let verifierResult: VerificationResult = await verifier.validate( - leafCertificate: leafCertificate, - intermediates: intermediates - ) + } - guard case .validCertificate = verifierResult else { - throw FidoU2FAttestationError.invalidLeafCertificate + let x5c: [Certificate] = try x5cCBOR.map { + guard case let .byteString(certificate) = $0 else { + throw FidoU2FAttestationError.invalidX5C } + return try Certificate(derEncoded: certificate) + } - // 2. Verify signature - // 2.1 Determine key type (with new Swift ASN.1/ Certificates library) - // 2.2 Create corresponding public key object (EC2PublicKey/RSAPublicKey/OKPPublicKey) - // 2.3 Call verify method on public key with signature + data - let leafCertificatePublicKey: Certificate.PublicKey = leafCertificate.publicKey - guard try leafCertificatePublicKey.verifySignature(Data(sig), algorithm: leafCertificate.signatureAlgorithm, data: verificationData) else { - throw FidoU2FAttestationError.invalidVerificationData - } + guard let leafCertificate = x5c.first else { throw FidoU2FAttestationError.invalidX5C } + let intermediates = CertificateStore(x5c[1...]) + let rootCertificates = CertificateStore( + try pemRootCertificates.map { try Certificate(derEncoded: [UInt8]($0)) } + ) + + /*var verifier = Verifier(rootCertificates: rootCertificates) { + // TODO: do we really want to validate a cert expiry for devices that cannot be updated? + // An expired device cert just means that the device is "old". + RFC5280Policy(validationTime: Date()) + } + let verifierResult: VerificationResult = await verifier.validate( + leafCertificate: leafCertificate, + intermediates: intermediates + ) - } else { // self attestation is in use - print("\n ••••••• Self attestation!!!!!! ••••••• \n") - /*guard credentialPublicKey.key.algorithm == alg else { - throw PackedAttestationError.algDoesNotMatch - }*/ + guard case .validCertificate = verifierResult else { + throw FidoU2FAttestationError.invalidLeafCertificate + }*/ - try credentialPublicKey.verify(signature: Data(sig), data: verificationData) + // 2. Verify signature + // 2.1 Determine key type (with new Swift ASN.1/ Certificates library) + // 2.2 Create corresponding public key object (EC2PublicKey/RSAPublicKey/OKPPublicKey) + // 2.3 Call verify method on public key with signature + data + let leafCertificatePublicKey: Certificate.PublicKey = leafCertificate.publicKey + guard try leafCertificatePublicKey.verifySignature(Data(sig), algorithm: .ecdsaWithSHA256, data: verificationData) else { + throw FidoU2FAttestationError.invalidVerificationData } + print("\n••••• Verified FidoU2FAttestation !!!! ••••") + + } } From bce79f02cd0c3e249abf350d772647a767c34255 Mon Sep 17 00:00:00 2001 From: Matthieu Barthelemy Date: Sat, 27 Apr 2024 18:59:04 +1000 Subject: [PATCH 04/30] Cleanuo --- .../Registration/AttestationObject.swift | 1 + .../Formats/FidoU2FAttestation.swift | 18 ++++++------------ .../Formats/PackedAttestation.swift | 6 ------ 3 files changed, 7 insertions(+), 18 deletions(-) diff --git a/Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift b/Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift index 10ff6efa..016b06ce 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift @@ -82,6 +82,7 @@ public struct AttestationObject { // pemRootCertificates: pemRootCertificates // ) + // Legacy format used mostly by older authenticators case .fidoU2F: try await FidoU2FAttestation.verify( attStmt: attestationStatement, diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2FAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2FAttestation.swift index 0a557ff5..47830263 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2FAttestation.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2FAttestation.swift @@ -14,24 +14,18 @@ import Foundation import SwiftCBOR -import SwiftASN1 import X509 import Crypto struct FidoU2FAttestation { enum FidoU2FAttestationError: Error { - case invalidAlg case invalidSig case invalidX5C case invalidLeafCertificate - case invalidLeafCertificatePublicKey - case missingAttestationCertificate - case algDoesNotMatch case invalidAttestationKeyType case missingAttestedCredential // Authenticator data cannot be verified case invalidVerificationData - case notImplemented } static func verify( @@ -45,8 +39,9 @@ struct FidoU2FAttestation { throw FidoU2FAttestationError.invalidSig } - let credentialId = authenticatorData.attestedData!.credentialID - let publicKeyBytes = authenticatorData.attestedData!.publicKey + guard let attestedData = authenticatorData.attestedData else { + throw FidoU2FAttestationError.missingAttestedCredential + } guard case let .ec2(key) = credentialPublicKey else { throw FidoU2FAttestationError.invalidAttestationKeyType @@ -55,7 +50,7 @@ struct FidoU2FAttestation { // With U2F, the public key format used when calculating the signature (`sig`) was encoded in ANSI X9.62 format let ansiPublicKey = [0x04] + key.xCoordinate + key.yCoordinate // https://fidoalliance.org/specs/fido-u2f-v1.1-id-20160915/fido-u2f-raw-message-formats-v1.1-id-20160915.html#registration-response-message-success - let verificationData = Data([0x00] + authenticatorData.relyingPartyIDHash + Array(clientDataHash) + credentialId + ansiPublicKey) + let verificationData = Data([0x00] + authenticatorData.relyingPartyIDHash + Array(clientDataHash) + attestedData.credentialID + ansiPublicKey) guard let x5cCBOR = attStmt["x5c"], case let .array(x5cCBOR) = x5cCBOR else { throw FidoU2FAttestationError.invalidX5C @@ -74,7 +69,7 @@ struct FidoU2FAttestation { try pemRootCertificates.map { try Certificate(derEncoded: [UInt8]($0)) } ) - /*var verifier = Verifier(rootCertificates: rootCertificates) { + var verifier = Verifier(rootCertificates: rootCertificates) { // TODO: do we really want to validate a cert expiry for devices that cannot be updated? // An expired device cert just means that the device is "old". RFC5280Policy(validationTime: Date()) @@ -83,10 +78,9 @@ struct FidoU2FAttestation { leafCertificate: leafCertificate, intermediates: intermediates ) - guard case .validCertificate = verifierResult else { throw FidoU2FAttestationError.invalidLeafCertificate - }*/ + } // 2. Verify signature // 2.1 Determine key type (with new Swift ASN.1/ Certificates library) diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedAttestation.swift index 3dc06f61..597b5431 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedAttestation.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedAttestation.swift @@ -14,7 +14,6 @@ import Foundation import SwiftCBOR -import SwiftASN1 import X509 import Crypto import _CryptoExtras @@ -25,13 +24,10 @@ struct PackedAttestation { case invalidSig case invalidX5C case invalidLeafCertificate - case invalidLeafCertificatePublicKey - case missingAttestationCertificate case algDoesNotMatch case missingAttestedCredential // Authenticator data cannot be verified case invalidVerificationData - case notImplemented } static func verify( @@ -53,7 +49,6 @@ struct PackedAttestation { let verificationData = authenticatorData + clientDataHash if let x5cCBOR = attStmt["x5c"] { - print("\n ••••••• Full attestation!!!!!! ••••••• \n") guard case let .array(x5cCBOR) = x5cCBOR else { throw PackedAttestationError.invalidX5C } @@ -79,7 +74,6 @@ struct PackedAttestation { leafCertificate: leafCertificate, intermediates: intermediates ) - guard case .validCertificate = verifierResult else { throw PackedAttestationError.invalidLeafCertificate } From a316f131c3a4ea47a01d58e0c1680dd85a77cd8f Mon Sep 17 00:00:00 2001 From: Matthieu Barthelemy Date: Sat, 27 Apr 2024 19:04:18 +1000 Subject: [PATCH 05/30] Cleanuo --- .../Formats/FidoU2FAttestation.swift | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2FAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2FAttestation.swift index 47830263..5d1df865 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2FAttestation.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2FAttestation.swift @@ -46,11 +46,6 @@ struct FidoU2FAttestation { guard case let .ec2(key) = credentialPublicKey else { throw FidoU2FAttestationError.invalidAttestationKeyType } - - // With U2F, the public key format used when calculating the signature (`sig`) was encoded in ANSI X9.62 format - let ansiPublicKey = [0x04] + key.xCoordinate + key.yCoordinate - // https://fidoalliance.org/specs/fido-u2f-v1.1-id-20160915/fido-u2f-raw-message-formats-v1.1-id-20160915.html#registration-response-message-success - let verificationData = Data([0x00] + authenticatorData.relyingPartyIDHash + Array(clientDataHash) + attestedData.credentialID + ansiPublicKey) guard let x5cCBOR = attStmt["x5c"], case let .array(x5cCBOR) = x5cCBOR else { throw FidoU2FAttestationError.invalidX5C @@ -82,10 +77,19 @@ struct FidoU2FAttestation { throw FidoU2FAttestationError.invalidLeafCertificate } - // 2. Verify signature - // 2.1 Determine key type (with new Swift ASN.1/ Certificates library) - // 2.2 Create corresponding public key object (EC2PublicKey/RSAPublicKey/OKPPublicKey) - // 2.3 Call verify method on public key with signature + data + // With U2F, the public key format used when calculating the signature (`sig`) was encoded in ANSI X9.62 format + let ansiPublicKey = [0x04] + key.xCoordinate + key.yCoordinate + + // https://fidoalliance.org/specs/fido-u2f-v1.1-id-20160915/fido-u2f-raw-message-formats-v1.1-id-20160915.html#registration-response-message-success + let verificationData = Data( + [0x00] + + authenticatorData.relyingPartyIDHash + + Array(clientDataHash) + + attestedData.credentialID + + ansiPublicKey + ) + + // Verify signature let leafCertificatePublicKey: Certificate.PublicKey = leafCertificate.publicKey guard try leafCertificatePublicKey.verifySignature(Data(sig), algorithm: .ecdsaWithSHA256, data: verificationData) else { throw FidoU2FAttestationError.invalidVerificationData From c0b9c7d1f4457f9fc2fd7f986e928f0c24b5c8e6 Mon Sep 17 00:00:00 2001 From: Matthieu Barthelemy Date: Sun, 28 Apr 2024 07:17:47 +1000 Subject: [PATCH 06/30] Move verifySignature to dedicated extension file --- .../Formats/FidoU2FAttestation.swift | 8 +-- .../Formats/PackedAttestation.swift | 39 +------------- .../Formats/PublicKey+verifySignature.swift | 52 +++++++++++++++++++ 3 files changed, 55 insertions(+), 44 deletions(-) create mode 100644 Sources/WebAuthn/Ceremonies/Registration/Formats/PublicKey+verifySignature.swift diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2FAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2FAttestation.swift index 5d1df865..af5aebaf 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2FAttestation.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2FAttestation.swift @@ -15,7 +15,6 @@ import Foundation import SwiftCBOR import X509 -import Crypto struct FidoU2FAttestation { enum FidoU2FAttestationError: Error { @@ -63,7 +62,7 @@ struct FidoU2FAttestation { let rootCertificates = CertificateStore( try pemRootCertificates.map { try Certificate(derEncoded: [UInt8]($0)) } ) - + var verifier = Verifier(rootCertificates: rootCertificates) { // TODO: do we really want to validate a cert expiry for devices that cannot be updated? // An expired device cert just means that the device is "old". @@ -82,7 +81,7 @@ struct FidoU2FAttestation { // https://fidoalliance.org/specs/fido-u2f-v1.1-id-20160915/fido-u2f-raw-message-formats-v1.1-id-20160915.html#registration-response-message-success let verificationData = Data( - [0x00] + [0x00] // A byte "reserved for future use" with the value 0x00. + authenticatorData.relyingPartyIDHash + Array(clientDataHash) + attestedData.credentialID @@ -94,9 +93,6 @@ struct FidoU2FAttestation { guard try leafCertificatePublicKey.verifySignature(Data(sig), algorithm: .ecdsaWithSHA256, data: verificationData) else { throw FidoU2FAttestationError.invalidVerificationData } - print("\n••••• Verified FidoU2FAttestation !!!! ••••") - - } } diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedAttestation.swift index 597b5431..0aaf4544 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedAttestation.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedAttestation.swift @@ -15,8 +15,6 @@ import Foundation import SwiftCBOR import X509 -import Crypto -import _CryptoExtras struct PackedAttestation { enum PackedAttestationError: Error { @@ -77,7 +75,7 @@ struct PackedAttestation { guard case .validCertificate = verifierResult else { throw PackedAttestationError.invalidLeafCertificate } - + // 2. Verify signature // 2.1 Determine key type (with new Swift ASN.1/ Certificates library) // 2.2 Create corresponding public key object (EC2PublicKey/RSAPublicKey/OKPPublicKey) @@ -97,38 +95,3 @@ struct PackedAttestation { } } } - - -extension Certificate.PublicKey { - func verifySignature(_ signature: Data, algorithm: Certificate.SignatureAlgorithm, data: Data) throws -> Bool { - switch algorithm { - - case .ecdsaWithSHA256: - guard let key = P256.Signing.PublicKey(self) else { - return false - } - let signature = try P256.Signing.ECDSASignature(derRepresentation: signature) - return key.isValidSignature(signature, for: data) - case .ecdsaWithSHA384: - guard let key = P384.Signing.PublicKey(self) else { - return false - } - let signature = try P384.Signing.ECDSASignature(derRepresentation: signature) - return key.isValidSignature(signature, for: data) - case .ecdsaWithSHA512: - guard let key = P521.Signing.PublicKey(self) else { - return false - } - let signature = try P521.Signing.ECDSASignature(derRepresentation: signature) - return key.isValidSignature(signature, for: data) - case .sha1WithRSAEncryption, .sha256WithRSAEncryption, .sha384WithRSAEncryption, .sha512WithRSAEncryption: - guard let key = _RSA.Signing.PublicKey(self) else { - return false - } - let signature = _RSA.Signing.RSASignature(rawRepresentation: signature) - return key.isValidSignature(signature, for: data) - default: // Should we return more explicit info (signature alg not supported) in that case? - return false - } - } -} diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/PublicKey+verifySignature.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/PublicKey+verifySignature.swift new file mode 100644 index 00000000..90bfd06f --- /dev/null +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/PublicKey+verifySignature.swift @@ -0,0 +1,52 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the WebAuthn Swift open source project +// +// Copyright (c) 2023 the WebAuthn Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of WebAuthn Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation +import X509 +import Crypto +import _CryptoExtras + +extension Certificate.PublicKey { + func verifySignature(_ signature: Data, algorithm: Certificate.SignatureAlgorithm, data: Data) throws -> Bool { + switch algorithm { + + case .ecdsaWithSHA256: + guard let key = P256.Signing.PublicKey(self) else { + return false + } + let signature = try P256.Signing.ECDSASignature(derRepresentation: signature) + return key.isValidSignature(signature, for: data) + case .ecdsaWithSHA384: + guard let key = P384.Signing.PublicKey(self) else { + return false + } + let signature = try P384.Signing.ECDSASignature(derRepresentation: signature) + return key.isValidSignature(signature, for: data) + case .ecdsaWithSHA512: + guard let key = P521.Signing.PublicKey(self) else { + return false + } + let signature = try P521.Signing.ECDSASignature(derRepresentation: signature) + return key.isValidSignature(signature, for: data) + case .sha1WithRSAEncryption, .sha256WithRSAEncryption, .sha384WithRSAEncryption, .sha512WithRSAEncryption: + guard let key = _RSA.Signing.PublicKey(self) else { + return false + } + let signature = _RSA.Signing.RSASignature(rawRepresentation: signature) + return key.isValidSignature(signature, for: data) + default: // Should we return more explicit info (signature alg not supported) in that case? + return false + } + } +} From 9733363c6986247d1d34406182a68d1e32a4775e Mon Sep 17 00:00:00 2001 From: Matthieu Barthelemy Date: Sun, 28 Apr 2024 13:00:24 +1000 Subject: [PATCH 07/30] AttestationResult.swift --- .../Registration/AttestationObject.swift | 19 ++++++++---- .../Registration/AttestationResult.swift | 23 +++++++++++++++ .../Ceremonies/Registration/Credential.swift | 4 +-- .../Formats/FidoU2FAttestation.swift | 29 ++++++++++++++----- .../Formats/PackedAttestation.swift | 6 ++-- .../Formats/PublicKey+verifySignature.swift | 1 + .../Registration/RegistrationCredential.swift | 6 ++-- Sources/WebAuthn/WebAuthnManager.swift | 7 ++--- 8 files changed, 71 insertions(+), 24 deletions(-) create mode 100644 Sources/WebAuthn/Ceremonies/Registration/AttestationResult.swift diff --git a/Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift b/Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift index 016b06ce..2e5c1e51 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift @@ -15,6 +15,7 @@ import Foundation import Crypto import SwiftCBOR +import X509 /// Contains the cryptographic attestation that a new key pair was created by that authenticator. public struct AttestationObject { @@ -22,6 +23,7 @@ public struct AttestationObject { let rawAuthenticatorData: [UInt8] let format: AttestationFormat let attestationStatement: CBOR + var trustPath: [Certificate] = [] func verify( relyingPartyID: String, @@ -29,7 +31,8 @@ public struct AttestationObject { clientDataHash: SHA256.Digest, supportedPublicKeyAlgorithms: [PublicKeyCredentialParameters], pemRootCertificatesByFormat: [AttestationFormat: [Data]] = [:] - ) async throws -> AttestedCredentialData { + ) async throws -> AttestationResult { + // TODO: remove print("\n•••••••• \(Self.self).verify(): format=\(format) ***\n") let relyingPartyIDHash = SHA256.hash(data: relyingPartyID.data(using: .utf8)!) @@ -58,6 +61,7 @@ public struct AttestationObject { } let pemRootCertificates = pemRootCertificatesByFormat[format] ?? [] + var trustedPath: [Certificate]! switch format { case .none: // if format is `none` statement must be empty @@ -65,7 +69,7 @@ public struct AttestationObject { throw WebAuthnError.attestationStatementMustBeEmpty } case .packed: - try await PackedAttestation.verify( + trustedPath = try await PackedAttestation.verify( attStmt: attestationStatement, authenticatorData: Data(rawAuthenticatorData), clientDataHash: Data(clientDataHash), @@ -84,7 +88,7 @@ public struct AttestationObject { // Legacy format used mostly by older authenticators case .fidoU2F: - try await FidoU2FAttestation.verify( + trustedPath = try await FidoU2FAttestation.verify( attStmt: attestationStatement, authenticatorData: authenticatorData, clientDataHash: Data(clientDataHash), @@ -94,7 +98,12 @@ public struct AttestationObject { default: throw WebAuthnError.attestationVerificationNotSupported } - - return attestedCredentialData + + return AttestationResult( + aaguid: [], + format: format, + trustChain: trustedPath, + attestedCredentialData: attestedCredentialData + ) } } diff --git a/Sources/WebAuthn/Ceremonies/Registration/AttestationResult.swift b/Sources/WebAuthn/Ceremonies/Registration/AttestationResult.swift new file mode 100644 index 00000000..8c6a67a0 --- /dev/null +++ b/Sources/WebAuthn/Ceremonies/Registration/AttestationResult.swift @@ -0,0 +1,23 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the WebAuthn Swift open source project +// +// Copyright (c) 2022 the WebAuthn Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of WebAuthn Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import X509 + +public struct AttestationResult { + public let aaguid: [UInt8]? + public let format: AttestationFormat + public let trustChain: [Certificate] + + let attestedCredentialData: AttestedCredentialData +} diff --git a/Sources/WebAuthn/Ceremonies/Registration/Credential.swift b/Sources/WebAuthn/Ceremonies/Registration/Credential.swift index 32288d9f..125c7094 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Credential.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Credential.swift @@ -40,10 +40,8 @@ public struct Credential { public let isBackedUp: Bool // MARK: Optional content - - public let aaguid: [UInt8]? - public let attestationObject: AttestationObject + public let attestationResult: AttestationResult public let attestationClientDataJSON: CollectedClientData } diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2FAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2FAttestation.swift index af5aebaf..f8b20498 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2FAttestation.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2FAttestation.swift @@ -21,6 +21,8 @@ struct FidoU2FAttestation { case invalidSig case invalidX5C case invalidLeafCertificate + // attestation cert can only have a ecdsaWithSHA256 signature + case invalidLeafCertificateSigType case invalidAttestationKeyType case missingAttestedCredential // Authenticator data cannot be verified @@ -33,7 +35,7 @@ struct FidoU2FAttestation { clientDataHash: Data, credentialPublicKey: CredentialPublicKey, pemRootCertificates: [Data] - ) async throws { + ) async throws -> [Certificate] { guard let sigCBOR = attStmt["sig"], case let .byteString(sig) = sigCBOR else { throw FidoU2FAttestationError.invalidSig } @@ -42,7 +44,7 @@ struct FidoU2FAttestation { throw FidoU2FAttestationError.missingAttestedCredential } - guard case let .ec2(key) = credentialPublicKey else { + guard case let .ec2(key) = credentialPublicKey, key.algorithm == .algES256 else { throw FidoU2FAttestationError.invalidAttestationKeyType } @@ -57,12 +59,20 @@ struct FidoU2FAttestation { return try Certificate(derEncoded: certificate) } + // U2F attestation can only have 1 certificate + guard x5c.count == 1 else { + throw FidoU2FAttestationError.invalidX5C + } + guard let leafCertificate = x5c.first else { throw FidoU2FAttestationError.invalidX5C } - let intermediates = CertificateStore(x5c[1...]) let rootCertificates = CertificateStore( try pemRootCertificates.map { try Certificate(derEncoded: [UInt8]($0)) } ) + guard leafCertificate.signatureAlgorithm == .ecdsaWithSHA256 else { + throw FidoU2FAttestationError.invalidLeafCertificateSigType + } + var verifier = Verifier(rootCertificates: rootCertificates) { // TODO: do we really want to validate a cert expiry for devices that cannot be updated? // An expired device cert just means that the device is "old". @@ -70,13 +80,13 @@ struct FidoU2FAttestation { } let verifierResult: VerificationResult = await verifier.validate( leafCertificate: leafCertificate, - intermediates: intermediates + intermediates: .init() ) - guard case .validCertificate = verifierResult else { + guard case .validCertificate(let chain) = verifierResult else { throw FidoU2FAttestationError.invalidLeafCertificate } - // With U2F, the public key format used when calculating the signature (`sig`) was encoded in ANSI X9.62 format + // With U2F, the public key used when calculating the signature (`sig`) was encoded in ANSI X9.62 format let ansiPublicKey = [0x04] + key.xCoordinate + key.yCoordinate // https://fidoalliance.org/specs/fido-u2f-v1.1-id-20160915/fido-u2f-raw-message-formats-v1.1-id-20160915.html#registration-response-message-success @@ -90,9 +100,14 @@ struct FidoU2FAttestation { // Verify signature let leafCertificatePublicKey: Certificate.PublicKey = leafCertificate.publicKey - guard try leafCertificatePublicKey.verifySignature(Data(sig), algorithm: .ecdsaWithSHA256, data: verificationData) else { + guard try leafCertificatePublicKey.verifySignature( + Data(sig), + algorithm: leafCertificate.signatureAlgorithm, + data: verificationData) else { throw FidoU2FAttestationError.invalidVerificationData } + + return chain } } diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedAttestation.swift index 0aaf4544..13532e4e 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedAttestation.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedAttestation.swift @@ -34,7 +34,7 @@ struct PackedAttestation { clientDataHash: Data, credentialPublicKey: CredentialPublicKey, pemRootCertificates: [Data] - ) async throws { + ) async throws -> [Certificate] { guard let algCBOR = attStmt["alg"], case let .negativeInt(algorithmNegative) = algCBOR, let alg = COSEAlgorithmIdentifier(rawValue: -1 - Int(algorithmNegative)) else { @@ -72,7 +72,7 @@ struct PackedAttestation { leafCertificate: leafCertificate, intermediates: intermediates ) - guard case .validCertificate = verifierResult else { + guard case .validCertificate(let chain) = verifierResult else { throw PackedAttestationError.invalidLeafCertificate } @@ -84,6 +84,7 @@ struct PackedAttestation { guard try leafCertificatePublicKey.verifySignature(Data(sig), algorithm: leafCertificate.signatureAlgorithm, data: verificationData) else { throw PackedAttestationError.invalidVerificationData } + return chain } else { // self attestation is in use print("\n ••••••• Self attestation!!!!!! ••••••• \n") @@ -92,6 +93,7 @@ struct PackedAttestation { } try credentialPublicKey.verify(signature: Data(sig), data: verificationData) + return [] } } } diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/PublicKey+verifySignature.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/PublicKey+verifySignature.swift index 90bfd06f..a966b331 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/PublicKey+verifySignature.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/PublicKey+verifySignature.swift @@ -39,6 +39,7 @@ extension Certificate.PublicKey { } let signature = try P521.Signing.ECDSASignature(derRepresentation: signature) return key.isValidSignature(signature, for: data) + // This hasn't been tested case .sha1WithRSAEncryption, .sha256WithRSAEncryption, .sha384WithRSAEncryption, .sha512WithRSAEncryption: guard let key = _RSA.Signing.PublicKey(self) else { return false diff --git a/Sources/WebAuthn/Ceremonies/Registration/RegistrationCredential.swift b/Sources/WebAuthn/Ceremonies/Registration/RegistrationCredential.swift index 30bc5816..49b28770 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/RegistrationCredential.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/RegistrationCredential.swift @@ -87,7 +87,7 @@ struct ParsedCredentialCreationResponse { relyingPartyOrigin: String, supportedPublicKeyAlgorithms: [PublicKeyCredentialParameters], pemRootCertificatesByFormat: [AttestationFormat: [Data]] - ) async throws -> AttestedCredentialData { + ) async throws -> AttestationResult { // Step 7. - 9. try response.clientData.verify( storedChallenge: storedChallenge, @@ -101,7 +101,7 @@ struct ParsedCredentialCreationResponse { // CBOR decoding happened already. Skipping Step 11. // Step 12. - 17. - let attestedCredentialData = try await response.attestationObject.verify( + let attestationResult = try await response.attestationObject.verify( relyingPartyID: relyingPartyID, verificationRequired: verifyUser, clientDataHash: hash, @@ -114,6 +114,6 @@ struct ParsedCredentialCreationResponse { throw WebAuthnError.credentialRawIDTooLong } - return attestedCredentialData + return attestationResult } } diff --git a/Sources/WebAuthn/WebAuthnManager.swift b/Sources/WebAuthn/WebAuthnManager.swift index 695ffba3..acb95e58 100644 --- a/Sources/WebAuthn/WebAuthnManager.swift +++ b/Sources/WebAuthn/WebAuthnManager.swift @@ -95,7 +95,7 @@ public struct WebAuthnManager { confirmCredentialIDNotRegisteredYet: (String) async throws -> Bool ) async throws -> Credential { let parsedData = try ParsedCredentialCreationResponse(from: credentialCreationData) - let attestedCredentialData = try await parsedData.verify( + let attestationResult = try await parsedData.verify( storedChallenge: challenge, verifyUser: requireUserVerification, relyingPartyID: configuration.relyingPartyID, @@ -115,12 +115,11 @@ public struct WebAuthnManager { return Credential( type: parsedData.type, id: parsedData.id.urlDecoded.asString(), - publicKey: attestedCredentialData.publicKey, + publicKey: attestationResult.attestedCredentialData.publicKey, signCount: parsedData.response.attestationObject.authenticatorData.counter, backupEligible: parsedData.response.attestationObject.authenticatorData.flags.isBackupEligible, isBackedUp: parsedData.response.attestationObject.authenticatorData.flags.isCurrentlyBackedUp, - aaguid: parsedData.response.attestationObject.authenticatorData.attestedData?.aaguid, - attestationObject: parsedData.response.attestationObject, + attestationResult: attestationResult, attestationClientDataJSON: parsedData.response.clientData ) } From 7275dc321b246935225495e992705400247a4027 Mon Sep 17 00:00:00 2001 From: Matthieu Barthelemy Date: Mon, 29 Apr 2024 09:57:15 +1000 Subject: [PATCH 08/30] Update TPM attestation; validate cert extension for .packed and .tpm --- .../Registration/AttestationObject.swift | 20 +- .../Registration/AttestationResult.swift | 10 + .../Formats/PackedAttestation.swift | 32 +- .../Formats/PublicKey+verifySignature.swift | 10 + .../Formats/TPMAttestation+Structs.swift | 793 +++++++++--------- .../Registration/Formats/TPMAttestation.swift | 245 +++--- 6 files changed, 604 insertions(+), 506 deletions(-) diff --git a/Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift b/Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift index 2e5c1e51..5be4a8bd 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift @@ -61,7 +61,7 @@ public struct AttestationObject { } let pemRootCertificates = pemRootCertificatesByFormat[format] ?? [] - var trustedPath: [Certificate]! + var trustedPath: [Certificate] = [] switch format { case .none: // if format is `none` statement must be empty @@ -76,15 +76,15 @@ public struct AttestationObject { credentialPublicKey: credentialPublicKey, pemRootCertificates: pemRootCertificates ) - // case .tpm: - // try TPMAttestation.verify( - // attStmt: attestationStatement, - // authenticatorData: rawAuthenticatorData, - // attestedCredentialData: attestedCredentialData, - // clientDataHash: Data(clientDataHash), - // credentialPublicKey: credentialPublicKey, - // pemRootCertificates: pemRootCertificates - // ) + case .tpm: + trustedPath = try await TPMAttestation.verify( + attStmt: attestationStatement, + authenticatorData: Data(rawAuthenticatorData), + attestedCredentialData: attestedCredentialData, + clientDataHash: Data(clientDataHash), + credentialPublicKey: credentialPublicKey, + pemRootCertificates: pemRootCertificates + ) // Legacy format used mostly by older authenticators case .fidoU2F: diff --git a/Sources/WebAuthn/Ceremonies/Registration/AttestationResult.swift b/Sources/WebAuthn/Ceremonies/Registration/AttestationResult.swift index 8c6a67a0..875ae7b1 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/AttestationResult.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/AttestationResult.swift @@ -15,8 +15,18 @@ import X509 public struct AttestationResult { + enum AttestationType { + /// Attestation key pair validated by device manufacturer CA + case basicFull + /// Attestation signed by the public key generated during the registration + case `self` + case attCA + case anonCA + case none + } public let aaguid: [UInt8]? public let format: AttestationFormat + //public let type: AttestationType public let trustChain: [Certificate] let attestedCredentialData: AttestedCredentialData diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedAttestation.swift index 13532e4e..9e127a29 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedAttestation.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedAttestation.swift @@ -15,6 +15,7 @@ import Foundation import SwiftCBOR import X509 +import SwiftASN1 struct PackedAttestation { enum PackedAttestationError: Error { @@ -26,6 +27,8 @@ struct PackedAttestation { case missingAttestedCredential // Authenticator data cannot be verified case invalidVerificationData + case invalidCertAaguid + case aaguidMismatch } static func verify( @@ -57,7 +60,7 @@ struct PackedAttestation { } return try Certificate(derEncoded: certificate) } - guard let leafCertificate = x5c.first else { throw PackedAttestationError.invalidX5C } + guard let attestnCert = x5c.first else { throw PackedAttestationError.invalidX5C } let intermediates = CertificateStore(x5c[1...]) let rootCertificates = CertificateStore( try pemRootCertificates.map { try Certificate(derEncoded: [UInt8]($0)) } @@ -69,7 +72,7 @@ struct PackedAttestation { RFC5280Policy(validationTime: Date()) } let verifierResult: VerificationResult = await verifier.validate( - leafCertificate: leafCertificate, + leafCertificate: attestnCert, intermediates: intermediates ) guard case .validCertificate(let chain) = verifierResult else { @@ -80,10 +83,31 @@ struct PackedAttestation { // 2.1 Determine key type (with new Swift ASN.1/ Certificates library) // 2.2 Create corresponding public key object (EC2PublicKey/RSAPublicKey/OKPPublicKey) // 2.3 Call verify method on public key with signature + data - let leafCertificatePublicKey: Certificate.PublicKey = leafCertificate.publicKey - guard try leafCertificatePublicKey.verifySignature(Data(sig), algorithm: leafCertificate.signatureAlgorithm, data: verificationData) else { + let leafCertificatePublicKey: Certificate.PublicKey = attestnCert.publicKey + guard try leafCertificatePublicKey.verifySignature( + Data(sig), + algorithm: attestnCert.signatureAlgorithm, + data: verificationData) else { throw PackedAttestationError.invalidVerificationData } + + // Verify that the value of the aaguid extension, if present, matches aaguid in authenticatorData + if let certAAGUID = attestnCert.extensions.first( + where: {$0.oid == .idFidoGenCeAaguid} + ) { + // The AAGUID is wrapped in two OCTET STRINGS + let derValue = try DER.parse(certAAGUID.value) + guard case .primitive(let certAaguidValue) = derValue.content else { + throw PackedAttestationError.invalidCertAaguid + } + + let authenticatorData = try AuthenticatorData(bytes: Array(authenticatorData)) + guard let attestedData = authenticatorData.attestedData, + attestedData.aaguid == Array(certAaguidValue) else { + throw PackedAttestationError.aaguidMismatch + } + } + return chain } else { // self attestation is in use diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/PublicKey+verifySignature.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/PublicKey+verifySignature.swift index a966b331..95533e77 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/PublicKey+verifySignature.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/PublicKey+verifySignature.swift @@ -14,6 +14,7 @@ import Foundation import X509 +import SwiftASN1 import Crypto import _CryptoExtras @@ -51,3 +52,12 @@ extension Certificate.PublicKey { } } } + +extension SwiftASN1.ASN1ObjectIdentifier { + static var idFidoGenCeAaguid: Self { + .init(arrayLiteral: 1, 3, 6, 1, 4, 1, 45724, 1, 1, 4) + } + static var tcgKpAIKCertificate: Self { + .init(arrayLiteral: 2, 23, 133, 8, 3) + } +} diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/TPMAttestation+Structs.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/TPMAttestation+Structs.swift index 0c75674e..8a147590 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/TPMAttestation+Structs.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/TPMAttestation+Structs.swift @@ -12,399 +12,400 @@ // //===----------------------------------------------------------------------===// -// import Foundation -// extension TPMAttestation { -// enum CertInfoError: Error { -// case magicInvalid -// case typeInvalid -// case dataTooShort -// case tpmImplementationIsWIP -// } - -// struct AttestationInformation { -// let name: Data -// let qualifiedName: Data -// } - -// struct CertInfo { -// let magic: Data -// let type: Data -// let qualifiedSigner: Data -// let extraData: Data -// let clockInfo: Data -// let firmwareVersion: Data -// let attested: AttestationInformation - -// init?(fromBytes data: Data) { -// var pointer = 0 - -// guard let magic = data[safe: pointer..<(pointer + 4)] else { return nil } -// self.magic = magic -// pointer += 4 - -// guard let type = data[safe: pointer..<(pointer + 2)] else { return nil } -// self.type = type -// pointer += 2 - -// guard let qualifiedSignerLengthData = data[safe: pointer..<(pointer + 2)] else { return nil } -// pointer += 2 -// let qualifiedSignerLength: Int = qualifiedSignerLengthData.toInteger(endian: .big) -// guard let qualifiedSigner = data[safe: pointer..<(pointer + qualifiedSignerLength)] else { return nil } -// self.qualifiedSigner = qualifiedSigner -// pointer += qualifiedSignerLength - -// guard let extraDataLengthData = data[safe: pointer..<(pointer + 2)] else { return nil } -// pointer += 2 -// let extraDataLength: Int = extraDataLengthData.toInteger(endian: .big) -// guard let extraData = data[safe: pointer..<(pointer + extraDataLength)] else { return nil } -// self.extraData = extraData -// pointer += extraDataLength - -// guard let clockInfo = data[safe: pointer..<(pointer + 17)] else { return nil } -// self.clockInfo = clockInfo -// pointer += 17 - -// guard let firmwareVersion = data[safe: pointer..<(pointer + 8)] else { return nil } -// self.firmwareVersion = firmwareVersion -// pointer += 8 - -// guard let attestedNameLengthData = data[safe: pointer..<(pointer + 2)] else { return nil } -// pointer += 2 -// let attestedNameLength: Int = attestedNameLengthData.toInteger(endian: .big) -// guard let attestedName = data[safe: pointer..<(pointer + attestedNameLength)] else { return nil } -// pointer += attestedNameLength - -// guard let qualifiedNameLengthData = data[safe: pointer..<(pointer + 2)] else { return nil } -// pointer += 2 -// let qualifiedNameLength: Int = qualifiedNameLengthData.toInteger(endian: .big) -// guard let qualifiedName = data[safe: pointer..<(pointer + qualifiedNameLength)] else { return nil } -// pointer += qualifiedNameLength - -// attested = AttestationInformation(name: attestedName, qualifiedName: qualifiedName) -// } - -// func verify() throws { -// let tpmGeneratedValue = 0xFF544347 -// guard magic.toInteger(endian: .big) == tpmGeneratedValue else { -// throw CertInfoError.magicInvalid -// } - -// let tpmStAttestCertify = 0x8017 -// guard type.toInteger(endian: .big) == tpmStAttestCertify else { -// throw CertInfoError.typeInvalid -// } - -// throw CertInfoError.tpmImplementationIsWIP -// } -// } - -// enum PubAreaParameters { -// case rsa(PubAreaParametersRSA) -// case ecc (PubAreaParametersECC) -// } - -// struct PubArea { -// let type: Data -// let nameAlg: Data -// let objectAttributes: Data -// let authPolicy: Data -// let parameters: PubAreaParameters -// let unique: PubAreaUnique - -// let mappedType: TPMAlg - -// init?(from data: Data) { -// var pointer = 0 - -// guard let type = data.safeSlice(length: 2, using: &pointer), -// let mappedType = TPMAlg(from: type), -// let nameAlg = data.safeSlice(length: 2, using: &pointer), -// let objectAttributes = data.safeSlice(length: 4, using: &pointer), -// let authPolicyLength: Int = data.safeSlice(length: 2, using: &pointer)?.toInteger(endian: .big), -// let authPolicy = data.safeSlice(length: authPolicyLength, using: &pointer) else { -// return nil -// } - -// self.type = type -// self.nameAlg = nameAlg -// self.objectAttributes = objectAttributes -// self.authPolicy = authPolicy - -// self.mappedType = mappedType - -// switch mappedType { -// case .rsa: -// guard let rsa = data.safeSlice(length: 10, using: &pointer), -// let parameters = PubAreaParametersRSA(from: rsa) else { return nil } -// self.parameters = .rsa(parameters) -// case .ecc: -// guard let ecc = data.safeSlice(length: 8, using: &pointer), -// let parameters = PubAreaParametersECC(from: ecc) else { return nil } -// self.parameters = .ecc(parameters) -// default: -// return nil -// } - -// guard data.count >= pointer, -// let unique = PubAreaUnique(from: data[pointer...], algorithm: mappedType) else { -// return nil -// } - -// self.unique = unique -// } -// } -// } - -// extension TPMAttestation { -// enum TPMAlg: String { -// case error = "TPM_ALG_ERROR" -// case rsa = "TPM_ALG_RSA" -// case sha1 = "TPM_ALG_SHA1" -// case hmac = "TPM_ALG_HMAC" -// case aes = "TPM_ALG_AES" -// case mgf1 = "TPM_ALG_MGF1" -// case keyedhash = "TPM_ALG_KEYEDHASH" -// case xor = "TPM_ALG_XOR" -// case sha256 = "TPM_ALG_SHA256" -// case sha384 = "TPM_ALG_SHA384" -// case sha512 = "TPM_ALG_SHA512" -// case null = "TPM_ALG_NULL" -// case sm3256 = "TPM_ALG_SM3_256" -// case sm4 = "TPM_ALG_SM4" -// case rsassa = "TPM_ALG_RSASSA" -// case rsaes = "TPM_ALG_RSAES" -// case rsapss = "TPM_ALG_RSAPSS" -// case oaep = "TPM_ALG_OAEP" -// case ecdsa = "TPM_ALG_ECDSA" -// case ecdh = "TPM_ALG_ECDH" -// case ecdaa = "TPM_ALG_ECDAA" -// case sm2 = "TPM_ALG_SM2" -// case ecschnorr = "TPM_ALG_ECSCHNORR" -// case ecmqv = "TPM_ALG_ECMQV" -// case kdf1Sp80056a = "TPM_ALG_KDF1_SP800_56A" -// case kdf2 = "TPM_ALG_KDF2" -// case kdf1Sp800108 = "TPM_ALG_KDF1_SP800_108" -// case ecc = "TPM_ALG_ECC" -// case symcipher = "TPM_ALG_SYMCIPHER" -// case camellia = "TPM_ALG_CAMELLIA" -// case ctr = "TPM_ALG_CTR" -// case ofb = "TPM_ALG_OFB" -// case cbc = "TPM_ALG_CBC" -// case cfb = "TPM_ALG_CFB" -// case ecb = "TPM_ALG_ECB" - -// // swiftlint:disable:next cyclomatic_complexity function_body_length -// init?(from data: Data) { -// let bytes = [UInt8](data) -// switch bytes { -// case [0x00, 0x00]: -// self = .error -// case [0x00, 0x01]: -// self = .rsa -// case [0x00, 0x04]: -// self = .sha1 -// case [0x00, 0x05]: -// self = .hmac -// case [0x00, 0x06]: -// self = .aes -// case [0x00, 0x07]: -// self = .mgf1 -// case [0x00, 0x08]: -// self = .keyedhash -// case [0x00, 0x0a]: -// self = .xor -// case [0x00, 0x0b]: -// self = .sha256 -// case [0x00, 0x0c]: -// self = .sha384 -// case [0x00, 0x0d]: -// self = .sha512 -// case [0x00, 0x10]: -// self = .null -// case [0x00, 0x12]: -// self = .sm3256 -// case [0x00, 0x13]: -// self = .sm4 -// case [0x00, 0x14]: -// self = .rsassa -// case [0x00, 0x15]: -// self = .rsaes -// case [0x00, 0x16]: -// self = .rsapss -// case [0x00, 0x17]: -// self = .oaep -// case [0x00, 0x18]: -// self = .ecdsa -// case [0x00, 0x19]: -// self = .ecdh -// case [0x00, 0x1a]: -// self = .ecdaa -// case [0x00, 0x1b]: -// self = .sm2 -// case [0x00, 0x1c]: -// self = .ecschnorr -// case [0x00, 0x1d]: -// self = .ecmqv -// case [0x00, 0x20]: -// self = .kdf1Sp80056a -// case [0x00, 0x21]: -// self = .kdf2 -// case [0x00, 0x22]: -// self = .kdf1Sp800108 -// case [0x00, 0x23]: -// self = .ecc -// case [0x00, 0x25]: -// self = .symcipher -// case [0x00, 0x26]: -// self = .camellia -// case [0x00, 0x40]: -// self = .ctr -// case [0x00, 0x41]: -// self = .ofb -// case [0x00, 0x42]: -// self = .cbc -// case [0x00, 0x43]: -// self = .cfb -// case [0x00, 0x44]: -// self = .ecb -// default: -// return nil -// } -// } -// } -// } - -// extension TPMAttestation { -// enum ECCCurve: String { -// case none = "NONE" -// case nistP192 = "NIST_P192" -// case nistP224 = "NIST_P224" -// case nistP256 = "NIST_P256" -// case nistP384 = "NIST_P384" -// case nistP521 = "NIST_P521" -// case bnP256 = "BN_P256" -// case bnP638 = "BN_P638" -// case sm2P256 = "SM2_P256" - -// init?(from data: Data) { -// let bytes = [UInt8](data) -// switch bytes { -// case [0x00, 0x00]: -// self = .none -// case [0x00, 0x01]: -// self = .nistP192 -// case [0x00, 0x02]: -// self = .nistP224 -// case [0x00, 0x03]: -// self = .nistP256 -// case [0x00, 0x04]: -// self = .nistP384 -// case [0x00, 0x05]: -// self = .nistP521 -// case [0x00, 0x10]: -// self = .bnP256 -// case [0x00, 0x11]: -// self = .bnP638 -// case [0x00, 0x20]: -// self = .sm2P256 -// default: -// return nil -// } -// } -// } -// } - -// extension TPMAttestation { -// struct PubAreaParametersRSA { -// let symmetric: TPMAlg -// let scheme: TPMAlg -// let key: Data -// let exponent: Data - -// init?(from data: Data) { -// guard let symmetricData = data[safe: 0..<2], -// let symmetric = TPMAlg(from: symmetricData), -// let schemeData = data[safe: 2..<4], -// let scheme = TPMAlg(from: schemeData), -// let key = data[safe: 4..<6], -// let exponent = data[safe: 6..<10] else { -// return nil -// } - -// self.symmetric = symmetric -// self.scheme = scheme -// self.key = key -// self.exponent = exponent -// } -// } -// } - -// extension TPMAttestation { -// struct PubAreaParametersECC { -// let symmetric: TPMAlg -// let scheme: TPMAlg -// let curveID: ECCCurve -// let kdf: TPMAlg - -// init?(from data: Data) { -// guard let symmetricData = data[safe: 0..<2], -// let symmetric = TPMAlg(from: symmetricData), -// let schemeData = data[safe: 2..<4], -// let scheme = TPMAlg(from: schemeData), -// let curveIDData = data[safe: 4..<6], -// let curveID = ECCCurve(from: curveIDData), -// let kdfData = data[safe: 6..<8], -// let kdf = TPMAlg(from: kdfData) else { -// return nil -// } - -// self.symmetric = symmetric -// self.scheme = scheme -// self.curveID = curveID -// self.kdf = kdf -// } -// } -// } - -// extension TPMAttestation { -// struct PubAreaUnique { -// let data: Data - -// init?(from data: Data, algorithm: TPMAlg) { -// switch algorithm { -// case .rsa: -// guard let uniqueLength: Int = data[safe: 0..<2]?.toInteger(endian: .big), -// let rsaUnique = data[safe: 2..<(2 + uniqueLength)] else { -// return nil -// } -// self.data = rsaUnique -// case .ecc: -// var pointer = 0 -// guard let uniqueXLength: Int = data.safeSlice(length: 2, using: &pointer)?.toInteger(endian: .big), -// let uniqueX = data.safeSlice(length: uniqueXLength, using: &pointer), -// let uniqueYLength: Int = data.safeSlice(length: 2, using: &pointer)?.toInteger(endian: .big), -// let uniqueY = data.safeSlice(length: uniqueYLength, using: &pointer) else { -// return nil -// } -// self.data = uniqueX + uniqueY -// default: -// return nil -// } -// } -// } -// } - -// extension COSECurve { -// init?(from eccCurve: TPMAttestation.ECCCurve) { -// switch eccCurve { -// case .nistP256, .bnP256, .sm2P256: -// self = .p256 -// case .nistP384: -// self = .p384 -// case .nistP521: -// self = .p521 -// default: -// return nil -// } -// } -// } \ No newline at end of file +import Foundation + +extension TPMAttestation { + enum CertInfoError: Error { + case magicInvalid + case typeInvalid + case dataTooShort + case tpmImplementationIsWIP + } + + struct AttestationInformation { + let name: Data + let qualifiedName: Data + } + + struct CertInfo { + let magic: Data + let type: Data + let qualifiedSigner: Data + let extraData: Data + let clockInfo: Data + let firmwareVersion: Data + let attested: AttestationInformation + + init?(fromBytes data: Data) { + var pointer = 0 + + guard let magic = data[safe: pointer..<(pointer + 4)] else { return nil } + self.magic = magic + pointer += 4 + + guard let type = data[safe: pointer..<(pointer + 2)] else { return nil } + self.type = type + pointer += 2 + + guard let qualifiedSignerLengthData = data[safe: pointer..<(pointer + 2)] else { return nil } + pointer += 2 + let qualifiedSignerLength: Int = qualifiedSignerLengthData.toInteger(endian: .big) + guard let qualifiedSigner = data[safe: pointer..<(pointer + qualifiedSignerLength)] else { return nil } + self.qualifiedSigner = qualifiedSigner + pointer += qualifiedSignerLength + + guard let extraDataLengthData = data[safe: pointer..<(pointer + 2)] else { return nil } + pointer += 2 + let extraDataLength: Int = extraDataLengthData.toInteger(endian: .big) + guard let extraData = data[safe: pointer..<(pointer + extraDataLength)] else { return nil } + self.extraData = extraData + pointer += extraDataLength + + guard let clockInfo = data[safe: pointer..<(pointer + 17)] else { return nil } + self.clockInfo = clockInfo + pointer += 17 + + guard let firmwareVersion = data[safe: pointer..<(pointer + 8)] else { return nil } + self.firmwareVersion = firmwareVersion + pointer += 8 + + guard let attestedNameLengthData = data[safe: pointer..<(pointer + 2)] else { return nil } + pointer += 2 + let attestedNameLength: Int = attestedNameLengthData.toInteger(endian: .big) + guard let attestedName = data[safe: pointer..<(pointer + attestedNameLength)] else { return nil } + pointer += attestedNameLength + + guard let qualifiedNameLengthData = data[safe: pointer..<(pointer + 2)] else { return nil } + pointer += 2 + let qualifiedNameLength: Int = qualifiedNameLengthData.toInteger(endian: .big) + guard let qualifiedName = data[safe: pointer..<(pointer + qualifiedNameLength)] else { return nil } + pointer += qualifiedNameLength + + attested = AttestationInformation(name: attestedName, qualifiedName: qualifiedName) + } + + func verify() throws { + let tpmGeneratedValue = 0xFF544347 + guard magic.toInteger(endian: .big) == tpmGeneratedValue else { + throw CertInfoError.magicInvalid + } + + let tpmStAttestCertify = 0x8017 + guard type.toInteger(endian: .big) == tpmStAttestCertify else { + throw CertInfoError.typeInvalid + } + + throw CertInfoError.tpmImplementationIsWIP + } + } + + enum PubAreaParameters { + case rsa(PubAreaParametersRSA) + case ecc (PubAreaParametersECC) + } + + struct PubArea { + let type: Data + let nameAlg: Data + let objectAttributes: Data + let authPolicy: Data + let parameters: PubAreaParameters + let unique: PubAreaUnique + + let mappedType: TPMAlg + + init?(from data: Data) { + var pointer = 0 + + guard let type = data.safeSlice(length: 2, using: &pointer), + let mappedType = TPMAlg(from: type), + let nameAlg = data.safeSlice(length: 2, using: &pointer), + let objectAttributes = data.safeSlice(length: 4, using: &pointer), + let authPolicyLength: Int = data.safeSlice(length: 2, using: &pointer)?.toInteger(endian: .big), + let authPolicy = data.safeSlice(length: authPolicyLength, using: &pointer) else { + return nil + } + + self.type = type + self.nameAlg = nameAlg + self.objectAttributes = objectAttributes + self.authPolicy = authPolicy + + self.mappedType = mappedType + + switch mappedType { + case .rsa: + guard let rsa = data.safeSlice(length: 10, using: &pointer), + let parameters = PubAreaParametersRSA(from: rsa) else { return nil } + self.parameters = .rsa(parameters) + case .ecc: + guard let ecc = data.safeSlice(length: 8, using: &pointer), + let parameters = PubAreaParametersECC(from: ecc) else { return nil } + self.parameters = .ecc(parameters) + default: + return nil + } + + guard data.count >= pointer, + let unique = PubAreaUnique(from: data[pointer...], algorithm: mappedType) else { + return nil + } + + self.unique = unique + } + } +} + +extension TPMAttestation { + enum TPMAlg: String { + case error = "TPM_ALG_ERROR" + case rsa = "TPM_ALG_RSA" + case sha1 = "TPM_ALG_SHA1" + case hmac = "TPM_ALG_HMAC" + case aes = "TPM_ALG_AES" + case mgf1 = "TPM_ALG_MGF1" + case keyedhash = "TPM_ALG_KEYEDHASH" + case xor = "TPM_ALG_XOR" + case sha256 = "TPM_ALG_SHA256" + case sha384 = "TPM_ALG_SHA384" + case sha512 = "TPM_ALG_SHA512" + case null = "TPM_ALG_NULL" + case sm3256 = "TPM_ALG_SM3_256" + case sm4 = "TPM_ALG_SM4" + case rsassa = "TPM_ALG_RSASSA" + case rsaes = "TPM_ALG_RSAES" + case rsapss = "TPM_ALG_RSAPSS" + case oaep = "TPM_ALG_OAEP" + case ecdsa = "TPM_ALG_ECDSA" + case ecdh = "TPM_ALG_ECDH" + case ecdaa = "TPM_ALG_ECDAA" + case sm2 = "TPM_ALG_SM2" + case ecschnorr = "TPM_ALG_ECSCHNORR" + case ecmqv = "TPM_ALG_ECMQV" + case kdf1Sp80056a = "TPM_ALG_KDF1_SP800_56A" + case kdf2 = "TPM_ALG_KDF2" + case kdf1Sp800108 = "TPM_ALG_KDF1_SP800_108" + case ecc = "TPM_ALG_ECC" + case symcipher = "TPM_ALG_SYMCIPHER" + case camellia = "TPM_ALG_CAMELLIA" + case ctr = "TPM_ALG_CTR" + case ofb = "TPM_ALG_OFB" + case cbc = "TPM_ALG_CBC" + case cfb = "TPM_ALG_CFB" + case ecb = "TPM_ALG_ECB" + + // swiftlint:disable:next cyclomatic_complexity function_body_length + init?(from data: Data) { + let bytes = [UInt8](data) + switch bytes { + case [0x00, 0x00]: + self = .error + case [0x00, 0x01]: + self = .rsa + case [0x00, 0x04]: + self = .sha1 + case [0x00, 0x05]: + self = .hmac + case [0x00, 0x06]: + self = .aes + case [0x00, 0x07]: + self = .mgf1 + case [0x00, 0x08]: + self = .keyedhash + case [0x00, 0x0a]: + self = .xor + case [0x00, 0x0b]: + self = .sha256 + case [0x00, 0x0c]: + self = .sha384 + case [0x00, 0x0d]: + self = .sha512 + case [0x00, 0x10]: + self = .null + case [0x00, 0x12]: + self = .sm3256 + case [0x00, 0x13]: + self = .sm4 + case [0x00, 0x14]: + self = .rsassa + case [0x00, 0x15]: + self = .rsaes + case [0x00, 0x16]: + self = .rsapss + case [0x00, 0x17]: + self = .oaep + case [0x00, 0x18]: + self = .ecdsa + case [0x00, 0x19]: + self = .ecdh + case [0x00, 0x1a]: + self = .ecdaa + case [0x00, 0x1b]: + self = .sm2 + case [0x00, 0x1c]: + self = .ecschnorr + case [0x00, 0x1d]: + self = .ecmqv + case [0x00, 0x20]: + self = .kdf1Sp80056a + case [0x00, 0x21]: + self = .kdf2 + case [0x00, 0x22]: + self = .kdf1Sp800108 + case [0x00, 0x23]: + self = .ecc + case [0x00, 0x25]: + self = .symcipher + case [0x00, 0x26]: + self = .camellia + case [0x00, 0x40]: + self = .ctr + case [0x00, 0x41]: + self = .ofb + case [0x00, 0x42]: + self = .cbc + case [0x00, 0x43]: + self = .cfb + case [0x00, 0x44]: + self = .ecb + default: + return nil + } + } + } +} + +extension TPMAttestation { + enum ECCCurve: String { + case none = "NONE" + case nistP192 = "NIST_P192" + case nistP224 = "NIST_P224" + case nistP256 = "NIST_P256" + case nistP384 = "NIST_P384" + case nistP521 = "NIST_P521" + case bnP256 = "BN_P256" + case bnP638 = "BN_P638" + case sm2P256 = "SM2_P256" + + init?(from data: Data) { + let bytes = [UInt8](data) + switch bytes { + case [0x00, 0x00]: + self = .none + case [0x00, 0x01]: + self = .nistP192 + case [0x00, 0x02]: + self = .nistP224 + case [0x00, 0x03]: + self = .nistP256 + case [0x00, 0x04]: + self = .nistP384 + case [0x00, 0x05]: + self = .nistP521 + case [0x00, 0x10]: + self = .bnP256 + case [0x00, 0x11]: + self = .bnP638 + case [0x00, 0x20]: + self = .sm2P256 + default: + return nil + } + } + } +} + +extension TPMAttestation { + struct PubAreaParametersRSA { + let symmetric: TPMAlg + let scheme: TPMAlg + let key: Data + let exponent: Data + + init?(from data: Data) { + guard let symmetricData = data[safe: 0..<2], + let symmetric = TPMAlg(from: symmetricData), + let schemeData = data[safe: 2..<4], + let scheme = TPMAlg(from: schemeData), + let key = data[safe: 4..<6], + let exponent = data[safe: 6..<10] else { + return nil + } + + self.symmetric = symmetric + self.scheme = scheme + self.key = key + self.exponent = exponent + } + } +} + +extension TPMAttestation { + struct PubAreaParametersECC { + let symmetric: TPMAlg + let scheme: TPMAlg + let curveID: ECCCurve + let kdf: TPMAlg + + init?(from data: Data) { + guard let symmetricData = data[safe: 0..<2], + let symmetric = TPMAlg(from: symmetricData), + let schemeData = data[safe: 2..<4], + let scheme = TPMAlg(from: schemeData), + let curveIDData = data[safe: 4..<6], + let curveID = ECCCurve(from: curveIDData), + let kdfData = data[safe: 6..<8], + let kdf = TPMAlg(from: kdfData) else { + return nil + } + + self.symmetric = symmetric + self.scheme = scheme + self.curveID = curveID + self.kdf = kdf + } + } +} + +extension TPMAttestation { + struct PubAreaUnique { + let data: Data + + init?(from data: Data, algorithm: TPMAlg) { + switch algorithm { + case .rsa: + guard let uniqueLength: Int = data[safe: 0..<2]?.toInteger(endian: .big), + let rsaUnique = data[safe: 2..<(2 + uniqueLength)] else { + return nil + } + self.data = rsaUnique + case .ecc: + var pointer = 0 + guard let uniqueXLength: Int = data.safeSlice(length: 2, using: &pointer)?.toInteger(endian: .big), + let uniqueX = data.safeSlice(length: uniqueXLength, using: &pointer), + let uniqueYLength: Int = data.safeSlice(length: 2, using: &pointer)?.toInteger(endian: .big), + let uniqueY = data.safeSlice(length: uniqueYLength, using: &pointer) else { + return nil + } + self.data = uniqueX + uniqueY + default: + return nil + } + } + } +} + +extension COSECurve { + init?(from eccCurve: TPMAttestation.ECCCurve) { + switch eccCurve { + case .nistP256, .bnP256, .sm2P256: + self = .p256 + case .nistP384: + self = .p384 + case .nistP521: + self = .p521 + default: + return nil + } + } +} diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/TPMAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/TPMAttestation.swift index 917d97fa..5117bfa0 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/TPMAttestation.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/TPMAttestation.swift @@ -12,109 +12,162 @@ // //===----------------------------------------------------------------------===// -// 🚨 WIP +import Foundation +import SwiftCBOR +import X509 +import SwiftASN1 -// import Foundation -// import SwiftCBOR +struct TPMAttestation { + enum TPMAttestationError: Error { + case pubAreaInvalid + case certInfoInvalid + case invalidAlg + case invalidVersion + case invalidX5c + case invalidPublicKey + case invalidLeafCertificate + case attestationCertificateSubjectNotEmpty + case attestationCertificateMissingTcgKpAIKCertificate + case attestationCertificateIsCA + case invalidCertAaguid + case aaguidMismatch + case pubAreaExponentDoesNotMatchPubKeyExponent + case invalidPubAreaCurve + case extraDataDoesNotMatchAttToBeSignedHash + } -// /// 🚨 WIP -// struct TPMAttestation { -// enum TPMAttestationError: Error { -// case pubAreaInvalid -// case certInfoInvalid -// case invalidAlg -// case invalidVersion -// case invalidX5c -// case invalidPublicKey -// case pubAreaExponentDoesNotMatchPubKeyExponent -// case invalidPubAreaCurve -// case extraDataDoesNotMatchAttToBeSignedHash -// } + static func verify( + attStmt: CBOR, + authenticatorData: Data, + attestedCredentialData: AttestedCredentialData, + clientDataHash: Data, + credentialPublicKey: CredentialPublicKey, + pemRootCertificates: [Data] + ) async throws -> [Certificate] { + // Verify version + guard let verCBOR = attStmt["ver"], + case let .utf8String(ver) = verCBOR, + ver == "2.0" else { + throw TPMAttestationError.invalidVersion + } -// static func verify( -// attStmt: CBOR, -// authenticatorData: Data, -// attestedCredentialData: AttestedCredentialData, -// clientDataHash: Data, -// credentialPublicKey: CredentialPublicKey, -// pemRootCertificates: [Data] -// ) throws { -// // Verify version -// guard let verCBOR = attStmt["ver"], -// case let .utf8String(ver) = verCBOR, -// ver == "2.0" else { -// throw TPMAttestationError.invalidVersion -// } + guard let x5cCBOR = attStmt["x5c"], + case let .array(x5cCBOR) = x5cCBOR else { + throw TPMAttestationError.invalidX5c + } + + // Verify certificate chain + let x5c: [Certificate] = try x5cCBOR.map { + guard case let .byteString(certificate) = $0 else { + throw TPMAttestationError.invalidX5c + } + return try Certificate(derEncoded: certificate) + } + guard let aikCert = x5c.first else { throw TPMAttestationError.invalidX5c } + let intermediates = CertificateStore(x5c[1...]) + let rootCertificates = CertificateStore( + try pemRootCertificates.map { try Certificate(derEncoded: [UInt8]($0)) } + ) -// // Verify certificate chain -// guard let x5cCBOR = attStmt["x5c"], -// case let .array(x5cArray) = x5cCBOR, -// case let .byteString(aikCert) = x5cArray.first else { -// throw TPMAttestationError.invalidX5c -// } -// let certificateChain = try x5cArray[1...].map { -// guard case let .byteString(caCert) = $0 else { throw TPMAttestationError.invalidX5c } -// return caCert -// } + // TPM Attestation Statement Certificate Requirements + // Subject field MUST be set to empty. + guard aikCert.subject.isEmpty else { + throw TPMAttestationError.attestationCertificateSubjectNotEmpty + } + // The Extended Key Usage extension MUST contain the OID 2.23.133.8.3 + guard aikCert.extensions.contains(where: {$0.oid == .tcgKpAIKCertificate}) else { + throw TPMAttestationError.attestationCertificateMissingTcgKpAIKCertificate + } + // The Basic Constraints extension MUST have the CA component set to false. + guard case .notCertificateAuthority = try aikCert.extensions.basicConstraints else { + throw TPMAttestationError.attestationCertificateIsCA + } + + + var verifier = Verifier(rootCertificates: rootCertificates) { + // TODO: do we really want to validate a cert expiry for devices that cannot be updated? + // An expired device cert just means that the device is "old". + RFC5280Policy(validationTime: Date()) + } + let verifierResult: VerificationResult = await verifier.validate( + leafCertificate: aikCert, + intermediates: intermediates + ) + guard case .validCertificate(let chain) = verifierResult else { + throw TPMAttestationError.invalidLeafCertificate + } + + // Verify that the value of the aaguid extension, if present, matches aaguid in authenticatorData + if let certAAGUID = aikCert.extensions.first( + where: {$0.oid == .idFidoGenCeAaguid} + ) { + // The AAGUID is wrapped in two OCTET STRINGS + let derValue = try DER.parse(certAAGUID.value) + guard case .primitive(let certAaguidValue) = derValue.content else { + throw TPMAttestationError.invalidCertAaguid + } + + let authenticatorData = try AuthenticatorData(bytes: Array(authenticatorData)) + guard let attestedData = authenticatorData.attestedData, + attestedData.aaguid == Array(certAaguidValue) else { + throw TPMAttestationError.aaguidMismatch + } + } -// // TODO: Validate certificate chain -// // try CertificateChain.validate( -// // x5c: aikCert + certificateChain, -// // pemRootCertificates: pemRootCertificates -// // ) + // Verify pubArea + guard let pubAreaCBOR = attStmt["pubArea"], + case let .byteString(pubArea) = pubAreaCBOR, + let pubArea = PubArea(from: Data(pubArea)) else { + throw TPMAttestationError.pubAreaInvalid + } + switch pubArea.parameters { + case let .rsa(rsaParameters): + guard case let .rsa(rsaPublicKeyData) = credentialPublicKey, + Array(pubArea.unique.data) == rsaPublicKeyData.n else { + throw TPMAttestationError.invalidPublicKey + } + var pubAreaExponent: Int = rsaParameters.exponent.toInteger(endian: .big) + if pubAreaExponent == 0 { + // "When zero, indicates that the exponent is the default of 2^16 + 1" + pubAreaExponent = 65537 + } -// // Verify pubArea -// guard let pubAreaCBOR = attStmt["pubArea"], -// case let .byteString(pubArea) = pubAreaCBOR, -// let pubArea = PubArea(from: Data(pubArea)) else { -// throw TPMAttestationError.pubAreaInvalid -// } -// switch pubArea.parameters { -// case let .rsa(rsaParameters): -// guard case let .rsa(rsaPublicKeyData) = credentialPublicKey, -// pubArea.unique.data == rsaPublicKeyData.n else { -// throw TPMAttestationError.invalidPublicKey -// } -// var pubAreaExponent: Int = rsaParameters.exponent.toInteger(endian: .big) -// if pubAreaExponent == 0 { -// // "When zero, indicates that the exponent is the default of 2^16 + 1" -// pubAreaExponent = 65537 -// } -// -// let pubKeyExponent: Int = rsaPublicKeyData.e.toInteger(endian: .big) -// guard pubAreaExponent == pubKeyExponent else { -// throw TPMAttestationError.pubAreaExponentDoesNotMatchPubKeyExponent -// } -// case let .ecc(eccParameters): -// guard case let .ec2(ec2PublicKeyData) = credentialPublicKey, -// pubArea.unique.data == ec2PublicKeyData.rawRepresentation else { -// throw TPMAttestationError.invalidPublicKey -// } -// -// guard let pubAreaCrv = COSECurve(from: eccParameters.curveID), -// pubAreaCrv == ec2PublicKeyData.curve else { -// throw TPMAttestationError.invalidPubAreaCurve -// } -// } + let pubKeyExponent: Int = rsaPublicKeyData.e.toInteger(endian: .big) + guard pubAreaExponent == pubKeyExponent else { + throw TPMAttestationError.pubAreaExponentDoesNotMatchPubKeyExponent + } + case let .ecc(eccParameters): + guard case let .ec2(ec2PublicKeyData) = credentialPublicKey, + Array(pubArea.unique.data) == ec2PublicKeyData.rawRepresentation else { + throw TPMAttestationError.invalidPublicKey + } + + guard let pubAreaCrv = COSECurve(from: eccParameters.curveID), + pubAreaCrv == ec2PublicKeyData.curve else { + throw TPMAttestationError.invalidPubAreaCurve + } + } -// // Verify certInfo -// guard let certInfoCBOR = attStmt["certInfo"], -// case let .byteString(certInfo) = certInfoCBOR, -// let parsedCertInfo = CertInfo(fromBytes: Data(certInfo)) else { -// throw TPMAttestationError.certInfoInvalid -// } -// try parsedCertInfo.verify() + // Verify certInfo + guard let certInfoCBOR = attStmt["certInfo"], + case let .byteString(certInfo) = certInfoCBOR, + let parsedCertInfo = CertInfo(fromBytes: Data(certInfo)) else { + throw TPMAttestationError.certInfoInvalid + } + try parsedCertInfo.verify() -// let attToBeSigned = authenticatorData + clientDataHash + let attToBeSigned = authenticatorData + clientDataHash -// guard let algCBOR = attStmt["alg"], -// case let .negativeInt(algorithmNegative) = algCBOR, -// let alg = COSEAlgorithmIdentifier(rawValue: -1 - Int(algorithmNegative)) else { -// throw TPMAttestationError.invalidAlg -// } + guard let algCBOR = attStmt["alg"], + case let .negativeInt(algorithmNegative) = algCBOR, + let alg = COSEAlgorithmIdentifier(rawValue: -1 - Int(algorithmNegative)) else { + throw TPMAttestationError.invalidAlg + } -// guard alg.hashAndCompare(data: attToBeSigned, to: parsedCertInfo.extraData) else { -// throw TPMAttestationError.extraDataDoesNotMatchAttToBeSignedHash -// } -// } -// } + guard alg.hashAndCompare(data: attToBeSigned, to: parsedCertInfo.extraData) else { + throw TPMAttestationError.extraDataDoesNotMatchAttToBeSignedHash + } + + return chain + } +} From 5d10ea5dca5c4736047ebb2219407957b5c58fcb Mon Sep 17 00:00:00 2001 From: Matthieu Barthelemy Date: Tue, 30 Apr 2024 08:31:12 +1000 Subject: [PATCH 09/30] WIP TPM attestation --- .../Formats/TPMAttestation+Structs.swift | 33 ++++++++++++++- .../Registration/Formats/TPMAttestation.swift | 15 +++---- .../Shared/COSE/COSEAlgorithmIdentifier.swift | 42 +++++++++---------- .../Shared/CredentialPublicKey.swift | 6 ++- .../WebAuthn/Helpers/Data+safeSubscript.swift | 3 +- .../TPMAttestationTests/CertInfoTests.swift | 34 +++++++-------- 6 files changed, 83 insertions(+), 50 deletions(-) diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/TPMAttestation+Structs.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/TPMAttestation+Structs.swift index 8a147590..45a9cfad 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/TPMAttestation+Structs.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/TPMAttestation+Structs.swift @@ -13,12 +13,15 @@ //===----------------------------------------------------------------------===// import Foundation +import Crypto extension TPMAttestation { enum CertInfoError: Error { case magicInvalid case typeInvalid case dataTooShort + case nameAlgInvalid + case pubAreaHashInvalid case tpmImplementationIsWIP } @@ -84,18 +87,44 @@ extension TPMAttestation { attested = AttestationInformation(name: attestedName, qualifiedName: qualifiedName) } - func verify() throws { + func verify(pubArea: Data) throws { + // Verify that magic is set to TPM_GENERATED_VALUE let tpmGeneratedValue = 0xFF544347 guard magic.toInteger(endian: .big) == tpmGeneratedValue else { throw CertInfoError.magicInvalid } + // Verify that type is set to TPM_ST_ATTEST_CERTIFY let tpmStAttestCertify = 0x8017 guard type.toInteger(endian: .big) == tpmStAttestCertify else { throw CertInfoError.typeInvalid } - throw CertInfoError.tpmImplementationIsWIP + // Verify pubArea hash + guard let nameAlg = self.attested.name[safe: 0..<2], + let nameAlg = TPMAlg(from: nameAlg), + let nameHash = self.attested.name[safe: 2.. Bool { switch self { - case .algES256: + case .algES256, .algRS256: return SHA256.hash(data: data) == compareHash - case .algES384: + case .algES384, .algRS384: return SHA384.hash(data: data) == compareHash - case .algES512: + case .algES512, .algRS512: return SHA512.hash(data: data) == compareHash + case .algRS1: + return Insecure.SHA1.hash(data: data) == compareHash } } } diff --git a/Sources/WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift b/Sources/WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift index a85ad418..1b0c2a5e 100644 --- a/Sources/WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift +++ b/Sources/WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift @@ -77,8 +77,7 @@ enum CredentialPublicKey { case .ellipticKey: self = try .ec2(EC2PublicKey(publicKeyObject: publicKeyObject, algorithm: algorithm)) case .rsaKey: - throw WebAuthnError.unsupported - // self = try .rsa(RSAPublicKeyData(publicKeyObject: publicKeyObject, algorithm: algorithm)) + self = try .rsa(RSAPublicKeyData(publicKeyObject: publicKeyObject, algorithm: algorithm)) case .octetKey: throw WebAuthnError.unsupported // self = try .okp(OKPPublicKey(publicKeyObject: publicKeyObject, algorithm: algorithm)) @@ -154,6 +153,8 @@ struct EC2PublicKey: PublicKey { .isValidSignature(ecdsaSignature, for: data) else { throw WebAuthnError.invalidSignature } + default: + throw WebAuthnError.unsupportedCOSEAlgorithm } } } @@ -185,6 +186,7 @@ struct RSAPublicKeyData: PublicKey { } func verify(signature: some DataProtocol, data: some DataProtocol) throws { + print("\n•••••• \(Self.self).verify() ") throw WebAuthnError.unsupported // let rsaSignature = _RSA.Signing.RSASignature(derRepresentation: signature) diff --git a/Sources/WebAuthn/Helpers/Data+safeSubscript.swift b/Sources/WebAuthn/Helpers/Data+safeSubscript.swift index e3198207..3ac25984 100644 --- a/Sources/WebAuthn/Helpers/Data+safeSubscript.swift +++ b/Sources/WebAuthn/Helpers/Data+safeSubscript.swift @@ -18,8 +18,9 @@ extension Data { struct IndexOutOfBounds: Error {} subscript(safe range: Range) -> Data? { + let actualRange = range.lowerBound + self.startIndex..= range.upperBound else { return nil } - return self[range] + return self[actualRange] } /// Safely slices bytes from `pointer` to `pointer` + `length`. Updates the pointer afterwards. diff --git a/Tests/WebAuthnTests/Formats/TPMAttestationTests/CertInfoTests.swift b/Tests/WebAuthnTests/Formats/TPMAttestationTests/CertInfoTests.swift index 7f9b139a..1486f129 100644 --- a/Tests/WebAuthnTests/Formats/TPMAttestationTests/CertInfoTests.swift +++ b/Tests/WebAuthnTests/Formats/TPMAttestationTests/CertInfoTests.swift @@ -12,23 +12,23 @@ // //===----------------------------------------------------------------------===// -// @testable import WebAuthn -// import XCTest +@testable import WebAuthn +import XCTest -// final class CertInfoTests: XCTestCase { -// func testInitReturnsNilIfDataIsTooShort() { -// XCTAssertNil(TPMAttestation.CertInfo(fromBytes: Data([UInt8](repeating: 0, count: 8)))) -// XCTAssertNil(TPMAttestation.CertInfo(fromBytes: Data())) -// } +final class CertInfoTests: XCTestCase { + func testInitReturnsNilIfDataIsTooShort() { + XCTAssertNil(TPMAttestation.CertInfo(fromBytes: Data([UInt8](repeating: 0, count: 8)))) + XCTAssertNil(TPMAttestation.CertInfo(fromBytes: Data())) + } -// func testVerifyThrowsIfMagicIsInvalid() throws { -// let certInfo = TPMAttestation.CertInfo(fromBytes: Data([UInt8](repeating: 0, count: 80)))! -// try assertThrowsError(certInfo.verify(), expect: TPMAttestation.CertInfoError.magicInvalid) -// } + func testVerifyThrowsIfMagicIsInvalid() throws { + let certInfo = TPMAttestation.CertInfo(fromBytes: Data([UInt8](repeating: 0, count: 80)))! + try assertThrowsError(certInfo.verify(pubArea: Data()), expect: TPMAttestation.CertInfoError.magicInvalid) + } -// func testVerifyThrowsIfTypeIsInvalid() throws { -// let certInfoBytes: [UInt8] = [0xFF, 0x54, 0x43, 0x47] + [UInt8](repeating: 0, count: 80) -// let certInfo = TPMAttestation.CertInfo(fromBytes: Data(certInfoBytes))! -// try assertThrowsError(certInfo.verify(), expect: TPMAttestation.CertInfoError.typeInvalid) -// } -// } + func testVerifyThrowsIfTypeIsInvalid() throws { + let certInfoBytes: [UInt8] = [0xFF, 0x54, 0x43, 0x47] + [UInt8](repeating: 0, count: 80) + let certInfo = TPMAttestation.CertInfo(fromBytes: Data(certInfoBytes))! + try assertThrowsError(certInfo.verify(pubArea: Data()), expect: TPMAttestation.CertInfoError.typeInvalid) + } +} From 3fafcf3d534b422455feb0f2becbb0dc56d61f5e Mon Sep 17 00:00:00 2001 From: Matthieu Barthelemy Date: Wed, 1 May 2024 06:19:41 +1000 Subject: [PATCH 10/30] Certs veerifications --- .../Formats/FidoU2FAttestation.swift | 16 ++--- .../Formats/PackedAttestation.swift | 9 ++- .../Formats/PackedVerificationPolicy.swift | 49 ++++++++++++++ .../Formats/PublicKey+verifySignature.swift | 3 + .../Formats/TPMAttestation+Structs.swift | 2 +- .../Registration/Formats/TPMAttestation.swift | 47 ++++++-------- .../Formats/TPMVerificationPolicy.swift | 64 +++++++++++++++++++ 7 files changed, 146 insertions(+), 44 deletions(-) create mode 100644 Sources/WebAuthn/Ceremonies/Registration/Formats/PackedVerificationPolicy.swift create mode 100644 Sources/WebAuthn/Ceremonies/Registration/Formats/TPMVerificationPolicy.swift diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2FAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2FAttestation.swift index f8b20498..505039fc 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2FAttestation.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2FAttestation.swift @@ -20,7 +20,7 @@ struct FidoU2FAttestation { enum FidoU2FAttestationError: Error { case invalidSig case invalidX5C - case invalidLeafCertificate + case invalidTrustPath // attestation cert can only have a ecdsaWithSHA256 signature case invalidLeafCertificateSigType case invalidAttestationKeyType @@ -63,7 +63,7 @@ struct FidoU2FAttestation { guard x5c.count == 1 else { throw FidoU2FAttestationError.invalidX5C } - + guard let leafCertificate = x5c.first else { throw FidoU2FAttestationError.invalidX5C } let rootCertificates = CertificateStore( try pemRootCertificates.map { try Certificate(derEncoded: [UInt8]($0)) } @@ -72,23 +72,21 @@ struct FidoU2FAttestation { guard leafCertificate.signatureAlgorithm == .ecdsaWithSHA256 else { throw FidoU2FAttestationError.invalidLeafCertificateSigType } - + var verifier = Verifier(rootCertificates: rootCertificates) { - // TODO: do we really want to validate a cert expiry for devices that cannot be updated? - // An expired device cert just means that the device is "old". - RFC5280Policy(validationTime: Date()) + PackedVerificationPolicy() } let verifierResult: VerificationResult = await verifier.validate( leafCertificate: leafCertificate, intermediates: .init() ) guard case .validCertificate(let chain) = verifierResult else { - throw FidoU2FAttestationError.invalidLeafCertificate + throw FidoU2FAttestationError.invalidTrustPath } // With U2F, the public key used when calculating the signature (`sig`) was encoded in ANSI X9.62 format let ansiPublicKey = [0x04] + key.xCoordinate + key.yCoordinate - + // https://fidoalliance.org/specs/fido-u2f-v1.1-id-20160915/fido-u2f-raw-message-formats-v1.1-id-20160915.html#registration-response-message-success let verificationData = Data( [0x00] // A byte "reserved for future use" with the value 0x00. @@ -97,7 +95,7 @@ struct FidoU2FAttestation { + attestedData.credentialID + ansiPublicKey ) - + // Verify signature let leafCertificatePublicKey: Certificate.PublicKey = leafCertificate.publicKey guard try leafCertificatePublicKey.verifySignature( diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedAttestation.swift index 9e127a29..922582a9 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedAttestation.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedAttestation.swift @@ -22,7 +22,7 @@ struct PackedAttestation { case invalidAlg case invalidSig case invalidX5C - case invalidLeafCertificate + case invalidTrustPath case algDoesNotMatch case missingAttestedCredential // Authenticator data cannot be verified @@ -69,14 +69,15 @@ struct PackedAttestation { var verifier = Verifier(rootCertificates: rootCertificates) { // TODO: do we really want to validate a cert expiry for devices that cannot be updated? // An expired device cert just means that the device is "old". - RFC5280Policy(validationTime: Date()) + //RFC5280Policy(validationTime: Date()) + PackedVerificationPolicy() } let verifierResult: VerificationResult = await verifier.validate( leafCertificate: attestnCert, intermediates: intermediates ) guard case .validCertificate(let chain) = verifierResult else { - throw PackedAttestationError.invalidLeafCertificate + throw PackedAttestationError.invalidTrustPath } // 2. Verify signature @@ -109,9 +110,7 @@ struct PackedAttestation { } return chain - } else { // self attestation is in use - print("\n ••••••• Self attestation!!!!!! ••••••• \n") guard credentialPublicKey.key.algorithm == alg else { throw PackedAttestationError.algDoesNotMatch } diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedVerificationPolicy.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedVerificationPolicy.swift new file mode 100644 index 00000000..4c379f9c --- /dev/null +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedVerificationPolicy.swift @@ -0,0 +1,49 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the WebAuthn Swift open source project +// +// Copyright (c) 2023 the WebAuthn Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of WebAuthn Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation +import SwiftASN1 +import X509 + +/// Based on https://www.w3.org/TR/webauthn-2/#sctn-packed-attestation-cert-requirements +/// Note: we are **not** validating the certificates dates. +public struct PackedVerificationPolicy: VerifierPolicy { + public let verifyingCriticalExtensions: [ASN1ObjectIdentifier] = [ + .X509ExtensionID.basicConstraints, + .X509ExtensionID.nameConstraints, + // The Subject Alternative Name extension MUST be set as defined in [TPMv2-EK-Profile] section 3.2.9. + .X509ExtensionID.subjectAlternativeName, + .X509ExtensionID.keyUsage, + .certificatePolicies, + ] + + public func chainMeetsPolicyRequirements(chain: UnverifiedCertificateChain) -> PolicyEvaluationResult { + let leaf = chain.leaf + + // Version MUST be set to 3 + guard leaf.version == .v3 else { + return .failsToMeetPolicy( + reason: "Version MUST be set to 3: \(leaf)" + ) + } + + // The Basic Constraints extension MUST have the CA component set to false + guard let basic = try? leaf.extensions.basicConstraints, case .notCertificateAuthority = basic else { + return .failsToMeetPolicy( + reason: "The Basic Constraints extension MUST have CA set to false: \(leaf)" + ) + } + return .meetsPolicy + } +} diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/PublicKey+verifySignature.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/PublicKey+verifySignature.swift index 95533e77..fc2c9707 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/PublicKey+verifySignature.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/PublicKey+verifySignature.swift @@ -60,4 +60,7 @@ extension SwiftASN1.ASN1ObjectIdentifier { static var tcgKpAIKCertificate: Self { .init(arrayLiteral: 2, 23, 133, 8, 3) } + static var certificatePolicies: Self { + .init(arrayLiteral: 2, 5, 29, 32) + } } diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/TPMAttestation+Structs.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/TPMAttestation+Structs.swift index 45a9cfad..11434cd5 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/TPMAttestation+Structs.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/TPMAttestation+Structs.swift @@ -130,7 +130,7 @@ extension TPMAttestation { enum PubAreaParameters { case rsa(PubAreaParametersRSA) - case ecc (PubAreaParametersECC) + case ecc(PubAreaParametersECC) } struct PubArea { diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/TPMAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/TPMAttestation.swift index ee86a157..2a2334c7 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/TPMAttestation.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/TPMAttestation.swift @@ -21,13 +21,16 @@ struct TPMAttestation { enum TPMAttestationError: Error { case pubAreaInvalid case certInfoInvalid + /// Invalid or unsupported attestation signature algorithm case invalidAlg + /// Unsupported TPM version case invalidVersion case invalidX5c case invalidPublicKey - case invalidLeafCertificate + case invalidTrustPath case attestationCertificateSubjectNotEmpty case attestationCertificateMissingTcgKpAIKCertificate + /// A leaf (atte4station) cert must not have the CA flag set. case attestationCertificateIsCA case invalidCertAaguid case aaguidMismatch @@ -57,44 +60,31 @@ struct TPMAttestation { } // Verify certificate chain - /*let x5c: [Certificate] = try x5cCBOR.map { + let x5c: [Certificate] = try x5cCBOR.map { guard case let .byteString(certificate) = $0 else { throw TPMAttestationError.invalidX5c } return try Certificate(derEncoded: certificate) } + guard let aikCert = x5c.first else { throw TPMAttestationError.invalidX5c } let intermediates = CertificateStore(x5c[1...]) let rootCertificates = CertificateStore( try pemRootCertificates.map { try Certificate(derEncoded: [UInt8]($0)) } ) - // TPM Attestation Statement Certificate Requirements - // Subject field MUST be set to empty. - guard aikCert.subject.isEmpty else { - throw TPMAttestationError.attestationCertificateSubjectNotEmpty - } - // The Extended Key Usage extension MUST contain the OID 2.23.133.8.3 - guard aikCert.extensions.contains(where: {$0.oid == .tcgKpAIKCertificate}) else { - throw TPMAttestationError.attestationCertificateMissingTcgKpAIKCertificate - } - // The Basic Constraints extension MUST have the CA component set to false. - guard case .notCertificateAuthority = try aikCert.extensions.basicConstraints else { - throw TPMAttestationError.attestationCertificateIsCA - } - - var verifier = Verifier(rootCertificates: rootCertificates) { - // TODO: do we really want to validate a cert expiry for devices that cannot be updated? - // An expired device cert just means that the device is "old". - RFC5280Policy(validationTime: Date()) + TPMVerificationPolicy() } let verifierResult: VerificationResult = await verifier.validate( leafCertificate: aikCert, - intermediates: intermediates + intermediates: intermediates, + diagnosticCallback: { result in + print("\n •••• Self.self result=\(result)") + } ) guard case .validCertificate(let chain) = verifierResult else { - throw TPMAttestationError.invalidLeafCertificate + throw TPMAttestationError.invalidTrustPath } // Verify that the value of the aaguid extension, if present, matches aaguid in authenticatorData @@ -112,7 +102,7 @@ struct TPMAttestation { attestedData.aaguid == Array(certAaguidValue) else { throw TPMAttestationError.aaguidMismatch } - }*/ + } // Verify pubArea guard let pubAreaCBOR = attStmt["pubArea"], @@ -120,8 +110,8 @@ struct TPMAttestation { let pubArea = PubArea(from: Data(pubAreaRaw)) else { throw TPMAttestationError.pubAreaInvalid } - switch pubArea.parameters { - case let .rsa(rsaParameters): + switch pubArea.parameters { + case let .rsa(rsaParameters): guard case let .rsa(rsaPublicKeyData) = credentialPublicKey, Array(pubArea.unique.data) == rsaPublicKeyData.n else { throw TPMAttestationError.invalidPublicKey @@ -136,7 +126,7 @@ struct TPMAttestation { guard pubAreaExponent == pubKeyExponent else { throw TPMAttestationError.pubAreaExponentDoesNotMatchPubKeyExponent } - case let .ecc(eccParameters): + case let .ecc(eccParameters): guard case let .ec2(ec2PublicKeyData) = credentialPublicKey, Array(pubArea.unique.data) == ec2PublicKeyData.rawRepresentation else { throw TPMAttestationError.invalidPublicKey @@ -146,8 +136,7 @@ struct TPMAttestation { pubAreaCrv == ec2PublicKeyData.curve else { throw TPMAttestationError.invalidPubAreaCurve } - } - + } // Verify certInfo guard let certInfoCBOR = attStmt["certInfo"], case let .byteString(certInfo) = certInfoCBOR, @@ -169,6 +158,6 @@ struct TPMAttestation { throw TPMAttestationError.extraDataDoesNotMatchAttToBeSignedHash } - return [] //chain + return chain } } diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/TPMVerificationPolicy.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/TPMVerificationPolicy.swift new file mode 100644 index 00000000..9091c36c --- /dev/null +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/TPMVerificationPolicy.swift @@ -0,0 +1,64 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the WebAuthn Swift open source project +// +// Copyright (c) 2023 the WebAuthn Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of WebAuthn Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation +import SwiftASN1 +import X509 + +/// Based on https://www.w3.org/TR/webauthn-2/#sctn-tpm-cert-requirements +/// Note: we are **not** validating the certificates dates. +public struct TPMVerificationPolicy: VerifierPolicy { + public let verifyingCriticalExtensions: [ASN1ObjectIdentifier] = [ + .X509ExtensionID.basicConstraints, + .X509ExtensionID.nameConstraints, + // The Subject Alternative Name extension MUST be set as defined in [TPMv2-EK-Profile] section 3.2.9. + .X509ExtensionID.subjectAlternativeName, + .X509ExtensionID.keyUsage, + .certificatePolicies, + ] + + public func chainMeetsPolicyRequirements(chain: UnverifiedCertificateChain) -> PolicyEvaluationResult { + let leaf = chain.leaf + + // Version MUST be set to 3 + guard leaf.version == .v3 else { + return .failsToMeetPolicy( + reason: "Version MUST be set to 3: \(leaf)" + ) + } + + // The Subject Alternative Name extension MUST be set as defined in [TPMv2-EK-Profile] section 3.2.9. + // Note: looks like some TPM attestation certs signed by Microsoft have nither subject nor SAN. + /*guard let san = try? leaf.extensions.subjectAlternativeNames else { + return .failsToMeetPolicy( + reason: "Subject Alternative Name extension MUST be set: \(leaf)" + ) + }*/ + + // The Extended Key Usage extension MUST contain the "joint-iso-itu-t(2) internationalorganizations(23) 133 tcg-kp(8) tcg-kp-AIKCertificate(3)" OID. + guard let eku = try? leaf.extensions.extendedKeyUsage, eku.contains(.init(oid: .tcgKpAIKCertificate)) else { + return .failsToMeetPolicy( + reason: "Extended Key Usage extension MUST contain the tcg-kp-AIKCertificate OID: \(leaf)" + ) + } + + // The Basic Constraints extension MUST have the CA component set to false + guard let basic = try? leaf.extensions.basicConstraints, case .notCertificateAuthority = basic else { + return .failsToMeetPolicy( + reason: "The Basic Constraints extension MUST have CA set to false: \(leaf)" + ) + } + return .meetsPolicy + } +} From 18045021fd425003a3139c5d25accbd5f63bc054 Mon Sep 17 00:00:00 2001 From: Matthieu Barthelemy Date: Thu, 2 May 2024 09:51:05 +1000 Subject: [PATCH 11/30] Protocol for attestation verify(); add WIP AndroidKey attestation support --- .../Registration/AttestationObject.swift | 15 ++- .../Registration/AttestationResult.swift | 4 +- .../Registration/AttestedCredentialData.swift | 8 +- .../Formats/AndroidKeyAttestation.swift | 94 +++++++++++++++++++ .../AndroidKeyVerificationPolicy.swift | 33 +++++++ .../Formats/AttestationProtocol.swift | 27 ++++++ .../Formats/FidoU2FAttestation.swift | 26 ++--- .../Formats/FidoU2FVerificationPolicy.swift | 46 +++++++++ .../Formats/PackedAttestation.swift | 18 ++-- .../Formats/PackedVerificationPolicy.swift | 2 - .../Registration/Formats/TPMAttestation.swift | 17 ++-- .../Ceremonies/Shared/AuthenticatorData.swift | 3 +- .../Shared/CredentialPublicKey.swift | 1 - 13 files changed, 239 insertions(+), 55 deletions(-) create mode 100644 Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKeyAttestation.swift create mode 100644 Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKeyVerificationPolicy.swift create mode 100644 Sources/WebAuthn/Ceremonies/Registration/Formats/AttestationProtocol.swift create mode 100644 Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2FVerificationPolicy.swift diff --git a/Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift b/Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift index 5be4a8bd..c9276b0c 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift @@ -71,7 +71,7 @@ public struct AttestationObject { case .packed: trustedPath = try await PackedAttestation.verify( attStmt: attestationStatement, - authenticatorData: Data(rawAuthenticatorData), + authenticatorData: authenticatorData, clientDataHash: Data(clientDataHash), credentialPublicKey: credentialPublicKey, pemRootCertificates: pemRootCertificates @@ -79,13 +79,19 @@ public struct AttestationObject { case .tpm: trustedPath = try await TPMAttestation.verify( attStmt: attestationStatement, - authenticatorData: Data(rawAuthenticatorData), - attestedCredentialData: attestedCredentialData, + authenticatorData: authenticatorData, + clientDataHash: Data(clientDataHash), + credentialPublicKey: credentialPublicKey, + pemRootCertificates: pemRootCertificates + ) + case .androidKey: + trustedPath = try await AndroidKeyAttestation.verify( + attStmt: attestationStatement, + authenticatorData: authenticatorData, clientDataHash: Data(clientDataHash), credentialPublicKey: credentialPublicKey, pemRootCertificates: pemRootCertificates ) - // Legacy format used mostly by older authenticators case .fidoU2F: trustedPath = try await FidoU2FAttestation.verify( @@ -100,7 +106,6 @@ public struct AttestationObject { } return AttestationResult( - aaguid: [], format: format, trustChain: trustedPath, attestedCredentialData: attestedCredentialData diff --git a/Sources/WebAuthn/Ceremonies/Registration/AttestationResult.swift b/Sources/WebAuthn/Ceremonies/Registration/AttestationResult.swift index 875ae7b1..c578f059 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/AttestationResult.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/AttestationResult.swift @@ -24,10 +24,10 @@ public struct AttestationResult { case anonCA case none } - public let aaguid: [UInt8]? + //public let aaguid: [UInt8]? public let format: AttestationFormat //public let type: AttestationType public let trustChain: [Certificate] - let attestedCredentialData: AttestedCredentialData + public let attestedCredentialData: AttestedCredentialData } diff --git a/Sources/WebAuthn/Ceremonies/Registration/AttestedCredentialData.swift b/Sources/WebAuthn/Ceremonies/Registration/AttestedCredentialData.swift index d264e977..432ab03c 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/AttestedCredentialData.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/AttestedCredentialData.swift @@ -13,8 +13,8 @@ //===----------------------------------------------------------------------===// // Contains the new public key created by the authenticator. -struct AttestedCredentialData: Equatable { - let aaguid: [UInt8] - let credentialID: [UInt8] - let publicKey: [UInt8] +public struct AttestedCredentialData: Equatable { + public let aaguid: [UInt8] + public let credentialID: [UInt8] + public let publicKey: [UInt8] } diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKeyAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKeyAttestation.swift new file mode 100644 index 00000000..02411f1e --- /dev/null +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKeyAttestation.swift @@ -0,0 +1,94 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the WebAuthn Swift open source project +// +// Copyright (c) 2023 the WebAuthn Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of WebAuthn Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation +import SwiftCBOR +import X509 +import SwiftASN1 + +// https://www.w3.org/TR/webauthn-2/#sctn-android-key-attestation +struct AndroidKeyAttestation: AttestationProtocol { + enum AndroidKeyAttestationError: Error { + case invalidSig + case invalidX5C + case invalidTrustPath + // Authenticator data cannot be verified + case invalidVerificationData + case credentialPublicKeyMismatch + } + + static func verify( + attStmt: CBOR, + authenticatorData: AuthenticatorData, + clientDataHash: Data, + credentialPublicKey: CredentialPublicKey, + pemRootCertificates: [Data] + ) async throws -> [Certificate] { + guard let sigCBOR = attStmt["sig"], case let .byteString(sig) = sigCBOR else { + throw AndroidKeyAttestationError.invalidSig + } + + guard let x5cCBOR = attStmt["x5c"], case let .array(x5cCBOR) = x5cCBOR else { + throw AndroidKeyAttestationError.invalidX5C + } + + let x5c: [Certificate] = try x5cCBOR.map { + guard case let .byteString(certificate) = $0 else { + throw AndroidKeyAttestationError.invalidX5C + } + return try Certificate(derEncoded: certificate) + } + + guard let leafCertificate = x5c.first else { throw AndroidKeyAttestationError.invalidX5C } + let intermediates = CertificateStore(x5c[1...]) + let rootCertificates = CertificateStore( + try pemRootCertificates.map { try Certificate(derEncoded: [UInt8]($0)) } + ) + + let verificationData = authenticatorData.rawData + clientDataHash + // Verify signature + let leafCertificatePublicKey: Certificate.PublicKey = leafCertificate.publicKey + guard try leafCertificatePublicKey.verifySignature( + Data(sig), + algorithm: leafCertificate.signatureAlgorithm, + data: verificationData) else { + throw AndroidKeyAttestationError.invalidVerificationData + } + + // We need to verify that the authenticator certificate's public key matches the public key present in + // authenticatorData.attestedData (credentialPublicKey). + // We can't directly compare two public keys, so instead we verify the signature with both keys: + // the authenticator cert (previous step above) and credentialPublicKey (below). + guard let _ = try? credentialPublicKey.verify(signature: Data(sig), data: verificationData) else { + throw AndroidKeyAttestationError.credentialPublicKeyMismatch + } + + var verifier = Verifier(rootCertificates: rootCertificates) { + AndroidKeyVerificationPolicy() + } + let verifierResult: VerificationResult = await verifier.validate( + leafCertificate: leafCertificate, + intermediates: intermediates, + diagnosticCallback: { result in + print("\n •••• \(Self.self) result=\(result)") + } + ) + guard case .validCertificate(let chain) = verifierResult else { + throw AndroidKeyAttestationError.invalidTrustPath + } + + return chain + } +} + diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKeyVerificationPolicy.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKeyVerificationPolicy.swift new file mode 100644 index 00000000..2593de7f --- /dev/null +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKeyVerificationPolicy.swift @@ -0,0 +1,33 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the WebAuthn Swift open source project +// +// Copyright (c) 2023 the WebAuthn Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of WebAuthn Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation +import SwiftASN1 +import X509 + +/// Based on https://www.w3.org/TR/webauthn-2/#sctn-fido-u2f-attestation +public struct AndroidKeyVerificationPolicy: VerifierPolicy { + public let verifyingCriticalExtensions: [ASN1ObjectIdentifier] = [ + .X509ExtensionID.basicConstraints, + .X509ExtensionID.nameConstraints, + .X509ExtensionID.subjectAlternativeName, + .X509ExtensionID.keyUsage, + ] + + public func chainMeetsPolicyRequirements(chain: UnverifiedCertificateChain) -> PolicyEvaluationResult { + let leaf = chain.leaf + + return .meetsPolicy + } +} diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/AttestationProtocol.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/AttestationProtocol.swift new file mode 100644 index 00000000..f34d90f6 --- /dev/null +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/AttestationProtocol.swift @@ -0,0 +1,27 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the WebAuthn Swift open source project +// +// Copyright (c) 2023 the WebAuthn Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of WebAuthn Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation +import SwiftCBOR +import X509 + +protocol AttestationProtocol { + static func verify( + attStmt: CBOR, + authenticatorData: AuthenticatorData, + clientDataHash: Data, + credentialPublicKey: CredentialPublicKey, + pemRootCertificates: [Data] + ) async throws -> [Certificate] +} diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2FAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2FAttestation.swift index 505039fc..b1cb4220 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2FAttestation.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2FAttestation.swift @@ -16,7 +16,7 @@ import Foundation import SwiftCBOR import X509 -struct FidoU2FAttestation { +struct FidoU2FAttestation: AttestationProtocol { enum FidoU2FAttestationError: Error { case invalidSig case invalidX5C @@ -24,7 +24,6 @@ struct FidoU2FAttestation { // attestation cert can only have a ecdsaWithSHA256 signature case invalidLeafCertificateSigType case invalidAttestationKeyType - case missingAttestedCredential // Authenticator data cannot be verified case invalidVerificationData } @@ -39,10 +38,6 @@ struct FidoU2FAttestation { guard let sigCBOR = attStmt["sig"], case let .byteString(sig) = sigCBOR else { throw FidoU2FAttestationError.invalidSig } - - guard let attestedData = authenticatorData.attestedData else { - throw FidoU2FAttestationError.missingAttestedCredential - } guard case let .ec2(key) = credentialPublicKey, key.algorithm == .algES256 else { throw FidoU2FAttestationError.invalidAttestationKeyType @@ -59,26 +54,18 @@ struct FidoU2FAttestation { return try Certificate(derEncoded: certificate) } - // U2F attestation can only have 1 certificate - guard x5c.count == 1 else { - throw FidoU2FAttestationError.invalidX5C - } - guard let leafCertificate = x5c.first else { throw FidoU2FAttestationError.invalidX5C } + let intermediates = CertificateStore(x5c[1...]) let rootCertificates = CertificateStore( try pemRootCertificates.map { try Certificate(derEncoded: [UInt8]($0)) } ) - guard leafCertificate.signatureAlgorithm == .ecdsaWithSHA256 else { - throw FidoU2FAttestationError.invalidLeafCertificateSigType - } - var verifier = Verifier(rootCertificates: rootCertificates) { - PackedVerificationPolicy() + FidoU2FVerificationPolicy() } let verifierResult: VerificationResult = await verifier.validate( leafCertificate: leafCertificate, - intermediates: .init() + intermediates: intermediates ) guard case .validCertificate(let chain) = verifierResult else { throw FidoU2FAttestationError.invalidTrustPath @@ -87,12 +74,13 @@ struct FidoU2FAttestation { // With U2F, the public key used when calculating the signature (`sig`) was encoded in ANSI X9.62 format let ansiPublicKey = [0x04] + key.xCoordinate + key.yCoordinate - // https://fidoalliance.org/specs/fido-u2f-v1.1-id-20160915/fido-u2f-raw-message-formats-v1.1-id-20160915.html#registration-response-message-success + // https://www.w3.org/TR/webauthn-2/#sctn-fido-u2f-attestation Verification Procedure step 5. let verificationData = Data( [0x00] // A byte "reserved for future use" with the value 0x00. + authenticatorData.relyingPartyIDHash + Array(clientDataHash) - + attestedData.credentialID + // This has been verified as not nil in AttestationObject + + authenticatorData.attestedData!.credentialID + ansiPublicKey ) diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2FVerificationPolicy.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2FVerificationPolicy.swift new file mode 100644 index 00000000..9fefb54b --- /dev/null +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2FVerificationPolicy.swift @@ -0,0 +1,46 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the WebAuthn Swift open source project +// +// Copyright (c) 2023 the WebAuthn Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of WebAuthn Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation +import SwiftASN1 +import X509 + +/// Based on https://www.w3.org/TR/webauthn-2/#sctn-fido-u2f-attestation +public struct FidoU2FVerificationPolicy: VerifierPolicy { + public let verifyingCriticalExtensions: [ASN1ObjectIdentifier] = [ + .X509ExtensionID.basicConstraints, + .X509ExtensionID.nameConstraints, + .X509ExtensionID.subjectAlternativeName, + .X509ExtensionID.keyUsage, + ] + + public func chainMeetsPolicyRequirements(chain: UnverifiedCertificateChain) -> PolicyEvaluationResult { + + // Check that x5c has exactly one element + guard chain.count == 1 else { + return .failsToMeetPolicy( + reason: "Authenticator attestation must return exactly 1 certificate, got \(chain.count)" + ) + } + + let leaf = chain.leaf + // Certificate public key must be an Elliptic Curve (EC) public key over the P-256 curve, + guard leaf.signatureAlgorithm == .ecdsaWithSHA256 else { + return .failsToMeetPolicy( + reason: "Public key must be Elliptic Curve (EC) P-256: \(leaf)" + ) + } + return .meetsPolicy + } +} diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedAttestation.swift index 922582a9..8095dec2 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedAttestation.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedAttestation.swift @@ -17,14 +17,13 @@ import SwiftCBOR import X509 import SwiftASN1 -struct PackedAttestation { +struct PackedAttestation: AttestationProtocol { enum PackedAttestationError: Error { case invalidAlg case invalidSig case invalidX5C case invalidTrustPath case algDoesNotMatch - case missingAttestedCredential // Authenticator data cannot be verified case invalidVerificationData case invalidCertAaguid @@ -33,7 +32,7 @@ struct PackedAttestation { static func verify( attStmt: CBOR, - authenticatorData: Data, + authenticatorData: AuthenticatorData, clientDataHash: Data, credentialPublicKey: CredentialPublicKey, pemRootCertificates: [Data] @@ -47,7 +46,7 @@ struct PackedAttestation { throw PackedAttestationError.invalidSig } - let verificationData = authenticatorData + clientDataHash + let verificationData = authenticatorData.rawData + clientDataHash if let x5cCBOR = attStmt["x5c"] { guard case let .array(x5cCBOR) = x5cCBOR else { @@ -61,15 +60,16 @@ struct PackedAttestation { return try Certificate(derEncoded: certificate) } guard let attestnCert = x5c.first else { throw PackedAttestationError.invalidX5C } + if x5c.count > 1 { + + } let intermediates = CertificateStore(x5c[1...]) let rootCertificates = CertificateStore( try pemRootCertificates.map { try Certificate(derEncoded: [UInt8]($0)) } ) var verifier = Verifier(rootCertificates: rootCertificates) { - // TODO: do we really want to validate a cert expiry for devices that cannot be updated? - // An expired device cert just means that the device is "old". - //RFC5280Policy(validationTime: Date()) + RFC5280Policy(validationTime: Date()) PackedVerificationPolicy() } let verifierResult: VerificationResult = await verifier.validate( @@ -102,9 +102,7 @@ struct PackedAttestation { throw PackedAttestationError.invalidCertAaguid } - let authenticatorData = try AuthenticatorData(bytes: Array(authenticatorData)) - guard let attestedData = authenticatorData.attestedData, - attestedData.aaguid == Array(certAaguidValue) else { + guard authenticatorData.attestedData?.aaguid == Array(certAaguidValue) else { throw PackedAttestationError.aaguidMismatch } } diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedVerificationPolicy.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedVerificationPolicy.swift index 4c379f9c..548b6935 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedVerificationPolicy.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedVerificationPolicy.swift @@ -22,10 +22,8 @@ public struct PackedVerificationPolicy: VerifierPolicy { public let verifyingCriticalExtensions: [ASN1ObjectIdentifier] = [ .X509ExtensionID.basicConstraints, .X509ExtensionID.nameConstraints, - // The Subject Alternative Name extension MUST be set as defined in [TPMv2-EK-Profile] section 3.2.9. .X509ExtensionID.subjectAlternativeName, .X509ExtensionID.keyUsage, - .certificatePolicies, ] public func chainMeetsPolicyRequirements(chain: UnverifiedCertificateChain) -> PolicyEvaluationResult { diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/TPMAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/TPMAttestation.swift index 2a2334c7..60b1ad48 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/TPMAttestation.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/TPMAttestation.swift @@ -17,7 +17,7 @@ import SwiftCBOR import X509 import SwiftASN1 -struct TPMAttestation { +struct TPMAttestation: AttestationProtocol { enum TPMAttestationError: Error { case pubAreaInvalid case certInfoInvalid @@ -41,8 +41,7 @@ struct TPMAttestation { static func verify( attStmt: CBOR, - authenticatorData: Data, - attestedCredentialData: AttestedCredentialData, + authenticatorData: AuthenticatorData, clientDataHash: Data, credentialPublicKey: CredentialPublicKey, pemRootCertificates: [Data] @@ -74,14 +73,12 @@ struct TPMAttestation { ) var verifier = Verifier(rootCertificates: rootCertificates) { + RFC5280Policy(validationTime: Date()) TPMVerificationPolicy() } let verifierResult: VerificationResult = await verifier.validate( leafCertificate: aikCert, - intermediates: intermediates, - diagnosticCallback: { result in - print("\n •••• Self.self result=\(result)") - } + intermediates: intermediates ) guard case .validCertificate(let chain) = verifierResult else { throw TPMAttestationError.invalidTrustPath @@ -97,9 +94,7 @@ struct TPMAttestation { throw TPMAttestationError.invalidCertAaguid } - let authenticatorData = try AuthenticatorData(bytes: Array(authenticatorData)) - guard let attestedData = authenticatorData.attestedData, - attestedData.aaguid == Array(certAaguidValue) else { + guard authenticatorData.attestedData?.aaguid == Array(certAaguidValue) else { throw TPMAttestationError.aaguidMismatch } } @@ -153,7 +148,7 @@ struct TPMAttestation { } // Verify that extraData is set to the hash of attToBeSigned using the hash algorithm employed in "alg" - let attToBeSigned = authenticatorData + clientDataHash + let attToBeSigned = authenticatorData.rawData + clientDataHash guard alg.hashAndCompare(data: attToBeSigned, to: parsedCertInfo.extraData) else { throw TPMAttestationError.extraDataDoesNotMatchAttToBeSignedHash } diff --git a/Sources/WebAuthn/Ceremonies/Shared/AuthenticatorData.swift b/Sources/WebAuthn/Ceremonies/Shared/AuthenticatorData.swift index 1de8d0b4..0cac68c6 100644 --- a/Sources/WebAuthn/Ceremonies/Shared/AuthenticatorData.swift +++ b/Sources/WebAuthn/Ceremonies/Shared/AuthenticatorData.swift @@ -25,6 +25,7 @@ struct AuthenticatorData: Equatable { /// For attestation signatures this value will be set. For assertion signatures not. let attestedData: AttestedCredentialData? let extData: [UInt8]? + let rawData: Data } extension AuthenticatorData { @@ -75,7 +76,7 @@ extension AuthenticatorData { self.counter = counter self.attestedData = attestedCredentialData self.extData = extensionData - + self.rawData = Data(bytes) } /// Parse and return the attested credential data and its length. diff --git a/Sources/WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift b/Sources/WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift index 1b0c2a5e..1981c529 100644 --- a/Sources/WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift +++ b/Sources/WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift @@ -72,7 +72,6 @@ enum CredentialPublicKey { throw WebAuthnError.unsupportedCOSEAlgorithm } - // Currently we only support elliptic curve algorithms switch keyType { case .ellipticKey: self = try .ec2(EC2PublicKey(publicKeyObject: publicKeyObject, algorithm: algorithm)) From c987f429555929912b048ee767475a9e37cf5f94 Mon Sep 17 00:00:00 2001 From: Matthieu Barthelemy Date: Sat, 4 May 2024 07:39:20 +1000 Subject: [PATCH 12/30] AndroidKey attestation support --- .../Formats/AndroidKeyAttestation.swift | 6 +- .../AndroidKeyVerificationPolicy.swift | 276 +++++++++++++++++- .../Formats/FidoU2FVerificationPolicy.swift | 6 +- .../Formats/PackedVerificationPolicy.swift | 6 +- .../Formats/PublicKey+verifySignature.swift | 3 + .../Formats/TPMVerificationPolicy.swift | 8 +- 6 files changed, 288 insertions(+), 17 deletions(-) diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKeyAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKeyAttestation.swift index 02411f1e..91048528 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKeyAttestation.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKeyAttestation.swift @@ -15,7 +15,6 @@ import Foundation import SwiftCBOR import X509 -import SwiftASN1 // https://www.w3.org/TR/webauthn-2/#sctn-android-key-attestation struct AndroidKeyAttestation: AttestationProtocol { @@ -23,8 +22,9 @@ struct AndroidKeyAttestation: AttestationProtocol { case invalidSig case invalidX5C case invalidTrustPath - // Authenticator data cannot be verified + /// Authenticator data cannot be verified case invalidVerificationData + /// The authenticator certificate public key does not match the attested data public key case credentialPublicKeyMismatch } @@ -75,7 +75,7 @@ struct AndroidKeyAttestation: AttestationProtocol { } var verifier = Verifier(rootCertificates: rootCertificates) { - AndroidKeyVerificationPolicy() + AndroidKeyVerificationPolicy(clientDataHash: clientDataHash) } let verifierResult: VerificationResult = await verifier.validate( leafCertificate: leafCertificate, diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKeyVerificationPolicy.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKeyVerificationPolicy.swift index 2593de7f..4e4aaf66 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKeyVerificationPolicy.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKeyVerificationPolicy.swift @@ -17,17 +17,285 @@ import SwiftASN1 import X509 /// Based on https://www.w3.org/TR/webauthn-2/#sctn-fido-u2f-attestation -public struct AndroidKeyVerificationPolicy: VerifierPolicy { - public let verifyingCriticalExtensions: [ASN1ObjectIdentifier] = [ +struct AndroidKeyVerificationPolicy: VerifierPolicy { + let verifyingCriticalExtensions: [ASN1ObjectIdentifier] = [ .X509ExtensionID.basicConstraints, .X509ExtensionID.nameConstraints, .X509ExtensionID.subjectAlternativeName, .X509ExtensionID.keyUsage, ] - public func chainMeetsPolicyRequirements(chain: UnverifiedCertificateChain) -> PolicyEvaluationResult { + private let clientDataHash: [UInt8] + + init(clientDataHash: Data) { + self.clientDataHash = Array(clientDataHash) + } + + func chainMeetsPolicyRequirements(chain: UnverifiedCertificateChain) -> PolicyEvaluationResult { let leaf = chain.leaf - + + guard let androidExtension = leaf.extensions[oid: .androidAttestation] else { + return .failsToMeetPolicy( + reason: "Required extension \(ASN1ObjectIdentifier.androidAttestation) not present: \(leaf)" + ) + } + + let keyDesc: AndroidKeyDescription! + do { + keyDesc = try AndroidKeyDescription(derEncoded: androidExtension.value) + } + catch let error { + return .failsToMeetPolicy( + reason: "Error parsing KeyDescription extension (\(ASN1ObjectIdentifier.androidAttestation)): \(error): \(leaf)" + ) + } + + // Verify that the attestationChallenge field in the attestation certificate extension data is identical to clientDataHash. + guard Array(keyDesc.attestationChallenge.bytes) == clientDataHash else { + return .failsToMeetPolicy( + reason: "Challenge hash in keyDescription does not match clientDataHash: \(leaf)" + ) + } + + // Allow authenticator keys that were either generated in secure hardware or in software + guard keyDesc.softwareEnforced.origin == 0 && keyDesc.teeEnforced.origin == 0 else { + return .failsToMeetPolicy( + reason: "keyDescription says authenticator key was not hardware or software generated: \(leaf)" + ) + } + + // Key must be dedicated to the RP ID + guard keyDesc.softwareEnforced.allApplications == nil && keyDesc.teeEnforced.allApplications == nil else { + return .failsToMeetPolicy( + reason: "keyDescription says authenticator key is for all aplications: \(leaf)" + ) + } + + // Key must have a signing purpose + guard keyDesc.softwareEnforced.purpose.contains(.sign) || keyDesc.teeEnforced.purpose.contains(.sign) else { + return .failsToMeetPolicy( + reason: "keyDescription says authenticator key is not for signing: \(leaf)" + ) + } + return .meetsPolicy } } + +// https://source.android.com/docs/security/features/keystore/attestation#schema +struct AndroidKeyDescription: DERImplicitlyTaggable { + internal init( + attestationVersion: Int, + attestationSecurityLevel: ASN1Any, + keymasterVersion: Int, + keymasterSecurityLevel: ASN1Any, + attestationChallenge: ASN1OctetString, + uniqueID: ASN1OctetString, + softwareEnforced: AuthorizationList, + teeEnforced: AuthorizationList + ) { + self.attestationVersion = attestationVersion + self.attestationSecurityLevel = attestationSecurityLevel + self.keymasterVersion = keymasterVersion + self.keymasterSecurityLevel = keymasterSecurityLevel + self.attestationChallenge = attestationChallenge + self.uniqueID = uniqueID + self.softwareEnforced = softwareEnforced + self.teeEnforced = teeEnforced + } + + static var defaultIdentifier: ASN1Identifier { + .sequence + } + + // We need these fields for verifying the attestation + var attestationChallenge: ASN1OctetString + var softwareEnforced: AuthorizationList + var teeEnforced: AuthorizationList + // We don't need or care about these fields + var attestationVersion: Int + var attestationSecurityLevel: ASN1Any + var keymasterVersion: Int + var keymasterSecurityLevel: ASN1Any + var uniqueID: ASN1OctetString + + init(derEncoded rootNode: ASN1Node, withIdentifier identifier: ASN1Identifier) throws { + self = try DER.sequence(rootNode, identifier: identifier) { nodes in + let version = try Int(derEncoded: &nodes) + let secLevel = try ASN1Any(derEncoded: &nodes) + let kMasterVersion = try Int(derEncoded: &nodes) + let kMasterSecLevel = try ASN1Any(derEncoded: &nodes) + let challenge = try ASN1OctetString(derEncoded: &nodes) + let id = try ASN1OctetString(derEncoded: &nodes) + let softwareEnforced = try AuthorizationList(derEncoded: &nodes) + let teeEnforced = try AuthorizationList(derEncoded: &nodes) + return AndroidKeyDescription.init( + attestationVersion: version, + attestationSecurityLevel: secLevel, + keymasterVersion: kMasterVersion, + keymasterSecurityLevel: kMasterSecLevel, + attestationChallenge: challenge, + uniqueID: id, + softwareEnforced: softwareEnforced, + teeEnforced: teeEnforced + ) + } + } + + func serialize(into coder: inout DER.Serializer, withIdentifier identifier: ASN1Identifier) throws {} +} + +struct AuthorizationList: DERParseable { + enum KeyPurpose: Int { + case encrypt, decrypt, sign, verify, derive, wrap + } + init(purpose: [KeyPurpose], origin: Int, allApplications: ASN1Any?) { + self.purpose = purpose + self.origin = origin + self.allApplications = allApplications + } + + // We only need these fields for verifying the attestation + var purpose: [KeyPurpose] = [] + var origin: Int? + var allApplications: ASN1Any? + + init(derEncoded rootNode: ASN1Node) throws { + self = try DER.sequence(rootNode, identifier: .sequence) { nodes in + var purpose: [KeyPurpose] = [] + _ = try DER.optionalExplicitlyTagged(&nodes, tagNumber: 1, tagClass: .contextSpecific) { node in + try DER.set(node, identifier: .set) { items in + while let item = items.next() { + if let intValue = try? Int(derEncoded: item), let currentPurpose = KeyPurpose(rawValue: intValue) { + purpose.append(currentPurpose) + } + } + } + } + + // We don't care about these fields but must decode them + let _ = try DER.optionalExplicitlyTagged(&nodes, tagNumber: 2, tagClass: .contextSpecific) { + try Int(derEncoded: $0) + } + let _ = try DER.optionalExplicitlyTagged(&nodes, tagNumber: 3, tagClass: .contextSpecific) { + try Int(derEncoded: $0) + } + let _ = try DER.optionalExplicitlyTagged(&nodes, tagNumber: 5, tagClass: .contextSpecific) { + ASN1Any(derEncoded: $0) + } + let _ = try DER.optionalExplicitlyTagged(&nodes, tagNumber: 6, tagClass: .contextSpecific) { + ASN1Any(derEncoded: $0) + } + let _ = try DER.optionalExplicitlyTagged(&nodes, tagNumber: 10, tagClass: .contextSpecific) { + ASN1Any(derEncoded: $0) + } + let _ = try DER.optionalExplicitlyTagged(&nodes, tagNumber: 200, tagClass: .contextSpecific) { + ASN1Any(derEncoded: $0) + } + let _ = try DER.optionalExplicitlyTagged(&nodes, tagNumber: 303, tagClass: .contextSpecific) { + ASN1Any(derEncoded: $0) + } + let _ = try DER.optionalExplicitlyTagged(&nodes, tagNumber: 400, tagClass: .contextSpecific) { + ASN1Any(derEncoded: $0) + } + let _ = try DER.optionalExplicitlyTagged(&nodes, tagNumber: 401, tagClass: .contextSpecific) { + ASN1Any(derEncoded: $0) + } + let _ = try DER.optionalExplicitlyTagged(&nodes, tagNumber: 402, tagClass: .contextSpecific) { + ASN1Any(derEncoded: $0) + } + let _ = try DER.optionalExplicitlyTagged(&nodes, tagNumber: 503, tagClass: .contextSpecific) { + ASN1Any(derEncoded: $0) + } + let _ = try DER.optionalExplicitlyTagged(&nodes, tagNumber: 504, tagClass: .contextSpecific) { + ASN1Any(derEncoded: $0) + } + let _ = try DER.optionalExplicitlyTagged(&nodes, tagNumber: 505, tagClass: .contextSpecific) { + ASN1Any(derEncoded: $0) + } + let _ = try DER.optionalExplicitlyTagged(&nodes, tagNumber: 506, tagClass: .contextSpecific) { + ASN1Any(derEncoded: $0) + } + let _ = try DER.optionalExplicitlyTagged(&nodes, tagNumber: 507, tagClass: .contextSpecific) { + ASN1Any(derEncoded: $0) + } + let _ = try DER.optionalExplicitlyTagged(&nodes, tagNumber: 508, tagClass: .contextSpecific) { + ASN1Any(derEncoded: $0) + } + let _ = try DER.optionalExplicitlyTagged(&nodes, tagNumber: 509, tagClass: .contextSpecific) { + ASN1Any(derEncoded: $0) + } + + let allApplications = try DER.optionalExplicitlyTagged(&nodes, tagNumber: 600, tagClass: .contextSpecific) { + ASN1Any(derEncoded: $0) + } + + // We don't care about these fields but must decode them + let _ = try DER.optionalExplicitlyTagged(&nodes, tagNumber: 601, tagClass: .contextSpecific) { + ASN1Any(derEncoded: $0) + } + let _ = try DER.optionalExplicitlyTagged(&nodes, tagNumber: 701, tagClass: .contextSpecific) { + ASN1Any(derEncoded: $0) + } + + let origin = try DER.optionalExplicitlyTagged(&nodes, tagNumber: 702, tagClass: .contextSpecific) { + try Int(derEncoded: $0) + } + + // We don't care about these fields but must decode them + let _ = try DER.optionalExplicitlyTagged(&nodes, tagNumber: 703, tagClass: .contextSpecific) { + ASN1Any(derEncoded: $0) + } + let _ = try DER.optionalExplicitlyTagged(&nodes, tagNumber: 704, tagClass: .contextSpecific) { + ASN1Any(derEncoded: $0) + } + let _ = try DER.optionalExplicitlyTagged(&nodes, tagNumber: 705, tagClass: .contextSpecific) { + ASN1Any(derEncoded: $0) + } + let _ = try DER.optionalExplicitlyTagged(&nodes, tagNumber: 706, tagClass: .contextSpecific) { + ASN1Any(derEncoded: $0) + } + let _ = try DER.optionalExplicitlyTagged(&nodes, tagNumber: 709, tagClass: .contextSpecific) { + ASN1Any(derEncoded: $0) + } + let _ = try DER.optionalExplicitlyTagged(&nodes, tagNumber: 709, tagClass: .contextSpecific) { + ASN1Any(derEncoded: $0) + } + let _ = try DER.optionalExplicitlyTagged(&nodes, tagNumber: 709, tagClass: .contextSpecific) { + ASN1Any(derEncoded: $0) + } + let _ = try DER.optionalExplicitlyTagged(&nodes, tagNumber: 710, tagClass: .contextSpecific) { + ASN1Any(derEncoded: $0) + } + let _ = try DER.optionalExplicitlyTagged(&nodes, tagNumber: 711, tagClass: .contextSpecific) { + ASN1Any(derEncoded: $0) + } + let _ = try DER.optionalExplicitlyTagged(&nodes, tagNumber: 712, tagClass: .contextSpecific) { + ASN1Any(derEncoded: $0) + } + let _ = try DER.optionalExplicitlyTagged(&nodes, tagNumber: 713, tagClass: .contextSpecific) { + ASN1Any(derEncoded: $0) + } + let _ = try DER.optionalExplicitlyTagged(&nodes, tagNumber: 714, tagClass: .contextSpecific) { + ASN1Any(derEncoded: $0) + } + let _ = try DER.optionalExplicitlyTagged(&nodes, tagNumber: 715, tagClass: .contextSpecific) { + ASN1Any(derEncoded: $0) + } + let _ = try DER.optionalExplicitlyTagged(&nodes, tagNumber: 716, tagClass: .contextSpecific) { + ASN1Any(derEncoded: $0) + } + let _ = try DER.optionalExplicitlyTagged(&nodes, tagNumber: 717, tagClass: .contextSpecific) { + ASN1Any(derEncoded: $0) + } + let _ = try DER.optionalExplicitlyTagged(&nodes, tagNumber: 718, tagClass: .contextSpecific) { + ASN1Any(derEncoded: $0) + } + let _ = try DER.optionalExplicitlyTagged(&nodes, tagNumber: 719, tagClass: .contextSpecific) { + ASN1Any(derEncoded: $0) + } + + return AuthorizationList(purpose: purpose, origin: origin ?? 0, allApplications: allApplications) + } + } +} diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2FVerificationPolicy.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2FVerificationPolicy.swift index 9fefb54b..732cfb1b 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2FVerificationPolicy.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2FVerificationPolicy.swift @@ -17,15 +17,15 @@ import SwiftASN1 import X509 /// Based on https://www.w3.org/TR/webauthn-2/#sctn-fido-u2f-attestation -public struct FidoU2FVerificationPolicy: VerifierPolicy { - public let verifyingCriticalExtensions: [ASN1ObjectIdentifier] = [ +struct FidoU2FVerificationPolicy: VerifierPolicy { + let verifyingCriticalExtensions: [ASN1ObjectIdentifier] = [ .X509ExtensionID.basicConstraints, .X509ExtensionID.nameConstraints, .X509ExtensionID.subjectAlternativeName, .X509ExtensionID.keyUsage, ] - public func chainMeetsPolicyRequirements(chain: UnverifiedCertificateChain) -> PolicyEvaluationResult { + func chainMeetsPolicyRequirements(chain: UnverifiedCertificateChain) -> PolicyEvaluationResult { // Check that x5c has exactly one element guard chain.count == 1 else { diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedVerificationPolicy.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedVerificationPolicy.swift index 548b6935..7f9591cc 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedVerificationPolicy.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedVerificationPolicy.swift @@ -18,15 +18,15 @@ import X509 /// Based on https://www.w3.org/TR/webauthn-2/#sctn-packed-attestation-cert-requirements /// Note: we are **not** validating the certificates dates. -public struct PackedVerificationPolicy: VerifierPolicy { - public let verifyingCriticalExtensions: [ASN1ObjectIdentifier] = [ +struct PackedVerificationPolicy: VerifierPolicy { + let verifyingCriticalExtensions: [ASN1ObjectIdentifier] = [ .X509ExtensionID.basicConstraints, .X509ExtensionID.nameConstraints, .X509ExtensionID.subjectAlternativeName, .X509ExtensionID.keyUsage, ] - public func chainMeetsPolicyRequirements(chain: UnverifiedCertificateChain) -> PolicyEvaluationResult { + func chainMeetsPolicyRequirements(chain: UnverifiedCertificateChain) -> PolicyEvaluationResult { let leaf = chain.leaf // Version MUST be set to 3 diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/PublicKey+verifySignature.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/PublicKey+verifySignature.swift index fc2c9707..8a94948b 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/PublicKey+verifySignature.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/PublicKey+verifySignature.swift @@ -63,4 +63,7 @@ extension SwiftASN1.ASN1ObjectIdentifier { static var certificatePolicies: Self { .init(arrayLiteral: 2, 5, 29, 32) } + static var androidAttestation: Self { + .init(arrayLiteral: 1, 3, 6, 1, 4, 1, 11129, 2, 1, 17) + } } diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/TPMVerificationPolicy.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/TPMVerificationPolicy.swift index 9091c36c..ac7da93e 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/TPMVerificationPolicy.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/TPMVerificationPolicy.swift @@ -18,8 +18,8 @@ import X509 /// Based on https://www.w3.org/TR/webauthn-2/#sctn-tpm-cert-requirements /// Note: we are **not** validating the certificates dates. -public struct TPMVerificationPolicy: VerifierPolicy { - public let verifyingCriticalExtensions: [ASN1ObjectIdentifier] = [ +struct TPMVerificationPolicy: VerifierPolicy { + let verifyingCriticalExtensions: [ASN1ObjectIdentifier] = [ .X509ExtensionID.basicConstraints, .X509ExtensionID.nameConstraints, // The Subject Alternative Name extension MUST be set as defined in [TPMv2-EK-Profile] section 3.2.9. @@ -28,7 +28,7 @@ public struct TPMVerificationPolicy: VerifierPolicy { .certificatePolicies, ] - public func chainMeetsPolicyRequirements(chain: UnverifiedCertificateChain) -> PolicyEvaluationResult { + func chainMeetsPolicyRequirements(chain: UnverifiedCertificateChain) -> PolicyEvaluationResult { let leaf = chain.leaf // Version MUST be set to 3 @@ -39,7 +39,7 @@ public struct TPMVerificationPolicy: VerifierPolicy { } // The Subject Alternative Name extension MUST be set as defined in [TPMv2-EK-Profile] section 3.2.9. - // Note: looks like some TPM attestation certs signed by Microsoft have nither subject nor SAN. + // Note: looks like some TPM attestation certs signed by Microsoft have neither subject nor SAN. /*guard let san = try? leaf.extensions.subjectAlternativeNames else { return .failsToMeetPolicy( reason: "Subject Alternative Name extension MUST be set: \(leaf)" From 760f7a6021d547b129266dd5e1aefba9f6c0f555 Mon Sep 17 00:00:00 2001 From: Matthieu Barthelemy Date: Sat, 4 May 2024 07:49:58 +1000 Subject: [PATCH 13/30] 1 folder per attestation format --- .../Formats/{ => AndroidKey}/AndroidKeyAttestation.swift | 0 .../{ => AndroidKey}/AndroidKeyVerificationPolicy.swift | 3 ++- .../Formats/{ => FidoU2F}/FidoU2FAttestation.swift | 1 + .../Formats/{ => FidoU2F}/FidoU2FVerificationPolicy.swift | 0 .../Formats/{ => Packed}/PackedAttestation.swift | 1 + .../Formats/{ => Packed}/PackedVerificationPolicy.swift | 0 .../Formats/{ => TPM}/TPMAttestation+Structs.swift | 0 .../Registration/Formats/{ => TPM}/TPMAttestation.swift | 1 + .../Formats/{ => TPM}/TPMVerificationPolicy.swift | 6 +++--- 9 files changed, 8 insertions(+), 4 deletions(-) rename Sources/WebAuthn/Ceremonies/Registration/Formats/{ => AndroidKey}/AndroidKeyAttestation.swift (100%) rename Sources/WebAuthn/Ceremonies/Registration/Formats/{ => AndroidKey}/AndroidKeyVerificationPolicy.swift (98%) rename Sources/WebAuthn/Ceremonies/Registration/Formats/{ => FidoU2F}/FidoU2FAttestation.swift (98%) rename Sources/WebAuthn/Ceremonies/Registration/Formats/{ => FidoU2F}/FidoU2FVerificationPolicy.swift (100%) rename Sources/WebAuthn/Ceremonies/Registration/Formats/{ => Packed}/PackedAttestation.swift (98%) rename Sources/WebAuthn/Ceremonies/Registration/Formats/{ => Packed}/PackedVerificationPolicy.swift (100%) rename Sources/WebAuthn/Ceremonies/Registration/Formats/{ => TPM}/TPMAttestation+Structs.swift (100%) rename Sources/WebAuthn/Ceremonies/Registration/Formats/{ => TPM}/TPMAttestation.swift (99%) rename Sources/WebAuthn/Ceremonies/Registration/Formats/{ => TPM}/TPMVerificationPolicy.swift (92%) diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKeyAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKey/AndroidKeyAttestation.swift similarity index 100% rename from Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKeyAttestation.swift rename to Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKey/AndroidKeyAttestation.swift diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKeyVerificationPolicy.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKey/AndroidKeyVerificationPolicy.swift similarity index 98% rename from Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKeyVerificationPolicy.swift rename to Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKey/AndroidKeyVerificationPolicy.swift index 4e4aaf66..1e693744 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKeyVerificationPolicy.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKey/AndroidKeyVerificationPolicy.swift @@ -16,7 +16,7 @@ import Foundation import SwiftASN1 import X509 -/// Based on https://www.w3.org/TR/webauthn-2/#sctn-fido-u2f-attestation +/// Based on https://www.w3.org/TR/webauthn-2/#sctn-android-key-attestation struct AndroidKeyVerificationPolicy: VerifierPolicy { let verifyingCriticalExtensions: [ASN1ObjectIdentifier] = [ .X509ExtensionID.basicConstraints, @@ -34,6 +34,7 @@ struct AndroidKeyVerificationPolicy: VerifierPolicy { func chainMeetsPolicyRequirements(chain: UnverifiedCertificateChain) -> PolicyEvaluationResult { let leaf = chain.leaf + // https://www.w3.org/TR/webauthn-2/#sctn-key-attstn-cert-requirements guard let androidExtension = leaf.extensions[oid: .androidAttestation] else { return .failsToMeetPolicy( reason: "Required extension \(ASN1ObjectIdentifier.androidAttestation) not present: \(leaf)" diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2FAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2F/FidoU2FAttestation.swift similarity index 98% rename from Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2FAttestation.swift rename to Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2F/FidoU2FAttestation.swift index b1cb4220..5e68806b 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2FAttestation.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2F/FidoU2FAttestation.swift @@ -16,6 +16,7 @@ import Foundation import SwiftCBOR import X509 +// https://www.w3.org/TR/webauthn-2/#sctn-fido-u2f-attestation struct FidoU2FAttestation: AttestationProtocol { enum FidoU2FAttestationError: Error { case invalidSig diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2FVerificationPolicy.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2F/FidoU2FVerificationPolicy.swift similarity index 100% rename from Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2FVerificationPolicy.swift rename to Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2F/FidoU2FVerificationPolicy.swift diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/Packed/PackedAttestation.swift similarity index 98% rename from Sources/WebAuthn/Ceremonies/Registration/Formats/PackedAttestation.swift rename to Sources/WebAuthn/Ceremonies/Registration/Formats/Packed/PackedAttestation.swift index 8095dec2..373a9164 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedAttestation.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/Packed/PackedAttestation.swift @@ -17,6 +17,7 @@ import SwiftCBOR import X509 import SwiftASN1 +// https://www.w3.org/TR/webauthn-2/#sctn-packed-attestation struct PackedAttestation: AttestationProtocol { enum PackedAttestationError: Error { case invalidAlg diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedVerificationPolicy.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/Packed/PackedVerificationPolicy.swift similarity index 100% rename from Sources/WebAuthn/Ceremonies/Registration/Formats/PackedVerificationPolicy.swift rename to Sources/WebAuthn/Ceremonies/Registration/Formats/Packed/PackedVerificationPolicy.swift diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/TPMAttestation+Structs.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMAttestation+Structs.swift similarity index 100% rename from Sources/WebAuthn/Ceremonies/Registration/Formats/TPMAttestation+Structs.swift rename to Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMAttestation+Structs.swift diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/TPMAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMAttestation.swift similarity index 99% rename from Sources/WebAuthn/Ceremonies/Registration/Formats/TPMAttestation.swift rename to Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMAttestation.swift index 60b1ad48..ced0a635 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/TPMAttestation.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMAttestation.swift @@ -17,6 +17,7 @@ import SwiftCBOR import X509 import SwiftASN1 +// https://www.w3.org/TR/webauthn-2/#sctn-tpm-attestation struct TPMAttestation: AttestationProtocol { enum TPMAttestationError: Error { case pubAreaInvalid diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/TPMVerificationPolicy.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMVerificationPolicy.swift similarity index 92% rename from Sources/WebAuthn/Ceremonies/Registration/Formats/TPMVerificationPolicy.swift rename to Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMVerificationPolicy.swift index ac7da93e..67cb2791 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/TPMVerificationPolicy.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMVerificationPolicy.swift @@ -34,7 +34,7 @@ struct TPMVerificationPolicy: VerifierPolicy { // Version MUST be set to 3 guard leaf.version == .v3 else { return .failsToMeetPolicy( - reason: "Version MUST be set to 3: \(leaf)" + reason: "Authenticator certificate version must be set to 3: \(leaf)" ) } @@ -49,14 +49,14 @@ struct TPMVerificationPolicy: VerifierPolicy { // The Extended Key Usage extension MUST contain the "joint-iso-itu-t(2) internationalorganizations(23) 133 tcg-kp(8) tcg-kp-AIKCertificate(3)" OID. guard let eku = try? leaf.extensions.extendedKeyUsage, eku.contains(.init(oid: .tcgKpAIKCertificate)) else { return .failsToMeetPolicy( - reason: "Extended Key Usage extension MUST contain the tcg-kp-AIKCertificate OID: \(leaf)" + reason: "Extended Key Usage extension must contain the tcg-kp-AIKCertificate OID: \(leaf)" ) } // The Basic Constraints extension MUST have the CA component set to false guard let basic = try? leaf.extensions.basicConstraints, case .notCertificateAuthority = basic else { return .failsToMeetPolicy( - reason: "The Basic Constraints extension MUST have CA set to false: \(leaf)" + reason: "The Basic Constraints extension must have CA set to false: \(leaf)" ) } return .meetsPolicy From 124e615f2199f8d9938276d0508c71388c92cf78 Mon Sep 17 00:00:00 2001 From: Matthieu Barthelemy Date: Sat, 4 May 2024 08:55:43 +1000 Subject: [PATCH 14/30] Return attestation type --- .../Ceremonies/Registration/AttestationObject.swift | 10 ++++++---- .../Ceremonies/Registration/AttestationResult.swift | 6 +++--- .../Formats/AndroidKey/AndroidKeyAttestation.swift | 4 ++-- .../Registration/Formats/AttestationProtocol.swift | 2 +- .../Formats/FidoU2F/FidoU2FAttestation.swift | 4 ++-- .../Formats/Packed/PackedAttestation.swift | 6 +++--- .../Registration/Formats/TPM/TPMAttestation.swift | 4 ++-- 7 files changed, 19 insertions(+), 17 deletions(-) diff --git a/Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift b/Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift index c9276b0c..2f92dad3 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift @@ -61,6 +61,7 @@ public struct AttestationObject { } let pemRootCertificates = pemRootCertificatesByFormat[format] ?? [] + var attestationType: AttestationResult.AttestationType! var trustedPath: [Certificate] = [] switch format { case .none: @@ -69,7 +70,7 @@ public struct AttestationObject { throw WebAuthnError.attestationStatementMustBeEmpty } case .packed: - trustedPath = try await PackedAttestation.verify( + (attestationType, trustedPath) = try await PackedAttestation.verify( attStmt: attestationStatement, authenticatorData: authenticatorData, clientDataHash: Data(clientDataHash), @@ -77,7 +78,7 @@ public struct AttestationObject { pemRootCertificates: pemRootCertificates ) case .tpm: - trustedPath = try await TPMAttestation.verify( + (attestationType, trustedPath) = try await TPMAttestation.verify( attStmt: attestationStatement, authenticatorData: authenticatorData, clientDataHash: Data(clientDataHash), @@ -85,7 +86,7 @@ public struct AttestationObject { pemRootCertificates: pemRootCertificates ) case .androidKey: - trustedPath = try await AndroidKeyAttestation.verify( + (attestationType, trustedPath) = try await AndroidKeyAttestation.verify( attStmt: attestationStatement, authenticatorData: authenticatorData, clientDataHash: Data(clientDataHash), @@ -94,7 +95,7 @@ public struct AttestationObject { ) // Legacy format used mostly by older authenticators case .fidoU2F: - trustedPath = try await FidoU2FAttestation.verify( + (attestationType, trustedPath) = try await FidoU2FAttestation.verify( attStmt: attestationStatement, authenticatorData: authenticatorData, clientDataHash: Data(clientDataHash), @@ -107,6 +108,7 @@ public struct AttestationObject { return AttestationResult( format: format, + type: attestationType, trustChain: trustedPath, attestedCredentialData: attestedCredentialData ) diff --git a/Sources/WebAuthn/Ceremonies/Registration/AttestationResult.swift b/Sources/WebAuthn/Ceremonies/Registration/AttestationResult.swift index c578f059..6d11c893 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/AttestationResult.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/AttestationResult.swift @@ -15,7 +15,7 @@ import X509 public struct AttestationResult { - enum AttestationType { + public enum AttestationType { /// Attestation key pair validated by device manufacturer CA case basicFull /// Attestation signed by the public key generated during the registration @@ -24,9 +24,9 @@ public struct AttestationResult { case anonCA case none } - //public let aaguid: [UInt8]? + public let format: AttestationFormat - //public let type: AttestationType + public let type: AttestationType public let trustChain: [Certificate] public let attestedCredentialData: AttestedCredentialData diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKey/AndroidKeyAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKey/AndroidKeyAttestation.swift index 91048528..e4ebc2dd 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKey/AndroidKeyAttestation.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKey/AndroidKeyAttestation.swift @@ -34,7 +34,7 @@ struct AndroidKeyAttestation: AttestationProtocol { clientDataHash: Data, credentialPublicKey: CredentialPublicKey, pemRootCertificates: [Data] - ) async throws -> [Certificate] { + ) async throws -> (AttestationResult.AttestationType, [Certificate]) { guard let sigCBOR = attStmt["sig"], case let .byteString(sig) = sigCBOR else { throw AndroidKeyAttestationError.invalidSig } @@ -88,7 +88,7 @@ struct AndroidKeyAttestation: AttestationProtocol { throw AndroidKeyAttestationError.invalidTrustPath } - return chain + return (.basicFull, chain) } } diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/AttestationProtocol.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/AttestationProtocol.swift index f34d90f6..87833c77 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/AttestationProtocol.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/AttestationProtocol.swift @@ -23,5 +23,5 @@ protocol AttestationProtocol { clientDataHash: Data, credentialPublicKey: CredentialPublicKey, pemRootCertificates: [Data] - ) async throws -> [Certificate] + ) async throws -> (AttestationResult.AttestationType, [Certificate]) } diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2F/FidoU2FAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2F/FidoU2FAttestation.swift index 5e68806b..9ff94d1f 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2F/FidoU2FAttestation.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2F/FidoU2FAttestation.swift @@ -35,7 +35,7 @@ struct FidoU2FAttestation: AttestationProtocol { clientDataHash: Data, credentialPublicKey: CredentialPublicKey, pemRootCertificates: [Data] - ) async throws -> [Certificate] { + ) async throws -> (AttestationResult.AttestationType, [Certificate]) { guard let sigCBOR = attStmt["sig"], case let .byteString(sig) = sigCBOR else { throw FidoU2FAttestationError.invalidSig } @@ -94,7 +94,7 @@ struct FidoU2FAttestation: AttestationProtocol { throw FidoU2FAttestationError.invalidVerificationData } - return chain + return (.basicFull, chain) } } diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/Packed/PackedAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/Packed/PackedAttestation.swift index 373a9164..3adb74f4 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/Packed/PackedAttestation.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/Packed/PackedAttestation.swift @@ -37,7 +37,7 @@ struct PackedAttestation: AttestationProtocol { clientDataHash: Data, credentialPublicKey: CredentialPublicKey, pemRootCertificates: [Data] - ) async throws -> [Certificate] { + ) async throws -> (AttestationResult.AttestationType, [Certificate]) { guard let algCBOR = attStmt["alg"], case let .negativeInt(algorithmNegative) = algCBOR, let alg = COSEAlgorithmIdentifier(rawValue: -1 - Int(algorithmNegative)) else { @@ -108,14 +108,14 @@ struct PackedAttestation: AttestationProtocol { } } - return chain + return (.basicFull, chain) } else { // self attestation is in use guard credentialPublicKey.key.algorithm == alg else { throw PackedAttestationError.algDoesNotMatch } try credentialPublicKey.verify(signature: Data(sig), data: verificationData) - return [] + return (.`self`, []) } } } diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMAttestation.swift index ced0a635..052c42ff 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMAttestation.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMAttestation.swift @@ -46,7 +46,7 @@ struct TPMAttestation: AttestationProtocol { clientDataHash: Data, credentialPublicKey: CredentialPublicKey, pemRootCertificates: [Data] - ) async throws -> [Certificate] { + ) async throws -> (AttestationResult.AttestationType, [Certificate]) { // Verify version guard let verCBOR = attStmt["ver"], case let .utf8String(ver) = verCBOR, @@ -154,6 +154,6 @@ struct TPMAttestation: AttestationProtocol { throw TPMAttestationError.extraDataDoesNotMatchAttToBeSignedHash } - return chain + return (.attCA, chain) } } From 1488ec0b7568d51ff4f7054855325bd636341904 Mon Sep 17 00:00:00 2001 From: Matthieu Barthelemy Date: Sat, 4 May 2024 10:02:56 +1000 Subject: [PATCH 15/30] Use X509 Certificate from swift-certificates --- .../Registration/AttestationObject.swift | 16 +++++++--------- .../AndroidKey/AndroidKeyAttestation.swift | 8 +++----- .../Formats/AttestationProtocol.swift | 2 +- .../Formats/FidoU2F/FidoU2FAttestation.swift | 8 +++----- .../Formats/Packed/PackedAttestation.swift | 8 +++----- .../Formats/TPM/TPMAttestation.swift | 8 +++----- .../Registration/RegistrationCredential.swift | 5 +++-- Sources/WebAuthn/WebAuthnManager.swift | 5 +++-- 8 files changed, 26 insertions(+), 34 deletions(-) diff --git a/Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift b/Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift index 2f92dad3..4f5f1f5a 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift @@ -30,10 +30,8 @@ public struct AttestationObject { verificationRequired: Bool, clientDataHash: SHA256.Digest, supportedPublicKeyAlgorithms: [PublicKeyCredentialParameters], - pemRootCertificatesByFormat: [AttestationFormat: [Data]] = [:] + rootCertificatesByFormat: [AttestationFormat: [Certificate]] = [:] ) async throws -> AttestationResult { - // TODO: remove - print("\n•••••••• \(Self.self).verify(): format=\(format) ***\n") let relyingPartyIDHash = SHA256.hash(data: relyingPartyID.data(using: .utf8)!) guard relyingPartyIDHash == authenticatorData.relyingPartyIDHash else { @@ -60,8 +58,8 @@ public struct AttestationObject { throw WebAuthnError.unsupportedCredentialPublicKeyAlgorithm } - let pemRootCertificates = pemRootCertificatesByFormat[format] ?? [] - var attestationType: AttestationResult.AttestationType! + let rootCertificates = rootCertificatesByFormat[format] ?? [] + var attestationType: AttestationResult.AttestationType = .none var trustedPath: [Certificate] = [] switch format { case .none: @@ -75,7 +73,7 @@ public struct AttestationObject { authenticatorData: authenticatorData, clientDataHash: Data(clientDataHash), credentialPublicKey: credentialPublicKey, - pemRootCertificates: pemRootCertificates + rootCertificates: rootCertificates ) case .tpm: (attestationType, trustedPath) = try await TPMAttestation.verify( @@ -83,7 +81,7 @@ public struct AttestationObject { authenticatorData: authenticatorData, clientDataHash: Data(clientDataHash), credentialPublicKey: credentialPublicKey, - pemRootCertificates: pemRootCertificates + rootCertificates: rootCertificates ) case .androidKey: (attestationType, trustedPath) = try await AndroidKeyAttestation.verify( @@ -91,7 +89,7 @@ public struct AttestationObject { authenticatorData: authenticatorData, clientDataHash: Data(clientDataHash), credentialPublicKey: credentialPublicKey, - pemRootCertificates: pemRootCertificates + rootCertificates: rootCertificates ) // Legacy format used mostly by older authenticators case .fidoU2F: @@ -100,7 +98,7 @@ public struct AttestationObject { authenticatorData: authenticatorData, clientDataHash: Data(clientDataHash), credentialPublicKey: credentialPublicKey, - pemRootCertificates: pemRootCertificates + rootCertificates: rootCertificates ) default: throw WebAuthnError.attestationVerificationNotSupported diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKey/AndroidKeyAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKey/AndroidKeyAttestation.swift index e4ebc2dd..6bd2dcf1 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKey/AndroidKeyAttestation.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKey/AndroidKeyAttestation.swift @@ -33,7 +33,7 @@ struct AndroidKeyAttestation: AttestationProtocol { authenticatorData: AuthenticatorData, clientDataHash: Data, credentialPublicKey: CredentialPublicKey, - pemRootCertificates: [Data] + rootCertificates: [Certificate] ) async throws -> (AttestationResult.AttestationType, [Certificate]) { guard let sigCBOR = attStmt["sig"], case let .byteString(sig) = sigCBOR else { throw AndroidKeyAttestationError.invalidSig @@ -52,9 +52,7 @@ struct AndroidKeyAttestation: AttestationProtocol { guard let leafCertificate = x5c.first else { throw AndroidKeyAttestationError.invalidX5C } let intermediates = CertificateStore(x5c[1...]) - let rootCertificates = CertificateStore( - try pemRootCertificates.map { try Certificate(derEncoded: [UInt8]($0)) } - ) + let rootCertificatesStore = CertificateStore(rootCertificates) let verificationData = authenticatorData.rawData + clientDataHash // Verify signature @@ -74,7 +72,7 @@ struct AndroidKeyAttestation: AttestationProtocol { throw AndroidKeyAttestationError.credentialPublicKeyMismatch } - var verifier = Verifier(rootCertificates: rootCertificates) { + var verifier = Verifier(rootCertificates: rootCertificatesStore) { AndroidKeyVerificationPolicy(clientDataHash: clientDataHash) } let verifierResult: VerificationResult = await verifier.validate( diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/AttestationProtocol.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/AttestationProtocol.swift index 87833c77..c770b4a6 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/AttestationProtocol.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/AttestationProtocol.swift @@ -22,6 +22,6 @@ protocol AttestationProtocol { authenticatorData: AuthenticatorData, clientDataHash: Data, credentialPublicKey: CredentialPublicKey, - pemRootCertificates: [Data] + rootCertificates: [Certificate] ) async throws -> (AttestationResult.AttestationType, [Certificate]) } diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2F/FidoU2FAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2F/FidoU2FAttestation.swift index 9ff94d1f..5814d35c 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2F/FidoU2FAttestation.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2F/FidoU2FAttestation.swift @@ -34,7 +34,7 @@ struct FidoU2FAttestation: AttestationProtocol { authenticatorData: AuthenticatorData, clientDataHash: Data, credentialPublicKey: CredentialPublicKey, - pemRootCertificates: [Data] + rootCertificates: [Certificate] ) async throws -> (AttestationResult.AttestationType, [Certificate]) { guard let sigCBOR = attStmt["sig"], case let .byteString(sig) = sigCBOR else { throw FidoU2FAttestationError.invalidSig @@ -57,11 +57,9 @@ struct FidoU2FAttestation: AttestationProtocol { guard let leafCertificate = x5c.first else { throw FidoU2FAttestationError.invalidX5C } let intermediates = CertificateStore(x5c[1...]) - let rootCertificates = CertificateStore( - try pemRootCertificates.map { try Certificate(derEncoded: [UInt8]($0)) } - ) + let rootCertificatesStore = CertificateStore(rootCertificates) - var verifier = Verifier(rootCertificates: rootCertificates) { + var verifier = Verifier(rootCertificates: rootCertificatesStore) { FidoU2FVerificationPolicy() } let verifierResult: VerificationResult = await verifier.validate( diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/Packed/PackedAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/Packed/PackedAttestation.swift index 3adb74f4..8718434e 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/Packed/PackedAttestation.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/Packed/PackedAttestation.swift @@ -36,7 +36,7 @@ struct PackedAttestation: AttestationProtocol { authenticatorData: AuthenticatorData, clientDataHash: Data, credentialPublicKey: CredentialPublicKey, - pemRootCertificates: [Data] + rootCertificates: [Certificate] ) async throws -> (AttestationResult.AttestationType, [Certificate]) { guard let algCBOR = attStmt["alg"], case let .negativeInt(algorithmNegative) = algCBOR, @@ -65,11 +65,9 @@ struct PackedAttestation: AttestationProtocol { } let intermediates = CertificateStore(x5c[1...]) - let rootCertificates = CertificateStore( - try pemRootCertificates.map { try Certificate(derEncoded: [UInt8]($0)) } - ) + let rootCertificatesStore = CertificateStore(rootCertificates) - var verifier = Verifier(rootCertificates: rootCertificates) { + var verifier = Verifier(rootCertificates: rootCertificatesStore) { RFC5280Policy(validationTime: Date()) PackedVerificationPolicy() } diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMAttestation.swift index 052c42ff..abc53ee0 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMAttestation.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMAttestation.swift @@ -45,7 +45,7 @@ struct TPMAttestation: AttestationProtocol { authenticatorData: AuthenticatorData, clientDataHash: Data, credentialPublicKey: CredentialPublicKey, - pemRootCertificates: [Data] + rootCertificates: [Certificate] ) async throws -> (AttestationResult.AttestationType, [Certificate]) { // Verify version guard let verCBOR = attStmt["ver"], @@ -69,11 +69,9 @@ struct TPMAttestation: AttestationProtocol { guard let aikCert = x5c.first else { throw TPMAttestationError.invalidX5c } let intermediates = CertificateStore(x5c[1...]) - let rootCertificates = CertificateStore( - try pemRootCertificates.map { try Certificate(derEncoded: [UInt8]($0)) } - ) + let rootCertificatesStore = CertificateStore(rootCertificates) - var verifier = Verifier(rootCertificates: rootCertificates) { + var verifier = Verifier(rootCertificates: rootCertificatesStore) { RFC5280Policy(validationTime: Date()) TPMVerificationPolicy() } diff --git a/Sources/WebAuthn/Ceremonies/Registration/RegistrationCredential.swift b/Sources/WebAuthn/Ceremonies/Registration/RegistrationCredential.swift index 49b28770..151e8bb3 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/RegistrationCredential.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/RegistrationCredential.swift @@ -14,6 +14,7 @@ import Foundation import Crypto +import X509 /// The unprocessed response received from `navigator.credentials.create()`. /// @@ -86,7 +87,7 @@ struct ParsedCredentialCreationResponse { relyingPartyID: String, relyingPartyOrigin: String, supportedPublicKeyAlgorithms: [PublicKeyCredentialParameters], - pemRootCertificatesByFormat: [AttestationFormat: [Data]] + rootCertificatesByFormat: [AttestationFormat: [Certificate]] ) async throws -> AttestationResult { // Step 7. - 9. try response.clientData.verify( @@ -106,7 +107,7 @@ struct ParsedCredentialCreationResponse { verificationRequired: verifyUser, clientDataHash: hash, supportedPublicKeyAlgorithms: supportedPublicKeyAlgorithms, - pemRootCertificatesByFormat: pemRootCertificatesByFormat + rootCertificatesByFormat: rootCertificatesByFormat ) // Step 23. diff --git a/Sources/WebAuthn/WebAuthnManager.swift b/Sources/WebAuthn/WebAuthnManager.swift index acb95e58..23bb69d5 100644 --- a/Sources/WebAuthn/WebAuthnManager.swift +++ b/Sources/WebAuthn/WebAuthnManager.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import Foundation +import X509 /// Main entrypoint for WebAuthn operations. /// @@ -91,7 +92,7 @@ public struct WebAuthnManager { credentialCreationData: RegistrationCredential, requireUserVerification: Bool = false, supportedPublicKeyAlgorithms: [PublicKeyCredentialParameters] = .supported, - pemRootCertificatesByFormat: [AttestationFormat: [Data]] = [:], + rootCertificatesByFormat: [AttestationFormat: [Certificate]] = [:], confirmCredentialIDNotRegisteredYet: (String) async throws -> Bool ) async throws -> Credential { let parsedData = try ParsedCredentialCreationResponse(from: credentialCreationData) @@ -101,7 +102,7 @@ public struct WebAuthnManager { relyingPartyID: configuration.relyingPartyID, relyingPartyOrigin: configuration.relyingPartyOrigin, supportedPublicKeyAlgorithms: supportedPublicKeyAlgorithms, - pemRootCertificatesByFormat: pemRootCertificatesByFormat + rootCertificatesByFormat: rootCertificatesByFormat ) // TODO: Step 18. -> Verify client extensions From 57d05adc418a712c6e426474917a7b0d2a6f1383 Mon Sep 17 00:00:00 2001 From: Matthieu Barthelemy Date: Sat, 4 May 2024 13:15:13 +1000 Subject: [PATCH 16/30] Throw proper WebAuthnErrors --- .../AndroidKey/AndroidKeyAttestation.swift | 24 ++++------- .../Formats/FidoU2F/FidoU2FAttestation.swift | 25 ++++-------- .../Formats/Packed/PackedAttestation.swift | 40 ++++++------------- .../Formats/TPM/TPMAttestation.swift | 19 ++++----- Sources/WebAuthn/WebAuthnError.swift | 26 ++++++++++++ 5 files changed, 60 insertions(+), 74 deletions(-) diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKey/AndroidKeyAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKey/AndroidKeyAttestation.swift index 6bd2dcf1..a6000a38 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKey/AndroidKeyAttestation.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKey/AndroidKeyAttestation.swift @@ -18,16 +18,6 @@ import X509 // https://www.w3.org/TR/webauthn-2/#sctn-android-key-attestation struct AndroidKeyAttestation: AttestationProtocol { - enum AndroidKeyAttestationError: Error { - case invalidSig - case invalidX5C - case invalidTrustPath - /// Authenticator data cannot be verified - case invalidVerificationData - /// The authenticator certificate public key does not match the attested data public key - case credentialPublicKeyMismatch - } - static func verify( attStmt: CBOR, authenticatorData: AuthenticatorData, @@ -36,21 +26,21 @@ struct AndroidKeyAttestation: AttestationProtocol { rootCertificates: [Certificate] ) async throws -> (AttestationResult.AttestationType, [Certificate]) { guard let sigCBOR = attStmt["sig"], case let .byteString(sig) = sigCBOR else { - throw AndroidKeyAttestationError.invalidSig + throw WebAuthnError.invalidSignature } guard let x5cCBOR = attStmt["x5c"], case let .array(x5cCBOR) = x5cCBOR else { - throw AndroidKeyAttestationError.invalidX5C + throw WebAuthnError.invalidAttestationCertificate } let x5c: [Certificate] = try x5cCBOR.map { guard case let .byteString(certificate) = $0 else { - throw AndroidKeyAttestationError.invalidX5C + throw WebAuthnError.invalidAttestationCertificate } return try Certificate(derEncoded: certificate) } - guard let leafCertificate = x5c.first else { throw AndroidKeyAttestationError.invalidX5C } + guard let leafCertificate = x5c.first else { throw WebAuthnError.invalidAttestationCertificate } let intermediates = CertificateStore(x5c[1...]) let rootCertificatesStore = CertificateStore(rootCertificates) @@ -61,7 +51,7 @@ struct AndroidKeyAttestation: AttestationProtocol { Data(sig), algorithm: leafCertificate.signatureAlgorithm, data: verificationData) else { - throw AndroidKeyAttestationError.invalidVerificationData + throw WebAuthnError.invalidVerificationData } // We need to verify that the authenticator certificate's public key matches the public key present in @@ -69,7 +59,7 @@ struct AndroidKeyAttestation: AttestationProtocol { // We can't directly compare two public keys, so instead we verify the signature with both keys: // the authenticator cert (previous step above) and credentialPublicKey (below). guard let _ = try? credentialPublicKey.verify(signature: Data(sig), data: verificationData) else { - throw AndroidKeyAttestationError.credentialPublicKeyMismatch + throw WebAuthnError.attestationPublicKeyMismatch } var verifier = Verifier(rootCertificates: rootCertificatesStore) { @@ -83,7 +73,7 @@ struct AndroidKeyAttestation: AttestationProtocol { } ) guard case .validCertificate(let chain) = verifierResult else { - throw AndroidKeyAttestationError.invalidTrustPath + throw WebAuthnError.invalidTrustPath } return (.basicFull, chain) diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2F/FidoU2FAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2F/FidoU2FAttestation.swift index 5814d35c..16e6c8ff 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2F/FidoU2FAttestation.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2F/FidoU2FAttestation.swift @@ -18,17 +18,6 @@ import X509 // https://www.w3.org/TR/webauthn-2/#sctn-fido-u2f-attestation struct FidoU2FAttestation: AttestationProtocol { - enum FidoU2FAttestationError: Error { - case invalidSig - case invalidX5C - case invalidTrustPath - // attestation cert can only have a ecdsaWithSHA256 signature - case invalidLeafCertificateSigType - case invalidAttestationKeyType - // Authenticator data cannot be verified - case invalidVerificationData - } - static func verify( attStmt: CBOR, authenticatorData: AuthenticatorData, @@ -37,25 +26,25 @@ struct FidoU2FAttestation: AttestationProtocol { rootCertificates: [Certificate] ) async throws -> (AttestationResult.AttestationType, [Certificate]) { guard let sigCBOR = attStmt["sig"], case let .byteString(sig) = sigCBOR else { - throw FidoU2FAttestationError.invalidSig + throw WebAuthnError.invalidSignature } guard case let .ec2(key) = credentialPublicKey, key.algorithm == .algES256 else { - throw FidoU2FAttestationError.invalidAttestationKeyType + throw WebAuthnError.invalidAttestationPublicKeyType } guard let x5cCBOR = attStmt["x5c"], case let .array(x5cCBOR) = x5cCBOR else { - throw FidoU2FAttestationError.invalidX5C + throw WebAuthnError.invalidAttestationCertificate } let x5c: [Certificate] = try x5cCBOR.map { guard case let .byteString(certificate) = $0 else { - throw FidoU2FAttestationError.invalidX5C + throw WebAuthnError.invalidAttestationCertificate } return try Certificate(derEncoded: certificate) } - guard let leafCertificate = x5c.first else { throw FidoU2FAttestationError.invalidX5C } + guard let leafCertificate = x5c.first else { throw WebAuthnError.invalidAttestationCertificate } let intermediates = CertificateStore(x5c[1...]) let rootCertificatesStore = CertificateStore(rootCertificates) @@ -67,7 +56,7 @@ struct FidoU2FAttestation: AttestationProtocol { intermediates: intermediates ) guard case .validCertificate(let chain) = verifierResult else { - throw FidoU2FAttestationError.invalidTrustPath + throw WebAuthnError.invalidTrustPath } // With U2F, the public key used when calculating the signature (`sig`) was encoded in ANSI X9.62 format @@ -89,7 +78,7 @@ struct FidoU2FAttestation: AttestationProtocol { Data(sig), algorithm: leafCertificate.signatureAlgorithm, data: verificationData) else { - throw FidoU2FAttestationError.invalidVerificationData + throw WebAuthnError.invalidVerificationData } return (.basicFull, chain) diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/Packed/PackedAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/Packed/PackedAttestation.swift index 8718434e..7eb81d7c 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/Packed/PackedAttestation.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/Packed/PackedAttestation.swift @@ -19,18 +19,6 @@ import SwiftASN1 // https://www.w3.org/TR/webauthn-2/#sctn-packed-attestation struct PackedAttestation: AttestationProtocol { - enum PackedAttestationError: Error { - case invalidAlg - case invalidSig - case invalidX5C - case invalidTrustPath - case algDoesNotMatch - // Authenticator data cannot be verified - case invalidVerificationData - case invalidCertAaguid - case aaguidMismatch - } - static func verify( attStmt: CBOR, authenticatorData: AuthenticatorData, @@ -41,26 +29,26 @@ struct PackedAttestation: AttestationProtocol { guard let algCBOR = attStmt["alg"], case let .negativeInt(algorithmNegative) = algCBOR, let alg = COSEAlgorithmIdentifier(rawValue: -1 - Int(algorithmNegative)) else { - throw PackedAttestationError.invalidAlg + throw WebAuthnError.invalidAttestationSignatureAlgorithm } guard let sigCBOR = attStmt["sig"], case let .byteString(sig) = sigCBOR else { - throw PackedAttestationError.invalidSig + throw WebAuthnError.invalidSignature } let verificationData = authenticatorData.rawData + clientDataHash if let x5cCBOR = attStmt["x5c"] { guard case let .array(x5cCBOR) = x5cCBOR else { - throw PackedAttestationError.invalidX5C + throw WebAuthnError.invalidAttestationCertificate } let x5c: [Certificate] = try x5cCBOR.map { guard case let .byteString(certificate) = $0 else { - throw PackedAttestationError.invalidX5C + throw WebAuthnError.invalidAttestationCertificate } return try Certificate(derEncoded: certificate) } - guard let attestnCert = x5c.first else { throw PackedAttestationError.invalidX5C } + guard let attestnCert = x5c.first else { throw WebAuthnError.invalidAttestationCertificate } if x5c.count > 1 { } @@ -76,7 +64,7 @@ struct PackedAttestation: AttestationProtocol { intermediates: intermediates ) guard case .validCertificate(let chain) = verifierResult else { - throw PackedAttestationError.invalidTrustPath + throw WebAuthnError.invalidTrustPath } // 2. Verify signature @@ -88,7 +76,7 @@ struct PackedAttestation: AttestationProtocol { Data(sig), algorithm: attestnCert.signatureAlgorithm, data: verificationData) else { - throw PackedAttestationError.invalidVerificationData + throw WebAuthnError.invalidVerificationData } // Verify that the value of the aaguid extension, if present, matches aaguid in authenticatorData @@ -97,19 +85,17 @@ struct PackedAttestation: AttestationProtocol { ) { // The AAGUID is wrapped in two OCTET STRINGS let derValue = try DER.parse(certAAGUID.value) - guard case .primitive(let certAaguidValue) = derValue.content else { - throw PackedAttestationError.invalidCertAaguid - } - - guard authenticatorData.attestedData?.aaguid == Array(certAaguidValue) else { - throw PackedAttestationError.aaguidMismatch + guard case .primitive(let certAaguidValue) = derValue.content, + authenticatorData.attestedData?.aaguid == Array(certAaguidValue) else { + throw WebAuthnError.aaguidMismatch } } return (.basicFull, chain) - } else { // self attestation is in use + } + else { // self attestation is in use guard credentialPublicKey.key.algorithm == alg else { - throw PackedAttestationError.algDoesNotMatch + throw WebAuthnError.attestationPublicKeyAlgorithmMismatch } try credentialPublicKey.verify(signature: Data(sig), data: verificationData) diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMAttestation.swift index abc53ee0..2be3da91 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMAttestation.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMAttestation.swift @@ -22,19 +22,14 @@ struct TPMAttestation: AttestationProtocol { enum TPMAttestationError: Error { case pubAreaInvalid case certInfoInvalid - /// Invalid or unsupported attestation signature algorithm - case invalidAlg /// Unsupported TPM version case invalidVersion - case invalidX5c case invalidPublicKey - case invalidTrustPath case attestationCertificateSubjectNotEmpty case attestationCertificateMissingTcgKpAIKCertificate - /// A leaf (atte4station) cert must not have the CA flag set. + /// Leaf (attestation) cert must not have the CA flag set. case attestationCertificateIsCA case invalidCertAaguid - case aaguidMismatch case pubAreaExponentDoesNotMatchPubKeyExponent case invalidPubAreaCurve case extraDataDoesNotMatchAttToBeSignedHash @@ -56,18 +51,18 @@ struct TPMAttestation: AttestationProtocol { guard let x5cCBOR = attStmt["x5c"], case let .array(x5cCBOR) = x5cCBOR else { - throw TPMAttestationError.invalidX5c + throw WebAuthnError.invalidAttestationCertificate } // Verify certificate chain let x5c: [Certificate] = try x5cCBOR.map { guard case let .byteString(certificate) = $0 else { - throw TPMAttestationError.invalidX5c + throw WebAuthnError.invalidAttestationCertificate } return try Certificate(derEncoded: certificate) } - guard let aikCert = x5c.first else { throw TPMAttestationError.invalidX5c } + guard let aikCert = x5c.first else { throw WebAuthnError.invalidAttestationCertificate } let intermediates = CertificateStore(x5c[1...]) let rootCertificatesStore = CertificateStore(rootCertificates) @@ -80,7 +75,7 @@ struct TPMAttestation: AttestationProtocol { intermediates: intermediates ) guard case .validCertificate(let chain) = verifierResult else { - throw TPMAttestationError.invalidTrustPath + throw WebAuthnError.invalidTrustPath } // Verify that the value of the aaguid extension, if present, matches aaguid in authenticatorData @@ -94,7 +89,7 @@ struct TPMAttestation: AttestationProtocol { } guard authenticatorData.attestedData?.aaguid == Array(certAaguidValue) else { - throw TPMAttestationError.aaguidMismatch + throw WebAuthnError.aaguidMismatch } } @@ -143,7 +138,7 @@ struct TPMAttestation: AttestationProtocol { guard let algCBOR = attStmt["alg"], case let .negativeInt(algorithmNegative) = algCBOR, let alg = COSEAlgorithmIdentifier(rawValue: -1 - Int(algorithmNegative)) else { - throw TPMAttestationError.invalidAlg + throw WebAuthnError.invalidAttestationSignatureAlgorithm } // Verify that extraData is set to the hash of attToBeSigned using the hash algorithm employed in "alg" diff --git a/Sources/WebAuthn/WebAuthnError.swift b/Sources/WebAuthn/WebAuthnError.swift index 71d5142d..4c4b19c3 100644 --- a/Sources/WebAuthn/WebAuthnError.swift +++ b/Sources/WebAuthn/WebAuthnError.swift @@ -67,6 +67,16 @@ public struct WebAuthnError: Error, Hashable { case invalidExponent case unsupportedCOSEAlgorithmForRSAPublicKey case unsupported + + // MARK: Attestation + case invalidAttestationCertificate + case invalidTrustPath + case invalidAttestationSignatureAlgorithm + case invalidAttestationPublicKeyType + case invalidVerificationData + case attestationPublicKeyAlgorithmMismatch + case aaguidMismatch + case attestationPublicKeyMismatch } let reason: Reason @@ -127,4 +137,20 @@ public struct WebAuthnError: Error, Hashable { public static let invalidExponent = Self(reason: .invalidExponent) public static let unsupportedCOSEAlgorithmForRSAPublicKey = Self(reason: .unsupportedCOSEAlgorithmForRSAPublicKey) public static let unsupported = Self(reason: .unsupported) + + // MARK: Attestation + /// Cannot read or parse attestation certificate from attestation statement + public static let invalidAttestationCertificate = Self(reason: .invalidAttestationCertificate) + /// Cannot authenticator attestation certificate trust chain up to root CA + public static let invalidTrustPath = Self(reason: .invalidTrustPath) + /// Attestation statement algorithm has invalid or unsupported COSE algorithm identifier + public static let invalidAttestationSignatureAlgorithm = Self(reason: .invalidAttestationSignatureAlgorithm) + public static let invalidAttestationPublicKeyType = Self(reason: .invalidAttestationPublicKeyType) + /// Authenticator verification data cannot be validated against attestation signature (authenticator data has been corrupted or tampered with?) + public static let invalidVerificationData = Self(reason: .invalidVerificationData) + public static let attestationPublicKeyAlgorithmMismatch = Self(reason: .attestationPublicKeyAlgorithmMismatch) + /// The authenticator certificate public key does not match the attested data public key + public static let attestationPublicKeyMismatch = Self(reason: .attestationPublicKeyMismatch) + /// Value of AAGUID in authenticator data doesn't match value in attestation certificate + public static let aaguidMismatch = Self(reason: .aaguidMismatch) } From 60b92114c1ea539d531fcd1acac61854b7be998f Mon Sep 17 00:00:00 2001 From: Matthieu Barthelemy Date: Sun, 5 May 2024 09:59:51 +1000 Subject: [PATCH 17/30] Enable EdDSA/Ed25519 credential public keys --- .../Ceremonies/Shared/COSE/COSEAlgorithmIdentifier.swift | 7 +++++-- .../WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift | 5 +---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/WebAuthn/Ceremonies/Shared/COSE/COSEAlgorithmIdentifier.swift b/Sources/WebAuthn/Ceremonies/Shared/COSE/COSEAlgorithmIdentifier.swift index eddaed08..cef64dd7 100644 --- a/Sources/WebAuthn/Ceremonies/Shared/COSE/COSEAlgorithmIdentifier.swift +++ b/Sources/WebAuthn/Ceremonies/Shared/COSE/COSEAlgorithmIdentifier.swift @@ -41,9 +41,10 @@ public enum COSEAlgorithmIdentifier: Int, RawRepresentable, CaseIterable, Encoda /// AlgPS512 RSASSA-PSS with SHA-512 //case algPS512 = -39 // AlgEdDSA EdDSA - //case algEdDSA = -8 + case algEdDSA = -8 - func hashAndCompare(data: Data, to compareHash: Data) -> Bool { + // This is only called for TPM attestations. + func hashAndCompare(data: Data, to compareHash: Data) throws -> Bool { switch self { case .algES256, .algRS256: return SHA256.hash(data: data) == compareHash @@ -53,6 +54,8 @@ public enum COSEAlgorithmIdentifier: Int, RawRepresentable, CaseIterable, Encoda return SHA512.hash(data: data) == compareHash case .algRS1: return Insecure.SHA1.hash(data: data) == compareHash + case .algEdDSA: + throw WebAuthnError.unsupportedCOSEAlgorithm } } } diff --git a/Sources/WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift b/Sources/WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift index 1981c529..44c81d7c 100644 --- a/Sources/WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift +++ b/Sources/WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift @@ -78,8 +78,7 @@ enum CredentialPublicKey { case .rsaKey: self = try .rsa(RSAPublicKeyData(publicKeyObject: publicKeyObject, algorithm: algorithm)) case .octetKey: - throw WebAuthnError.unsupported - // self = try .okp(OKPPublicKey(publicKeyObject: publicKeyObject, algorithm: algorithm)) + self = try .okp(OKPPublicKey(publicKeyObject: publicKeyObject, algorithm: algorithm)) } } @@ -185,7 +184,6 @@ struct RSAPublicKeyData: PublicKey { } func verify(signature: some DataProtocol, data: some DataProtocol) throws { - print("\n•••••• \(Self.self).verify() ") throw WebAuthnError.unsupported // let rsaSignature = _RSA.Signing.RSASignature(derRepresentation: signature) @@ -209,7 +207,6 @@ struct RSAPublicKeyData: PublicKey { } } -/// Currently not in use struct OKPPublicKey: PublicKey { let algorithm: COSEAlgorithmIdentifier let curve: UInt64 From 8d0d3c33634947ad9c78497b7b18fc96e7e4ae24 Mon Sep 17 00:00:00 2001 From: Matthieu Barthelemy Date: Sun, 5 May 2024 10:00:19 +1000 Subject: [PATCH 18/30] Throw proper WebAuthnErrors --- .../AndroidKeyVerificationPolicy.swift | 2 +- .../FidoU2F/FidoU2FVerificationPolicy.swift | 3 +-- .../Packed/PackedVerificationPolicy.swift | 7 +++---- .../Formats/TPM/TPMAttestation.swift | 20 ++++++------------- .../Formats/TPM/TPMVerificationPolicy.swift | 6 +++--- Sources/WebAuthn/WebAuthnError.swift | 9 +++++++++ 6 files changed, 23 insertions(+), 24 deletions(-) diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKey/AndroidKeyVerificationPolicy.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKey/AndroidKeyVerificationPolicy.swift index 1e693744..3e09f9cf 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKey/AndroidKeyVerificationPolicy.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKey/AndroidKeyVerificationPolicy.swift @@ -16,7 +16,7 @@ import Foundation import SwiftASN1 import X509 -/// Based on https://www.w3.org/TR/webauthn-2/#sctn-android-key-attestation +// Based on https://www.w3.org/TR/webauthn-2/#sctn-android-key-attestation struct AndroidKeyVerificationPolicy: VerifierPolicy { let verifyingCriticalExtensions: [ASN1ObjectIdentifier] = [ .X509ExtensionID.basicConstraints, diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2F/FidoU2FVerificationPolicy.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2F/FidoU2FVerificationPolicy.swift index 732cfb1b..38834911 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2F/FidoU2FVerificationPolicy.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2F/FidoU2FVerificationPolicy.swift @@ -16,7 +16,7 @@ import Foundation import SwiftASN1 import X509 -/// Based on https://www.w3.org/TR/webauthn-2/#sctn-fido-u2f-attestation +// Based on https://www.w3.org/TR/webauthn-2/#sctn-fido-u2f-attestation struct FidoU2FVerificationPolicy: VerifierPolicy { let verifyingCriticalExtensions: [ASN1ObjectIdentifier] = [ .X509ExtensionID.basicConstraints, @@ -26,7 +26,6 @@ struct FidoU2FVerificationPolicy: VerifierPolicy { ] func chainMeetsPolicyRequirements(chain: UnverifiedCertificateChain) -> PolicyEvaluationResult { - // Check that x5c has exactly one element guard chain.count == 1 else { return .failsToMeetPolicy( diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/Packed/PackedVerificationPolicy.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/Packed/PackedVerificationPolicy.swift index 7f9591cc..4140be69 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/Packed/PackedVerificationPolicy.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/Packed/PackedVerificationPolicy.swift @@ -16,8 +16,7 @@ import Foundation import SwiftASN1 import X509 -/// Based on https://www.w3.org/TR/webauthn-2/#sctn-packed-attestation-cert-requirements -/// Note: we are **not** validating the certificates dates. +// Based on https://www.w3.org/TR/webauthn-2/#sctn-packed-attestation-cert-requirements struct PackedVerificationPolicy: VerifierPolicy { let verifyingCriticalExtensions: [ASN1ObjectIdentifier] = [ .X509ExtensionID.basicConstraints, @@ -32,14 +31,14 @@ struct PackedVerificationPolicy: VerifierPolicy { // Version MUST be set to 3 guard leaf.version == .v3 else { return .failsToMeetPolicy( - reason: "Version MUST be set to 3: \(leaf)" + reason: "Version must be set to 3: \(leaf)" ) } // The Basic Constraints extension MUST have the CA component set to false guard let basic = try? leaf.extensions.basicConstraints, case .notCertificateAuthority = basic else { return .failsToMeetPolicy( - reason: "The Basic Constraints extension MUST have CA set to false: \(leaf)" + reason: "The Basic Constraints extension must have CA set to false: \(leaf)" ) } return .meetsPolicy diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMAttestation.swift index 2be3da91..0be3a0c0 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMAttestation.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMAttestation.swift @@ -20,18 +20,10 @@ import SwiftASN1 // https://www.w3.org/TR/webauthn-2/#sctn-tpm-attestation struct TPMAttestation: AttestationProtocol { enum TPMAttestationError: Error { - case pubAreaInvalid case certInfoInvalid - /// Unsupported TPM version - case invalidVersion - case invalidPublicKey - case attestationCertificateSubjectNotEmpty case attestationCertificateMissingTcgKpAIKCertificate - /// Leaf (attestation) cert must not have the CA flag set. - case attestationCertificateIsCA case invalidCertAaguid case pubAreaExponentDoesNotMatchPubKeyExponent - case invalidPubAreaCurve case extraDataDoesNotMatchAttToBeSignedHash } @@ -46,7 +38,7 @@ struct TPMAttestation: AttestationProtocol { guard let verCBOR = attStmt["ver"], case let .utf8String(ver) = verCBOR, ver == "2.0" else { - throw TPMAttestationError.invalidVersion + throw WebAuthnError.tpmInvalidVersion } guard let x5cCBOR = attStmt["x5c"], @@ -97,13 +89,13 @@ struct TPMAttestation: AttestationProtocol { guard let pubAreaCBOR = attStmt["pubArea"], case let .byteString(pubAreaRaw) = pubAreaCBOR, let pubArea = PubArea(from: Data(pubAreaRaw)) else { - throw TPMAttestationError.pubAreaInvalid + throw WebAuthnError.tpmInvalidPubArea } switch pubArea.parameters { case let .rsa(rsaParameters): guard case let .rsa(rsaPublicKeyData) = credentialPublicKey, Array(pubArea.unique.data) == rsaPublicKeyData.n else { - throw TPMAttestationError.invalidPublicKey + throw WebAuthnError.tpmInvalidPubAreaPublicKey } var pubAreaExponent: Int = rsaParameters.exponent.toInteger(endian: .big) if pubAreaExponent == 0 { @@ -118,12 +110,12 @@ struct TPMAttestation: AttestationProtocol { case let .ecc(eccParameters): guard case let .ec2(ec2PublicKeyData) = credentialPublicKey, Array(pubArea.unique.data) == ec2PublicKeyData.rawRepresentation else { - throw TPMAttestationError.invalidPublicKey + throw WebAuthnError.tpmInvalidPubAreaPublicKey } guard let pubAreaCrv = COSECurve(from: eccParameters.curveID), pubAreaCrv == ec2PublicKeyData.curve else { - throw TPMAttestationError.invalidPubAreaCurve + throw WebAuthnError.tpmInvalidPubAreaCurve } } // Verify certInfo @@ -143,7 +135,7 @@ struct TPMAttestation: AttestationProtocol { // Verify that extraData is set to the hash of attToBeSigned using the hash algorithm employed in "alg" let attToBeSigned = authenticatorData.rawData + clientDataHash - guard alg.hashAndCompare(data: attToBeSigned, to: parsedCertInfo.extraData) else { + guard try alg.hashAndCompare(data: attToBeSigned, to: parsedCertInfo.extraData) else { throw TPMAttestationError.extraDataDoesNotMatchAttToBeSignedHash } diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMVerificationPolicy.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMVerificationPolicy.swift index 67cb2791..2307fb6a 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMVerificationPolicy.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMVerificationPolicy.swift @@ -16,8 +16,7 @@ import Foundation import SwiftASN1 import X509 -/// Based on https://www.w3.org/TR/webauthn-2/#sctn-tpm-cert-requirements -/// Note: we are **not** validating the certificates dates. +// Based on https://www.w3.org/TR/webauthn-2/#sctn-tpm-cert-requirements struct TPMVerificationPolicy: VerifierPolicy { let verifyingCriticalExtensions: [ASN1ObjectIdentifier] = [ .X509ExtensionID.basicConstraints, @@ -40,7 +39,8 @@ struct TPMVerificationPolicy: VerifierPolicy { // The Subject Alternative Name extension MUST be set as defined in [TPMv2-EK-Profile] section 3.2.9. // Note: looks like some TPM attestation certs signed by Microsoft have neither subject nor SAN. - /*guard let san = try? leaf.extensions.subjectAlternativeNames else { + // I'm unable to find sample TPM attestation payloads that actually pass this verification. + /*guard let _ = try? leaf.extensions.subjectAlternativeNames else { return .failsToMeetPolicy( reason: "Subject Alternative Name extension MUST be set: \(leaf)" ) diff --git a/Sources/WebAuthn/WebAuthnError.swift b/Sources/WebAuthn/WebAuthnError.swift index 4c4b19c3..b17964d4 100644 --- a/Sources/WebAuthn/WebAuthnError.swift +++ b/Sources/WebAuthn/WebAuthnError.swift @@ -77,6 +77,10 @@ public struct WebAuthnError: Error, Hashable { case attestationPublicKeyAlgorithmMismatch case aaguidMismatch case attestationPublicKeyMismatch + case tpmInvalidVersion + case tpmInvalidPubArea + case tpmInvalidPubAreaPublicKey + case tpmInvalidPubAreaCurve } let reason: Reason @@ -153,4 +157,9 @@ public struct WebAuthnError: Error, Hashable { public static let attestationPublicKeyMismatch = Self(reason: .attestationPublicKeyMismatch) /// Value of AAGUID in authenticator data doesn't match value in attestation certificate public static let aaguidMismatch = Self(reason: .aaguidMismatch) + /// Invalid TPM version + public static let tpmInvalidVersion = Self(reason: .tpmInvalidVersion) + public static let tpmInvalidPubArea = Self(reason: .tpmInvalidPubArea) + public static let tpmInvalidPubAreaPublicKey = Self(reason: .tpmInvalidPubAreaPublicKey) + public static let tpmInvalidPubAreaCurve = Self(reason: .tpmInvalidPubAreaCurve) } From ff94cd6d64a5881dc4eb0f36d1146b0594547cf4 Mon Sep 17 00:00:00 2001 From: Matthieu Barthelemy Date: Sun, 5 May 2024 10:16:32 +1000 Subject: [PATCH 19/30] Enable EdDSA/Ed25519 credential public keys --- Sources/WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift | 5 ++++- Tests/WebAuthnTests/WebAuthnManagerIntegrationTests.swift | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Sources/WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift b/Sources/WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift index 44c81d7c..af9b3d66 100644 --- a/Sources/WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift +++ b/Sources/WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift @@ -228,6 +228,9 @@ struct OKPPublicKey: PublicKey { } func verify(signature: some DataProtocol, data: some DataProtocol) throws { - throw WebAuthnError.unsupported + let pkey = try Curve25519.Signing.PublicKey(rawRepresentation: self.xCoordinate) + guard pkey.isValidSignature(signature, for: data) else { + throw WebAuthnError.invalidSignature + } } } diff --git a/Tests/WebAuthnTests/WebAuthnManagerIntegrationTests.swift b/Tests/WebAuthnTests/WebAuthnManagerIntegrationTests.swift index 345f4cd3..64003ff0 100644 --- a/Tests/WebAuthnTests/WebAuthnManagerIntegrationTests.swift +++ b/Tests/WebAuthnTests/WebAuthnManagerIntegrationTests.swift @@ -79,7 +79,7 @@ final class WebAuthnManagerIntegrationTests: XCTestCase { credentialCreationData: registrationResponse, requireUserVerification: true, supportedPublicKeyAlgorithms: publicKeyCredentialParameters, - pemRootCertificatesByFormat: [:], + rootCertificatesByFormat: [:], confirmCredentialIDNotRegisteredYet: { _ in true } ) From c63796ce41c604aecc1bd5028e01a617923d6ba2 Mon Sep 17 00:00:00 2001 From: Matthieu Barthelemy Date: Sun, 5 May 2024 10:49:15 +1000 Subject: [PATCH 20/30] Comment out public key verifySignature for RSA since unable to test it --- .../Registration/Formats/PublicKey+verifySignature.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/PublicKey+verifySignature.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/PublicKey+verifySignature.swift index 8a94948b..21cd1a05 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/PublicKey+verifySignature.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/PublicKey+verifySignature.swift @@ -41,14 +41,14 @@ extension Certificate.PublicKey { let signature = try P521.Signing.ECDSASignature(derRepresentation: signature) return key.isValidSignature(signature, for: data) // This hasn't been tested - case .sha1WithRSAEncryption, .sha256WithRSAEncryption, .sha384WithRSAEncryption, .sha512WithRSAEncryption: + /*case .sha1WithRSAEncryption, .sha256WithRSAEncryption, .sha384WithRSAEncryption, .sha512WithRSAEncryption: guard let key = _RSA.Signing.PublicKey(self) else { return false } let signature = _RSA.Signing.RSASignature(rawRepresentation: signature) - return key.isValidSignature(signature, for: data) + return key.isValidSignature(signature, for: data)*/ default: // Should we return more explicit info (signature alg not supported) in that case? - return false + throw WebAuthnError.unsupported } } } From e6465cf3780c56d74bf8d0b6d62cd159d93e3fa1 Mon Sep 17 00:00:00 2001 From: Matthieu Barthelemy Date: Sun, 5 May 2024 11:53:44 +1000 Subject: [PATCH 21/30] Validate EDDSA algorithm --- .../Ceremonies/Shared/CredentialPublicKey.swift | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Sources/WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift b/Sources/WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift index af9b3d66..ef7e8b1c 100644 --- a/Sources/WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift +++ b/Sources/WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift @@ -228,9 +228,15 @@ struct OKPPublicKey: PublicKey { } func verify(signature: some DataProtocol, data: some DataProtocol) throws { - let pkey = try Curve25519.Signing.PublicKey(rawRepresentation: self.xCoordinate) - guard pkey.isValidSignature(signature, for: data) else { - throw WebAuthnError.invalidSignature + switch algorithm { + case .algEdDSA: + let pkey = try Curve25519.Signing.PublicKey(rawRepresentation: self.xCoordinate) + guard pkey.isValidSignature(signature, for: data) else { + throw WebAuthnError.invalidSignature + } + default: + throw WebAuthnError.unsupportedCOSEAlgorithm } + } } From fe72a7fa7dd75e265d6011eb5e9aadc80eb5cfb3 Mon Sep 17 00:00:00 2001 From: Matthieu Barthelemy Date: Sun, 5 May 2024 11:53:57 +1000 Subject: [PATCH 22/30] Throw proper WebAuthnErrors --- .../Formats/TPM/TPMAttestation.swift | 16 ++++------------ Sources/WebAuthn/WebAuthnError.swift | 8 ++++++++ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMAttestation.swift index 0be3a0c0..f05249b6 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMAttestation.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMAttestation.swift @@ -19,14 +19,6 @@ import SwiftASN1 // https://www.w3.org/TR/webauthn-2/#sctn-tpm-attestation struct TPMAttestation: AttestationProtocol { - enum TPMAttestationError: Error { - case certInfoInvalid - case attestationCertificateMissingTcgKpAIKCertificate - case invalidCertAaguid - case pubAreaExponentDoesNotMatchPubKeyExponent - case extraDataDoesNotMatchAttToBeSignedHash - } - static func verify( attStmt: CBOR, authenticatorData: AuthenticatorData, @@ -77,7 +69,7 @@ struct TPMAttestation: AttestationProtocol { // The AAGUID is wrapped in two OCTET STRINGS let derValue = try DER.parse(certAAGUID.value) guard case .primitive(let certAaguidValue) = derValue.content else { - throw TPMAttestationError.invalidCertAaguid + throw WebAuthnError.tpmInvalidCertAaguid } guard authenticatorData.attestedData?.aaguid == Array(certAaguidValue) else { @@ -105,7 +97,7 @@ struct TPMAttestation: AttestationProtocol { let pubKeyExponent: Int = rsaPublicKeyData.e.toInteger(endian: .big) guard pubAreaExponent == pubKeyExponent else { - throw TPMAttestationError.pubAreaExponentDoesNotMatchPubKeyExponent + throw WebAuthnError.tpmPubAreaExponentDoesNotMatchPubKeyExponent } case let .ecc(eccParameters): guard case let .ec2(ec2PublicKeyData) = credentialPublicKey, @@ -122,7 +114,7 @@ struct TPMAttestation: AttestationProtocol { guard let certInfoCBOR = attStmt["certInfo"], case let .byteString(certInfo) = certInfoCBOR, let parsedCertInfo = CertInfo(fromBytes: Data(certInfo)) else { - throw TPMAttestationError.certInfoInvalid + throw WebAuthnError.tpmCertInfoInvalid } try parsedCertInfo.verify(pubArea: Data(pubAreaRaw)) @@ -136,7 +128,7 @@ struct TPMAttestation: AttestationProtocol { // Verify that extraData is set to the hash of attToBeSigned using the hash algorithm employed in "alg" let attToBeSigned = authenticatorData.rawData + clientDataHash guard try alg.hashAndCompare(data: attToBeSigned, to: parsedCertInfo.extraData) else { - throw TPMAttestationError.extraDataDoesNotMatchAttToBeSignedHash + throw WebAuthnError.tpmExtraDataDoesNotMatchAttToBeSignedHash } return (.attCA, chain) diff --git a/Sources/WebAuthn/WebAuthnError.swift b/Sources/WebAuthn/WebAuthnError.swift index b17964d4..603b364c 100644 --- a/Sources/WebAuthn/WebAuthnError.swift +++ b/Sources/WebAuthn/WebAuthnError.swift @@ -81,6 +81,10 @@ public struct WebAuthnError: Error, Hashable { case tpmInvalidPubArea case tpmInvalidPubAreaPublicKey case tpmInvalidPubAreaCurve + case tpmCertInfoInvalid + case tpmInvalidCertAaguid + case tpmPubAreaExponentDoesNotMatchPubKeyExponent + case tpmExtraDataDoesNotMatchAttToBeSignedHash } let reason: Reason @@ -162,4 +166,8 @@ public struct WebAuthnError: Error, Hashable { public static let tpmInvalidPubArea = Self(reason: .tpmInvalidPubArea) public static let tpmInvalidPubAreaPublicKey = Self(reason: .tpmInvalidPubAreaPublicKey) public static let tpmInvalidPubAreaCurve = Self(reason: .tpmInvalidPubAreaCurve) + public static let tpmCertInfoInvalid = Self(reason: .tpmCertInfoInvalid) + public static let tpmInvalidCertAaguid = Self(reason: .tpmInvalidCertAaguid) + public static let tpmPubAreaExponentDoesNotMatchPubKeyExponent = Self(reason: .tpmPubAreaExponentDoesNotMatchPubKeyExponent) + public static let tpmExtraDataDoesNotMatchAttToBeSignedHash = (Self: reason: .tpmExtraDataDoesNotMatchAttToBeSignedHash) } From e637a9c0279ee53278e31d538af15beb1fce6295 Mon Sep 17 00:00:00 2001 From: Matthieu Barthelemy Date: Sun, 5 May 2024 12:22:06 +1000 Subject: [PATCH 23/30] Throw proper WebAuthnErrors --- Sources/WebAuthn/WebAuthnError.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/WebAuthn/WebAuthnError.swift b/Sources/WebAuthn/WebAuthnError.swift index 603b364c..240b7e0f 100644 --- a/Sources/WebAuthn/WebAuthnError.swift +++ b/Sources/WebAuthn/WebAuthnError.swift @@ -169,5 +169,5 @@ public struct WebAuthnError: Error, Hashable { public static let tpmCertInfoInvalid = Self(reason: .tpmCertInfoInvalid) public static let tpmInvalidCertAaguid = Self(reason: .tpmInvalidCertAaguid) public static let tpmPubAreaExponentDoesNotMatchPubKeyExponent = Self(reason: .tpmPubAreaExponentDoesNotMatchPubKeyExponent) - public static let tpmExtraDataDoesNotMatchAttToBeSignedHash = (Self: reason: .tpmExtraDataDoesNotMatchAttToBeSignedHash) + public static let tpmExtraDataDoesNotMatchAttToBeSignedHash = Self( reason: .tpmExtraDataDoesNotMatchAttToBeSignedHash) } From f10b5da7a65372f4cdb9f8ba8de26548a02485bd Mon Sep 17 00:00:00 2001 From: Matthieu Barthelemy Date: Mon, 6 May 2024 07:44:31 +1000 Subject: [PATCH 24/30] Fix verifySignature --- .../Ceremonies/Registration/Credential.swift | 2 +- .../AndroidKey/AndroidKeyAttestation.swift | 12 +++--- .../Formats/FidoU2F/FidoU2FAttestation.swift | 3 +- .../Formats/Packed/PackedAttestation.swift | 15 +++---- .../Formats/PublicKey+verifySignature.swift | 37 +++++++++++------ .../Shared/COSE/COSEAlgorithmIdentifier.swift | 12 +++--- .../Shared/CredentialPublicKey.swift | 40 +++++++++---------- .../TestModels/TestCredentialPublicKey.swift | 4 +- 8 files changed, 70 insertions(+), 55 deletions(-) diff --git a/Sources/WebAuthn/Ceremonies/Registration/Credential.swift b/Sources/WebAuthn/Ceremonies/Registration/Credential.swift index 125c7094..4ceaaffa 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Credential.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Credential.swift @@ -40,7 +40,7 @@ public struct Credential { public let isBackedUp: Bool // MARK: Optional content - + public let attestationResult: AttestationResult public let attestationClientDataJSON: CollectedClientData diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKey/AndroidKeyAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKey/AndroidKeyAttestation.swift index a6000a38..0744e492 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKey/AndroidKeyAttestation.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKey/AndroidKeyAttestation.swift @@ -25,6 +25,11 @@ struct AndroidKeyAttestation: AttestationProtocol { credentialPublicKey: CredentialPublicKey, rootCertificates: [Certificate] ) async throws -> (AttestationResult.AttestationType, [Certificate]) { + guard let algCBOR = attStmt["alg"], + case let .negativeInt(algorithmNegative) = algCBOR, + let alg = COSEAlgorithmIdentifier(rawValue: -1 - Int(algorithmNegative)) else { + throw WebAuthnError.invalidAttestationSignatureAlgorithm + } guard let sigCBOR = attStmt["sig"], case let .byteString(sig) = sigCBOR else { throw WebAuthnError.invalidSignature } @@ -49,7 +54,7 @@ struct AndroidKeyAttestation: AttestationProtocol { let leafCertificatePublicKey: Certificate.PublicKey = leafCertificate.publicKey guard try leafCertificatePublicKey.verifySignature( Data(sig), - algorithm: leafCertificate.signatureAlgorithm, + algorithm: alg, data: verificationData) else { throw WebAuthnError.invalidVerificationData } @@ -67,10 +72,7 @@ struct AndroidKeyAttestation: AttestationProtocol { } let verifierResult: VerificationResult = await verifier.validate( leafCertificate: leafCertificate, - intermediates: intermediates, - diagnosticCallback: { result in - print("\n •••• \(Self.self) result=\(result)") - } + intermediates: intermediates ) guard case .validCertificate(let chain) = verifierResult else { throw WebAuthnError.invalidTrustPath diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2F/FidoU2FAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2F/FidoU2FAttestation.swift index 16e6c8ff..4ae0925b 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2F/FidoU2FAttestation.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2F/FidoU2FAttestation.swift @@ -49,6 +49,7 @@ struct FidoU2FAttestation: AttestationProtocol { let rootCertificatesStore = CertificateStore(rootCertificates) var verifier = Verifier(rootCertificates: rootCertificatesStore) { + RFC5280Policy(validationTime: Date()) FidoU2FVerificationPolicy() } let verifierResult: VerificationResult = await verifier.validate( @@ -76,7 +77,7 @@ struct FidoU2FAttestation: AttestationProtocol { let leafCertificatePublicKey: Certificate.PublicKey = leafCertificate.publicKey guard try leafCertificatePublicKey.verifySignature( Data(sig), - algorithm: leafCertificate.signatureAlgorithm, + algorithm: .algES256, data: verificationData) else { throw WebAuthnError.invalidVerificationData } diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/Packed/PackedAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/Packed/PackedAttestation.swift index 7eb81d7c..8d87faea 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/Packed/PackedAttestation.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/Packed/PackedAttestation.swift @@ -34,7 +34,7 @@ struct PackedAttestation: AttestationProtocol { guard let sigCBOR = attStmt["sig"], case let .byteString(sig) = sigCBOR else { throw WebAuthnError.invalidSignature } - + let verificationData = authenticatorData.rawData + clientDataHash if let x5cCBOR = attStmt["x5c"] { @@ -49,9 +49,7 @@ struct PackedAttestation: AttestationProtocol { return try Certificate(derEncoded: certificate) } guard let attestnCert = x5c.first else { throw WebAuthnError.invalidAttestationCertificate } - if x5c.count > 1 { - - } + let intermediates = CertificateStore(x5c[1...]) let rootCertificatesStore = CertificateStore(rootCertificates) @@ -62,11 +60,14 @@ struct PackedAttestation: AttestationProtocol { let verifierResult: VerificationResult = await verifier.validate( leafCertificate: attestnCert, intermediates: intermediates + /*diagnosticCallback: { result in + print("\n •••• \(Self.self) result=\(result)") + }*/ ) guard case .validCertificate(let chain) = verifierResult else { throw WebAuthnError.invalidTrustPath } - + // 2. Verify signature // 2.1 Determine key type (with new Swift ASN.1/ Certificates library) // 2.2 Create corresponding public key object (EC2PublicKey/RSAPublicKey/OKPPublicKey) @@ -74,11 +75,11 @@ struct PackedAttestation: AttestationProtocol { let leafCertificatePublicKey: Certificate.PublicKey = attestnCert.publicKey guard try leafCertificatePublicKey.verifySignature( Data(sig), - algorithm: attestnCert.signatureAlgorithm, + algorithm: alg , data: verificationData) else { throw WebAuthnError.invalidVerificationData } - + // Verify that the value of the aaguid extension, if present, matches aaguid in authenticatorData if let certAAGUID = attestnCert.extensions.first( where: {$0.oid == .idFidoGenCeAaguid} diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/PublicKey+verifySignature.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/PublicKey+verifySignature.swift index 21cd1a05..ea1d3624 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/PublicKey+verifySignature.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/PublicKey+verifySignature.swift @@ -19,35 +19,48 @@ import Crypto import _CryptoExtras extension Certificate.PublicKey { - func verifySignature(_ signature: Data, algorithm: Certificate.SignatureAlgorithm, data: Data) throws -> Bool { + func verifySignature(_ signature: Data, algorithm: COSEAlgorithmIdentifier, data: Data) throws -> Bool { switch algorithm { - - case .ecdsaWithSHA256: + case .algES256: guard let key = P256.Signing.PublicKey(self) else { return false } let signature = try P256.Signing.ECDSASignature(derRepresentation: signature) return key.isValidSignature(signature, for: data) - case .ecdsaWithSHA384: + case .algES384: guard let key = P384.Signing.PublicKey(self) else { return false } let signature = try P384.Signing.ECDSASignature(derRepresentation: signature) return key.isValidSignature(signature, for: data) - case .ecdsaWithSHA512: + case .algES512: guard let key = P521.Signing.PublicKey(self) else { return false } let signature = try P521.Signing.ECDSASignature(derRepresentation: signature) return key.isValidSignature(signature, for: data) - // This hasn't been tested - /*case .sha1WithRSAEncryption, .sha256WithRSAEncryption, .sha384WithRSAEncryption, .sha512WithRSAEncryption: - guard let key = _RSA.Signing.PublicKey(self) else { - return false + case .algPS256, .algPS384, .algPS512, .algRS1, .algRS256, .algRS384, .algRS512: + // We currently have no way to access the publickey `backing` so we try various possibilities + if let key = _RSA.Signing.PublicKey(self) { + let signature = _RSA.Signing.RSASignature(rawRepresentation: signature) + return key.isValidSignature(signature, for: data) + } + else if let key = P256.Signing.PublicKey(self) { + let signature = try P256.Signing.ECDSASignature(derRepresentation: signature) + return key.isValidSignature(signature, for: data) + } + else if let key = P384.Signing.PublicKey(self) { + let signature = try P384.Signing.ECDSASignature(derRepresentation: signature) + return key.isValidSignature(signature, for: data) + } + else if let key = P521.Signing.PublicKey(self) { + let signature = try P521.Signing.ECDSASignature(derRepresentation: signature) + return key.isValidSignature(signature, for: data) + } + else { + throw WebAuthnError.unsupported } - let signature = _RSA.Signing.RSASignature(rawRepresentation: signature) - return key.isValidSignature(signature, for: data)*/ - default: // Should we return more explicit info (signature alg not supported) in that case? + default: throw WebAuthnError.unsupported } } diff --git a/Sources/WebAuthn/Ceremonies/Shared/COSE/COSEAlgorithmIdentifier.swift b/Sources/WebAuthn/Ceremonies/Shared/COSE/COSEAlgorithmIdentifier.swift index cef64dd7..04bef4ee 100644 --- a/Sources/WebAuthn/Ceremonies/Shared/COSE/COSEAlgorithmIdentifier.swift +++ b/Sources/WebAuthn/Ceremonies/Shared/COSE/COSEAlgorithmIdentifier.swift @@ -35,22 +35,22 @@ public enum COSEAlgorithmIdentifier: Int, RawRepresentable, CaseIterable, Encoda /// AlgRS512 RSASSA-PKCS1-v1_5 with SHA-512 case algRS512 = -259 /// AlgPS256 RSASSA-PSS with SHA-256 - //case algPS256 = -37 + case algPS256 = -37 /// AlgPS384 RSASSA-PSS with SHA-384 - //case algPS384 = -38 + case algPS384 = -38 /// AlgPS512 RSASSA-PSS with SHA-512 - //case algPS512 = -39 + case algPS512 = -39 // AlgEdDSA EdDSA case algEdDSA = -8 // This is only called for TPM attestations. func hashAndCompare(data: Data, to compareHash: Data) throws -> Bool { switch self { - case .algES256, .algRS256: + case .algES256, .algRS256, .algPS256: return SHA256.hash(data: data) == compareHash - case .algES384, .algRS384: + case .algES384, .algRS384, .algPS384: return SHA384.hash(data: data) == compareHash - case .algES512, .algRS512: + case .algES512, .algRS512, .algPS512: return SHA512.hash(data: data) == compareHash case .algRS1: return Insecure.SHA1.hash(data: data) == compareHash diff --git a/Sources/WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift b/Sources/WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift index ef7e8b1c..b3eca0a6 100644 --- a/Sources/WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift +++ b/Sources/WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift @@ -157,7 +157,6 @@ struct EC2PublicKey: PublicKey { } } -/// Currently not in use struct RSAPublicKeyData: PublicKey { let algorithm: COSEAlgorithmIdentifier // swiftlint:disable:next identifier_name @@ -184,26 +183,25 @@ struct RSAPublicKeyData: PublicKey { } func verify(signature: some DataProtocol, data: some DataProtocol) throws { - throw WebAuthnError.unsupported - // let rsaSignature = _RSA.Signing.RSASignature(derRepresentation: signature) - - // var rsaPadding: _RSA.Signing.Padding - // switch algorithm { - // case .algRS1, .algRS256, .algRS384, .algRS512: - // rsaPadding = .insecurePKCS1v1_5 - // case .algPS256, .algPS384, .algPS512: - // rsaPadding = .PSS - // default: - // throw WebAuthnError.unsupportedCOSEAlgorithmForRSAPublicKey - // } - - // guard try _RSA.Signing.PublicKey(rawRepresentation: rawRepresentation).isValidSignature( - // rsaSignature, - // for: data, - // padding: rsaPadding - // ) else { - // throw WebAuthnError.invalidSignature - // } + let rsaSignature = _RSA.Signing.RSASignature(rawRepresentation: signature) + + var rsaPadding: _RSA.Signing.Padding + switch algorithm { + case .algRS1, .algRS256, .algRS384, .algRS512: + rsaPadding = .insecurePKCS1v1_5 + case .algPS256, .algPS384, .algPS512: + rsaPadding = .PSS + default: + throw WebAuthnError.unsupportedCOSEAlgorithmForRSAPublicKey + } + + guard try _RSA.Signing.PublicKey(derRepresentation: rawRepresentation).isValidSignature( + rsaSignature, + for: data, + padding: rsaPadding + ) else { + throw WebAuthnError.invalidSignature + } } } diff --git a/Tests/WebAuthnTests/Utils/TestModels/TestCredentialPublicKey.swift b/Tests/WebAuthnTests/Utils/TestModels/TestCredentialPublicKey.swift index 91599228..13ac7c3e 100644 --- a/Tests/WebAuthnTests/Utils/TestModels/TestCredentialPublicKey.swift +++ b/Tests/WebAuthnTests/Utils/TestModels/TestCredentialPublicKey.swift @@ -60,7 +60,7 @@ struct TestCredentialPublicKeyBuilder { .crv(.p256) .alg(.algES256) .xCoordinate(TestECCKeyPair.publicKeyXCoordinate) - .yCoordiante(TestECCKeyPair.publicKeyYCoordinate) + .yCoordinate(TestECCKeyPair.publicKeyYCoordinate) } func kty(_ kty: COSEKeyType) -> Self { @@ -87,7 +87,7 @@ struct TestCredentialPublicKeyBuilder { return temp } - func yCoordiante(_ yCoordinate: [UInt8]) -> Self { + func yCoordinate(_ yCoordinate: [UInt8]) -> Self { var temp = self temp.wrapped.yCoordinate = .byteString(yCoordinate) return temp From d2c83ebae45d184e7d58b02f2d645176d6523d2e Mon Sep 17 00:00:00 2001 From: Matthieu Barthelemy Date: Fri, 10 May 2024 08:12:18 +1000 Subject: [PATCH 25/30] WIP tests --- .../Registration/AttestationObject.swift | 1 + .../Formats/FidoU2F/FidoU2FAttestation.swift | 8 +- .../FidoU2F/FidoU2FVerificationPolicy.swift | 7 - .../Formats/Packed/PackedAttestation.swift | 13 +- .../Formats/PublicKey+verifySignature.swift | 35 ++- .../Formats/TPM/TPMAttestation.swift | 55 ++-- .../Shared/CredentialPublicKey.swift | 25 +- .../Formats/FidoU2FAttestationTests.swift | 168 +++++++++++ .../Formats/PackedAttestationTests.swift | 272 ++++++++++++++++++ .../Formats/TPMAttestationTests.swift | 110 +++++++ .../TPMAttestationTests/CertInfoTests.swift | 1 + .../TestModels/TestAttestationObject.swift | 6 + .../Utils/TestModels/TestAuthData.swift | 3 +- .../TestModels/TestCredentialPublicKey.swift | 37 +++ .../Utils/TestModels/TestECCKeyPair.swift | 49 ++++ .../Utils/TestModels/TestRSAKeyPair.swift | 129 +++++++++ 16 files changed, 860 insertions(+), 59 deletions(-) create mode 100644 Tests/WebAuthnTests/Formats/FidoU2FAttestationTests.swift create mode 100644 Tests/WebAuthnTests/Formats/PackedAttestationTests.swift create mode 100644 Tests/WebAuthnTests/Formats/TPMAttestationTests.swift create mode 100644 Tests/WebAuthnTests/Utils/TestModels/TestRSAKeyPair.swift diff --git a/Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift b/Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift index 4f5f1f5a..2b5279ac 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift @@ -61,6 +61,7 @@ public struct AttestationObject { let rootCertificates = rootCertificatesByFormat[format] ?? [] var attestationType: AttestationResult.AttestationType = .none var trustedPath: [Certificate] = [] + print("\n •••• \(Self.self).verify() format=\(format)") switch format { case .none: // if format is `none` statement must be empty diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2F/FidoU2FAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2F/FidoU2FAttestation.swift index 4ae0925b..5959e5c3 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2F/FidoU2FAttestation.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2F/FidoU2FAttestation.swift @@ -44,8 +44,12 @@ struct FidoU2FAttestation: AttestationProtocol { return try Certificate(derEncoded: certificate) } + // Check that x5c has exactly one element + guard x5c.count == 1 else { + throw WebAuthnError.invalidTrustPath + } + guard let leafCertificate = x5c.first else { throw WebAuthnError.invalidAttestationCertificate } - let intermediates = CertificateStore(x5c[1...]) let rootCertificatesStore = CertificateStore(rootCertificates) var verifier = Verifier(rootCertificates: rootCertificatesStore) { @@ -54,7 +58,7 @@ struct FidoU2FAttestation: AttestationProtocol { } let verifierResult: VerificationResult = await verifier.validate( leafCertificate: leafCertificate, - intermediates: intermediates + intermediates: .init() ) guard case .validCertificate(let chain) = verifierResult else { throw WebAuthnError.invalidTrustPath diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2F/FidoU2FVerificationPolicy.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2F/FidoU2FVerificationPolicy.swift index 38834911..197a3cd8 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2F/FidoU2FVerificationPolicy.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2F/FidoU2FVerificationPolicy.swift @@ -26,13 +26,6 @@ struct FidoU2FVerificationPolicy: VerifierPolicy { ] func chainMeetsPolicyRequirements(chain: UnverifiedCertificateChain) -> PolicyEvaluationResult { - // Check that x5c has exactly one element - guard chain.count == 1 else { - return .failsToMeetPolicy( - reason: "Authenticator attestation must return exactly 1 certificate, got \(chain.count)" - ) - } - let leaf = chain.leaf // Certificate public key must be an Elliptic Curve (EC) public key over the P-256 curve, guard leaf.signatureAlgorithm == .ecdsaWithSHA256 else { diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/Packed/PackedAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/Packed/PackedAttestation.swift index 8d87faea..ff29f18d 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/Packed/PackedAttestation.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/Packed/PackedAttestation.swift @@ -60,22 +60,16 @@ struct PackedAttestation: AttestationProtocol { let verifierResult: VerificationResult = await verifier.validate( leafCertificate: attestnCert, intermediates: intermediates - /*diagnosticCallback: { result in - print("\n •••• \(Self.self) result=\(result)") - }*/ ) guard case .validCertificate(let chain) = verifierResult else { throw WebAuthnError.invalidTrustPath } // 2. Verify signature - // 2.1 Determine key type (with new Swift ASN.1/ Certificates library) - // 2.2 Create corresponding public key object (EC2PublicKey/RSAPublicKey/OKPPublicKey) - // 2.3 Call verify method on public key with signature + data let leafCertificatePublicKey: Certificate.PublicKey = attestnCert.publicKey guard try leafCertificatePublicKey.verifySignature( Data(sig), - algorithm: alg , + algorithm: alg, data: verificationData) else { throw WebAuthnError.invalidVerificationData } @@ -99,7 +93,10 @@ struct PackedAttestation: AttestationProtocol { throw WebAuthnError.attestationPublicKeyAlgorithmMismatch } - try credentialPublicKey.verify(signature: Data(sig), data: verificationData) + guard (try? credentialPublicKey.verify(signature: Data(sig), data: verificationData)) != nil else { + throw WebAuthnError.invalidVerificationData + } + return (.`self`, []) } } diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/PublicKey+verifySignature.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/PublicKey+verifySignature.swift index ea1d3624..f8a50ee2 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/PublicKey+verifySignature.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/PublicKey+verifySignature.swift @@ -20,6 +20,7 @@ import _CryptoExtras extension Certificate.PublicKey { func verifySignature(_ signature: Data, algorithm: COSEAlgorithmIdentifier, data: Data) throws -> Bool { + print("\n •••• \(Self.self).verifySignature() 1, algorithm=\(algorithm)") switch algorithm { case .algES256: guard let key = P256.Signing.PublicKey(self) else { @@ -27,39 +28,35 @@ extension Certificate.PublicKey { } let signature = try P256.Signing.ECDSASignature(derRepresentation: signature) return key.isValidSignature(signature, for: data) + case .algES384: guard let key = P384.Signing.PublicKey(self) else { return false } let signature = try P384.Signing.ECDSASignature(derRepresentation: signature) return key.isValidSignature(signature, for: data) + case .algES512: guard let key = P521.Signing.PublicKey(self) else { return false } let signature = try P521.Signing.ECDSASignature(derRepresentation: signature) return key.isValidSignature(signature, for: data) - case .algPS256, .algPS384, .algPS512, .algRS1, .algRS256, .algRS384, .algRS512: - // We currently have no way to access the publickey `backing` so we try various possibilities - if let key = _RSA.Signing.PublicKey(self) { - let signature = _RSA.Signing.RSASignature(rawRepresentation: signature) - return key.isValidSignature(signature, for: data) - } - else if let key = P256.Signing.PublicKey(self) { - let signature = try P256.Signing.ECDSASignature(derRepresentation: signature) - return key.isValidSignature(signature, for: data) - } - else if let key = P384.Signing.PublicKey(self) { - let signature = try P384.Signing.ECDSASignature(derRepresentation: signature) - return key.isValidSignature(signature, for: data) - } - else if let key = P521.Signing.PublicKey(self) { - let signature = try P521.Signing.ECDSASignature(derRepresentation: signature) - return key.isValidSignature(signature, for: data) + + case .algRS1, .algRS256, .algRS384, .algRS512: + guard let key = _RSA.Signing.PublicKey(self) else { + return false } - else { - throw WebAuthnError.unsupported + let signature = _RSA.Signing.RSASignature(rawRepresentation: signature) + return key.isValidSignature(signature, for: data, padding: .insecurePKCS1v1_5) + + case .algPS256, .algPS384, .algPS512: + guard let key = _RSA.Signing.PublicKey(self) else { + return false } + let signature = _RSA.Signing.RSASignature(rawRepresentation: signature) + return key.isValidSignature(signature, for: data, padding: .PSS) + default: throw WebAuthnError.unsupported } diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMAttestation.swift index f05249b6..5c86382d 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMAttestation.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMAttestation.swift @@ -77,6 +77,10 @@ struct TPMAttestation: AttestationProtocol { } } + if let pubAreaCBOR = attStmt["pubArea"], case let .byteString(pubAreaRaw) = pubAreaCBOR { + let pubArea = PubArea(from: Data(pubAreaRaw)) + print("\n••• \(Self.self) pubAreaRaw64=\(Data(pubAreaRaw).base64EncodedString())\npubArea=\(pubArea!)") + } // Verify pubArea guard let pubAreaCBOR = attStmt["pubArea"], case let .byteString(pubAreaRaw) = pubAreaCBOR, @@ -85,31 +89,40 @@ struct TPMAttestation: AttestationProtocol { } switch pubArea.parameters { case let .rsa(rsaParameters): - guard case let .rsa(rsaPublicKeyData) = credentialPublicKey, - Array(pubArea.unique.data) == rsaPublicKeyData.n else { - throw WebAuthnError.tpmInvalidPubAreaPublicKey - } - var pubAreaExponent: Int = rsaParameters.exponent.toInteger(endian: .big) - if pubAreaExponent == 0 { - // "When zero, indicates that the exponent is the default of 2^16 + 1" - pubAreaExponent = 65537 - } + if case let .rsa(rsaPublicKeyData) = credentialPublicKey { + print("\n •••• \(Self.self) pubArea.unique.data=\(Array(pubArea.unique.data)), rsaPublicKeyData.n=\(rsaPublicKeyData.n)") + } + guard case let .rsa(rsaPublicKeyData) = credentialPublicKey, + Array(pubArea.unique.data) == rsaPublicKeyData.n else { + throw WebAuthnError.tpmInvalidPubAreaPublicKey + } + var pubAreaExponent: Int = rsaParameters.exponent.toInteger(endian: .big) + if pubAreaExponent == 0 { + // "When zero, indicates that the exponent is the default of 2^16 + 1" + pubAreaExponent = 65537 + } - let pubKeyExponent: Int = rsaPublicKeyData.e.toInteger(endian: .big) - guard pubAreaExponent == pubKeyExponent else { - throw WebAuthnError.tpmPubAreaExponentDoesNotMatchPubKeyExponent - } + let pubKeyExponent: Int = rsaPublicKeyData.e.toInteger(endian: .big) + guard pubAreaExponent == pubKeyExponent else { + throw WebAuthnError.tpmPubAreaExponentDoesNotMatchPubKeyExponent + } case let .ecc(eccParameters): - guard case let .ec2(ec2PublicKeyData) = credentialPublicKey, - Array(pubArea.unique.data) == ec2PublicKeyData.rawRepresentation else { - throw WebAuthnError.tpmInvalidPubAreaPublicKey - } + guard case let .ec2(ec2PublicKeyData) = credentialPublicKey, + Array(pubArea.unique.data) == ec2PublicKeyData.rawRepresentation else { + throw WebAuthnError.tpmInvalidPubAreaPublicKey + } - guard let pubAreaCrv = COSECurve(from: eccParameters.curveID), - pubAreaCrv == ec2PublicKeyData.curve else { - throw WebAuthnError.tpmInvalidPubAreaCurve - } + guard let pubAreaCrv = COSECurve(from: eccParameters.curveID), + pubAreaCrv == ec2PublicKeyData.curve else { + throw WebAuthnError.tpmInvalidPubAreaCurve + } } + + /*if let certInfoCBOR = attStmt["certInfo"], + case let .byteString(certInfo) = certInfoCBOR { + let parsedCertInfo = CertInfo(fromBytes: Data(certInfo)) + print("\n••• \(Self.self) certInfo64=\(Data(certInfo).base64EncodedString())\nparsedCertInfo=\(parsedCertInfo!)") + }*/ // Verify certInfo guard let certInfoCBOR = attStmt["certInfo"], case let .byteString(certInfo) = certInfoCBOR, diff --git a/Sources/WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift b/Sources/WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift index b3eca0a6..9c144715 100644 --- a/Sources/WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift +++ b/Sources/WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift @@ -16,6 +16,7 @@ import Crypto import _CryptoExtras import Foundation import SwiftCBOR +import SwiftASN1 protocol PublicKey { var algorithm: COSEAlgorithmIdentifier { get } @@ -72,6 +73,7 @@ enum CredentialPublicKey { throw WebAuthnError.unsupportedCOSEAlgorithm } + print("\n•••• \(Self.self).init() keyType=\(keyType), algorithm=\(algorithm)") switch keyType { case .ellipticKey: self = try .ec2(EC2PublicKey(publicKeyObject: publicKeyObject, algorithm: algorithm)) @@ -182,6 +184,24 @@ struct RSAPublicKeyData: PublicKey { e = eBytes } + // We receive a "raw" public key but the RSA PublicKey constructor requires a DER-encoded value + private struct RSAPublicKeyDER: DERSerializable { + var n: ArraySlice + var e: ArraySlice + + init(n: [UInt8], e: [UInt8]) { + self.n = ArraySlice(n) + self.e = ArraySlice(e) + } + + func serialize(into coder: inout SwiftASN1.DER.Serializer) throws { + try coder.appendConstructedNode(identifier: .sequence) { coder in + try coder.serialize(self.n) + try coder.serialize(self.e) + } + } + } + func verify(signature: some DataProtocol, data: some DataProtocol) throws { let rsaSignature = _RSA.Signing.RSASignature(rawRepresentation: signature) @@ -195,7 +215,10 @@ struct RSAPublicKeyData: PublicKey { throw WebAuthnError.unsupportedCOSEAlgorithmForRSAPublicKey } - guard try _RSA.Signing.PublicKey(derRepresentation: rawRepresentation).isValidSignature( + var serializer = DER.Serializer() + let keyDER = RSAPublicKeyDER(n: self.n, e: self.e) + try serializer.serialize(keyDER) + guard try _RSA.Signing.PublicKey(derRepresentation: serializer.serializedBytes).isValidSignature( rsaSignature, for: data, padding: rsaPadding diff --git a/Tests/WebAuthnTests/Formats/FidoU2FAttestationTests.swift b/Tests/WebAuthnTests/Formats/FidoU2FAttestationTests.swift new file mode 100644 index 00000000..9a2bca59 --- /dev/null +++ b/Tests/WebAuthnTests/Formats/FidoU2FAttestationTests.swift @@ -0,0 +1,168 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the WebAuthn Swift open source project +// +// Copyright (c) 2023 the WebAuthn Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of WebAuthn Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@testable import WebAuthn +import XCTest +import SwiftCBOR +import Crypto +import X509 + +// swiftlint:disable:next type_body_length +final class RegistrationFidoU2FAttestationTests: XCTestCase { + var webAuthnManager: WebAuthnManager! + + let challenge: [UInt8] = [1, 0, 1] + let relyingPartyDisplayName = "Testy test" + let relyingPartyID = "example.com" + let relyingPartyOrigin = "https://example.com" + static let credentialId = "e0fac9350509f71748d83782ccaf6b4c1462c615c70e255da1344e40887c8fcd".hexadecimal! + let mockClientDataJSONBytes = TestClientDataJSON(challenge: TestConstants.mockChallenge.base64URLEncodedString()).jsonBytes + + override func setUp() { + let configuration = WebAuthnManager.Configuration( + relyingPartyID: relyingPartyID, + relyingPartyName: relyingPartyDisplayName, + relyingPartyOrigin: relyingPartyOrigin + ) + webAuthnManager = .init(configuration: configuration, challengeGenerator: .mock(generate: challenge)) + } + + func testAttestationInvalidVerifData() async throws { + let authData = TestAuthDataBuilder().validMock() + // invalid verification data + let verificationData: [UInt8] = [0x00, 0x01] + let mockCerts = try TestECCKeyPair.certificates() + + let mockAttestationObject = TestAttestationObjectBuilder() + .fmt(.fidoU2F) + .authData(authData) + .attStmt( + .map([ + .utf8String("sig"): .byteString(Array( + try TestECCKeyPair.signature(data: Data(verificationData)) + .derRepresentation + )), + .utf8String("x5c"): .array([.byteString(Array(mockCerts.leaf))]) + ]) + ) + .build() + .cborEncoded + + await assertThrowsError( + try await finishRegistration( + attestationObject: mockAttestationObject, + rootCertificatesByFormat: [.fidoU2F: [mockCerts.ca]] + ), + expect: WebAuthnError.invalidVerificationData + ) + } + + func testAttestationMissingx5c() async throws { + let authData = TestAuthDataBuilder().validMock() + + let mockAttestationObject = TestAttestationObjectBuilder() + .fmt(.fidoU2F) + .authData(authData) + .attStmt( + .map([ + .utf8String("sig"): .byteString(Array( + try TestECCKeyPair.signature(data: Data([0x00, 0x01])) + .derRepresentation + )), + ]) + ) + .build() + .cborEncoded + + await assertThrowsError( + try await finishRegistration( + attestationObject: mockAttestationObject, + rootCertificatesByFormat: [:] + ), + expect: WebAuthnError.invalidAttestationCertificate + ) + } + + func testBasicAttestationSucceeds() async throws { + let mockCerts = try TestECCKeyPair.certificates() + let credentialId: [UInt8] = [0b00000001] + let authData = TestAuthDataBuilder() + .relyingPartyIDHash(fromRelyingPartyID: "example.com") + .flags(0b11000101) + .counter([0b00000000, 0b00000000, 0b00000000, 0b00000000]) + .attestedCredData( + aaguid: [UInt8](repeating: 0, count: 16), + credentialIDLength: [0b00000000, 0b00000001], + credentialID: credentialId, + credentialPublicKey: TestCredentialPublicKeyBuilder().validMock().buildAsByteArray() + ) + .extensions([UInt8](repeating: 0, count: 20)) + + let rpIdHash = SHA256.hash(data: Data(self.relyingPartyID.utf8)) + let clientDataHash = SHA256.hash(data: mockClientDataJSONBytes) + // With U2F, the public key used when calculating the signature (`sig`) is encoded in ANSI X9.62 format + let publicKeyU2F: [UInt8] = [0x04] + TestECCKeyPair.publicKeyXCoordinate + TestECCKeyPair.publicKeyYCoordinate + // Let verificationData be the concatenation of (0x00 || rpIdHash || clientDataHash || credentialId || publicKeyU2F) + let verificationData: [UInt8] = [0x00] + rpIdHash + clientDataHash + credentialId + publicKeyU2F + + let mockAttestationObject = TestAttestationObjectBuilder() + .fmt(.fidoU2F) + .authData(authData) + .attStmt( + .map([ + .utf8String("sig"): .byteString(Array( + try TestECCKeyPair.signature(data: Data(verificationData)) + .derRepresentation + )), + .utf8String("x5c"): .array([.byteString(Array(mockCerts.leaf))]) + ]) + ) + .build() + .cborEncoded + + let credential = try await finishRegistration( + attestationObject: mockAttestationObject, + rootCertificatesByFormat: [.fidoU2F: [mockCerts.ca]] + ) + XCTAssertEqual(credential.attestationResult.format, .fidoU2F) + XCTAssertEqual(credential.attestationResult.type, .basicFull) + XCTAssertEqual(credential.attestationResult.trustChain.count, 2) + } + + private func finishRegistration( + challenge: [UInt8] = TestConstants.mockChallenge, + type: CredentialType = .publicKey, + rawID: [UInt8] = credentialId, + attestationObject: [UInt8], + requireUserVerification: Bool = false, + rootCertificatesByFormat: [AttestationFormat: [Certificate]] = [:], + confirmCredentialIDNotRegisteredYet: (String) async throws -> Bool = { _ in true } + ) async throws -> Credential { + try await webAuthnManager.finishRegistration( + challenge: challenge, + credentialCreationData: RegistrationCredential( + id: rawID.base64URLEncodedString(), + type: type, + rawID: rawID, + attestationResponse: AuthenticatorAttestationResponse( + clientDataJSON: mockClientDataJSONBytes, + attestationObject: attestationObject + ) + ), + requireUserVerification: requireUserVerification, + rootCertificatesByFormat: rootCertificatesByFormat, + confirmCredentialIDNotRegisteredYet: confirmCredentialIDNotRegisteredYet + ) + } +} diff --git a/Tests/WebAuthnTests/Formats/PackedAttestationTests.swift b/Tests/WebAuthnTests/Formats/PackedAttestationTests.swift new file mode 100644 index 00000000..e0407a77 --- /dev/null +++ b/Tests/WebAuthnTests/Formats/PackedAttestationTests.swift @@ -0,0 +1,272 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the WebAuthn Swift open source project +// +// Copyright (c) 2023 the WebAuthn Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of WebAuthn Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@testable import WebAuthn +import XCTest +import SwiftCBOR +import Crypto +import _CryptoExtras +import X509 + +// swiftlint:disable:next type_body_length +final class RegistrationPackedAttestationTests: XCTestCase { + var webAuthnManager: WebAuthnManager! + var authDataECC: TestAuthDataBuilder! + var authDataRSA: TestAuthDataBuilder! + var clientDataHash: SHA256.Digest! + + let challenge: [UInt8] = [1, 0, 1] + let relyingPartyDisplayName = "Testy test" + let relyingPartyID = "example.com" + let relyingPartyOrigin = "https://example.com" + let mockClientDataJSONBytes = TestClientDataJSON(challenge: TestConstants.mockChallenge.base64URLEncodedString()).jsonBytes + + override func setUp() { + let configuration = WebAuthnManager.Configuration( + relyingPartyID: relyingPartyID, + relyingPartyName: relyingPartyDisplayName, + relyingPartyOrigin: relyingPartyOrigin + ) + webAuthnManager = .init(configuration: configuration, challengeGenerator: .mock(generate: challenge)) + let mockCredentialPublicKeyECC = TestCredentialPublicKeyBuilder().validMock().buildAsByteArray() + authDataECC = TestAuthDataBuilder().validMock() + .attestedCredData(credentialPublicKey: mockCredentialPublicKeyECC) + .noExtensionData() + let mockCredentialPublicKeyRSA = TestCredentialPublicKeyBuilder().validMockRSA().buildAsByteArray() + authDataRSA = TestAuthDataBuilder().validMock() + .attestedCredData(credentialPublicKey: mockCredentialPublicKeyRSA) + .noExtensionData() + + clientDataHash = SHA256.hash(data: Data(mockClientDataJSONBytes)) + } + + func testSelfAttestationAlgMismatch() async throws { + let mockAttestationObject = TestAttestationObjectBuilder() + .fmt(.packed) + .authData(authDataECC) + .attStmt( + .map([ + .utf8String("alg"): .negativeInt(UInt64(abs(COSEAlgorithmIdentifier.algES384.rawValue) - 1)), + .utf8String("sig"): .byteString(Array( + try TestECCKeyPair + .signature(data: Data([0x01])).derRepresentation + )), + ]) + ) + .build() + .cborEncoded + + await assertThrowsError( + try await finishRegistration( + attestationObject: mockAttestationObject, + rootCertificatesByFormat: [:] + ), + expect: WebAuthnError.attestationPublicKeyAlgorithmMismatch + ) + } + + func testBasicAttestationInvalidVerifData() async throws { + let verificationData: [UInt8] = [0x01] + let mockCerts = try TestECCKeyPair.certificates() + + let mockAttestationObject = TestAttestationObjectBuilder() + .fmt(.packed) + .authData(authDataECC) + .attStmt( + .map([ + .utf8String("alg"): .negativeInt(UInt64(abs(COSEAlgorithmIdentifier.algES256.rawValue) - 1)), + .utf8String("sig"): .byteString(Array( + try TestECCKeyPair + .signature(data: Data(verificationData)) + .derRepresentation + )), + .utf8String("x5c"): .array([.byteString(Array(mockCerts.leaf))]) + ]) + ) + .build() + .cborEncoded + + await assertThrowsError( + try await finishRegistration( + attestationObject: mockAttestationObject, + rootCertificatesByFormat: [.packed: [mockCerts.ca]] + ), + expect: WebAuthnError.invalidVerificationData + ) + } + + func testBasicAttestationInvalidTrustPath() async throws { + let mockCerts = try TestECCKeyPair.certificates() + let mockAttestationObject = TestAttestationObjectBuilder() + .fmt(.packed) + .authData(authDataECC) + .attStmt( + .map([ + .utf8String("alg"): .negativeInt(UInt64(abs(COSEAlgorithmIdentifier.algES256.rawValue) - 1)), + .utf8String("sig"): .byteString(Array( + try TestECCKeyPair + .signature(data: Data(authDataECC.build().byteArrayRepresentation) + clientDataHash) + .derRepresentation + )), + .utf8String("x5c"): .array([.byteString(Array(mockCerts.leaf))]) + ]) + ) + .build() + .cborEncoded + + await assertThrowsError( + try await finishRegistration( + attestationObject: mockAttestationObject, + rootCertificatesByFormat: [.packed: []] + ), + expect: WebAuthnError.invalidTrustPath + ) + } + + func testSelfAttestationECCSucceeds() async throws { + let mockAttestationObject = TestAttestationObjectBuilder() + .validMock() + .fmt(.packed) + .authData(authDataECC) + .attStmt( + .map([ + .utf8String("alg"): .negativeInt(UInt64(abs(COSEAlgorithmIdentifier.algES256.rawValue) - 1)), + .utf8String("sig"): .byteString(Array( + try TestECCKeyPair + .signature(data: Data(authDataECC.build().byteArrayRepresentation) + clientDataHash) + .derRepresentation + )) + ]) + ) + .build() + .cborEncoded + + let credential = try await finishRegistration(attestationObject: mockAttestationObject) + XCTAssertEqual(credential.attestationResult.format, .packed) + XCTAssertEqual(credential.attestationResult.type, .`self`) + XCTAssertEqual(credential.attestationResult.trustChain, []) + } + + func testBasicAttestationECCSucceeds() async throws { + let mockCerts = try TestECCKeyPair.certificates() + let mockAttestationObject = TestAttestationObjectBuilder() + .validMock() + .fmt(.packed) + .authData(authDataECC) + .attStmt( + .map([ + .utf8String("alg"): .negativeInt(UInt64(abs(COSEAlgorithmIdentifier.algES256.rawValue) - 1)), + .utf8String("sig"): .byteString(Array( + try TestECCKeyPair + .signature(data: Data(authDataECC.build().byteArrayRepresentation) + clientDataHash) + .derRepresentation + )), + .utf8String("x5c"): .array([.byteString(Array(mockCerts.leaf))]) + ]) + ) + .build() + .cborEncoded + + let credential = try await finishRegistration( + attestationObject: mockAttestationObject, + rootCertificatesByFormat: [.packed: [mockCerts.ca]] + ) + XCTAssertEqual(credential.attestationResult.format, .packed) + XCTAssertEqual(credential.attestationResult.type, .basicFull) + XCTAssertEqual(credential.attestationResult.trustChain.count, 2) + } + + func testSelfPackedAttestationRSASucceeds() async throws { + let mockAttestationObject = TestAttestationObjectBuilder() + .validMock() + .fmt(.packed) + .authData(authDataRSA) + .attStmt( + .map([ + .utf8String("alg"): .negativeInt(UInt64(abs(COSEAlgorithmIdentifier.algRS256.rawValue) - 1)), + .utf8String("sig"): .byteString(Array( + try TestRSAKeyPair + .signature(data: Data(authDataRSA.build().byteArrayRepresentation) + clientDataHash) + .rawRepresentation + )) + ]) + ) + .build() + .cborEncoded + + let credential = try await finishRegistration(attestationObject: mockAttestationObject) + + XCTAssertEqual(credential.attestationResult.format, .packed) + XCTAssertEqual(credential.attestationResult.type, .`self`) + XCTAssertEqual(credential.attestationResult.trustChain, []) + } + + func testBasicPackedAttestationRSASucceeds() async throws { + let mockCerts = try TestRSAKeyPair.certificates() + let mockAttestationObject = TestAttestationObjectBuilder() + .validMock() + .fmt(.packed) + .authData(authDataRSA) + .attStmt( + .map([ + .utf8String("alg"): .negativeInt(UInt64(abs(COSEAlgorithmIdentifier.algRS256.rawValue) - 1)), + .utf8String("sig"): .byteString(Array( + try TestRSAKeyPair + .signature(data: Data(authDataRSA.build().byteArrayRepresentation) + clientDataHash) + .rawRepresentation + )), + .utf8String("x5c"): .array([ + .byteString(Array(mockCerts.leaf)) + ]) + ]) + ) + .build() + .cborEncoded + + let credential = try await finishRegistration( + attestationObject: mockAttestationObject, + rootCertificatesByFormat: [.packed: [mockCerts.ca]] + ) + + XCTAssertEqual(credential.attestationResult.format, .packed) + XCTAssertEqual(credential.attestationResult.type, .basicFull) + XCTAssertEqual(credential.attestationResult.trustChain.count, 2) + } + + private func finishRegistration( + challenge: [UInt8] = TestConstants.mockChallenge, + type: CredentialType = .publicKey, + rawID: [UInt8] = "e0fac9350509f71748d83782ccaf6b4c1462c615c70e255da1344e40887c8fcd".hexadecimal!, + attestationObject: [UInt8], + requireUserVerification: Bool = false, + rootCertificatesByFormat: [AttestationFormat: [Certificate]] = [:], + confirmCredentialIDNotRegisteredYet: (String) async throws -> Bool = { _ in true } + ) async throws -> Credential { + try await webAuthnManager.finishRegistration( + challenge: challenge, + credentialCreationData: RegistrationCredential( + id: rawID.base64URLEncodedString(), + type: type, + rawID: rawID, + attestationResponse: AuthenticatorAttestationResponse( + clientDataJSON: mockClientDataJSONBytes, + attestationObject: attestationObject + ) + ), + requireUserVerification: requireUserVerification, + rootCertificatesByFormat: rootCertificatesByFormat, + confirmCredentialIDNotRegisteredYet: confirmCredentialIDNotRegisteredYet + ) + } +} diff --git a/Tests/WebAuthnTests/Formats/TPMAttestationTests.swift b/Tests/WebAuthnTests/Formats/TPMAttestationTests.swift new file mode 100644 index 00000000..0f6b1d3c --- /dev/null +++ b/Tests/WebAuthnTests/Formats/TPMAttestationTests.swift @@ -0,0 +1,110 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the WebAuthn Swift open source project +// +// Copyright (c) 2023 the WebAuthn Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of WebAuthn Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@testable import WebAuthn +import XCTest +import SwiftCBOR +import Crypto +import _CryptoExtras +import X509 + +// swiftlint:disable:next type_body_length +final class RegistrationTPMAttestationTests: XCTestCase { + var webAuthnManager: WebAuthnManager! + + let challenge: [UInt8] = [1, 0, 1] + let relyingPartyDisplayName = "Testy test" + let relyingPartyID = "example.com" + let relyingPartyOrigin = "https://example.com" + let mockClientDataJSONBytes = TestClientDataJSON(challenge: TestConstants.mockChallenge.base64URLEncodedString()).jsonBytes + + override func setUp() { + let configuration = WebAuthnManager.Configuration( + relyingPartyID: relyingPartyID, + relyingPartyName: relyingPartyDisplayName, + relyingPartyOrigin: relyingPartyOrigin + ) + webAuthnManager = .init(configuration: configuration, challengeGenerator: .mock(generate: challenge)) + } + + func testAttCAAttestationRSASucceeds() async throws { + let mockCredentialPublicKey = TestCredentialPublicKeyBuilder().validMockRSA().buildAsByteArray() + let authData = TestAuthDataBuilder().validMock() + .attestedCredData(credentialPublicKey: mockCredentialPublicKey) + .noExtensionData() + let hash = SHA256.hash(data: Data(mockClientDataJSONBytes)) + let mockCerts = try TestECCKeyPair.certificates() + + //let certInfoBytes: [UInt8] = [0xFF, 0x54, 0x43, 0x47] + [UInt8](repeating: 0, count: 80) + // RSA PubArea + let pubArea = Data(base64Encoded: "AAEACwAGBHIAIJ3/y/NsODrmmfuYaNxty4nXFTiEvigDkiwSQVi/rSKuABAAEAgAAAAAAAEAus2NNibjf6n9vIlQiEmunemlDObEtj7Cr2TDtD//tvJS1//fsW5mxHEz7wo+WiBKlVHwm9O1OKggQVHWlsoAG4QHJL82KwApuSPIYzKBpMTJGS8OZF9Eo7R8elX4JLBJcZ7uA3AtoPaai/zHIHXWVdMzRq2DY9Ymps6MU8jnGMr2Y0L/+IFPrfZhHNLqhx7/h+pNt6eJnp7MmhgIZBk1fKHcgcbDaXZ0fCD511jzu7QQ025OJoN1bXJai4UtPkwof0J2epXBJdu8ExPBY8KlXUBvRdTrsp/njQAKtBLn288I0jabg65Y/io+cWP5UuQTBI0FF6j/lOZ81ttk3oV/FQ==")! + let certInfo = Data(base64Encoded: "/1RDR4AXACIAC7fjlRE/X84oQtXc8hucRu9DFXUZD6UhJFkNJ57OM2mJABRqOl417tyWPsLqfhByWFhLi6W+OQAAAAjIGcaOvjsE9fnCGcEBo17KdHVMLNYAIgAL8wrHq55UwHsBdEMDTgPolqcoRsQvpP8QUY07Rjc/ZuoAIgALOeLcnM1NWggIfzd1ct6nAJwvcxnjsbUECgnvAgGp22w=")! + let mockAttestationObject = TestAttestationObjectBuilder() + .validMock() + .fmt(.tpm) + .authData(authData) + .attStmt( + .map([ + .utf8String("ver"): .utf8String("2.0"), + .utf8String("alg"): .negativeInt(UInt64(abs(COSEAlgorithmIdentifier.algRS256.rawValue) - 1)), + .utf8String("sig"): .byteString(Array( + try TestECCKeyPair + .signature(data: Data(authData.build().byteArrayRepresentation) + hash) + .derRepresentation + )), + .utf8String("x5c"): .array([.byteString(Array(mockCerts.leaf))]), + .utf8String("aikCert"): .byteString(Array(mockCerts.leaf)), + .utf8String("pubArea"): .byteString(Array(pubArea)), + .utf8String("certInfo"): .byteString(Array(certInfo)), + ]) + ) + .build() + .cborEncoded + + let credential = try await finishRegistration( + attestationObject: mockAttestationObject, + rootCertificatesByFormat: [.tpm: [mockCerts.ca]] + ) + XCTAssertEqual(credential.attestationResult.format, .tpm) + XCTAssertEqual(credential.attestationResult.type, .attCA) + XCTAssertEqual(credential.attestationResult.trustChain.count, 2) + } + + private func finishRegistration( + challenge: [UInt8] = TestConstants.mockChallenge, + type: CredentialType = .publicKey, + rawID: [UInt8] = "e0fac9350509f71748d83782ccaf6b4c1462c615c70e255da1344e40887c8fcd".hexadecimal!, + attestationObject: [UInt8], + requireUserVerification: Bool = false, + rootCertificatesByFormat: [AttestationFormat: [Certificate]] = [:], + confirmCredentialIDNotRegisteredYet: (String) async throws -> Bool = { _ in true } + ) async throws -> Credential { + try await webAuthnManager.finishRegistration( + challenge: challenge, + credentialCreationData: RegistrationCredential( + id: rawID.base64URLEncodedString(), + type: type, + rawID: rawID, + attestationResponse: AuthenticatorAttestationResponse( + clientDataJSON: mockClientDataJSONBytes, + attestationObject: attestationObject + ) + ), + requireUserVerification: requireUserVerification, + rootCertificatesByFormat: rootCertificatesByFormat, + confirmCredentialIDNotRegisteredYet: confirmCredentialIDNotRegisteredYet + ) + } + +} diff --git a/Tests/WebAuthnTests/Formats/TPMAttestationTests/CertInfoTests.swift b/Tests/WebAuthnTests/Formats/TPMAttestationTests/CertInfoTests.swift index 1486f129..a4f1da04 100644 --- a/Tests/WebAuthnTests/Formats/TPMAttestationTests/CertInfoTests.swift +++ b/Tests/WebAuthnTests/Formats/TPMAttestationTests/CertInfoTests.swift @@ -28,6 +28,7 @@ final class CertInfoTests: XCTestCase { func testVerifyThrowsIfTypeIsInvalid() throws { let certInfoBytes: [UInt8] = [0xFF, 0x54, 0x43, 0x47] + [UInt8](repeating: 0, count: 80) + let certInfo = TPMAttestation.CertInfo(fromBytes: Data(certInfoBytes))! try assertThrowsError(certInfo.verify(pubArea: Data()), expect: TPMAttestation.CertInfoError.typeInvalid) } diff --git a/Tests/WebAuthnTests/Utils/TestModels/TestAttestationObject.swift b/Tests/WebAuthnTests/Utils/TestModels/TestAttestationObject.swift index 6abdacaa..f3b5a44c 100644 --- a/Tests/WebAuthnTests/Utils/TestModels/TestAttestationObject.swift +++ b/Tests/WebAuthnTests/Utils/TestModels/TestAttestationObject.swift @@ -74,6 +74,12 @@ struct TestAttestationObjectBuilder { temp.wrapped.fmt = .utf8String(utf8String) return temp } + + func fmt(_ format: AttestationFormat) -> Self { + var temp = self + temp.wrapped.fmt = .utf8String(format.rawValue) + return temp + } // MARK: attStmt diff --git a/Tests/WebAuthnTests/Utils/TestModels/TestAuthData.swift b/Tests/WebAuthnTests/Utils/TestModels/TestAuthData.swift index 899c79ba..d0566a47 100644 --- a/Tests/WebAuthnTests/Utils/TestModels/TestAuthData.swift +++ b/Tests/WebAuthnTests/Utils/TestModels/TestAuthData.swift @@ -46,7 +46,7 @@ struct TestAuthData { struct TestAuthDataBuilder { private var wrapped: TestAuthData - + init(wrapped: TestAuthData = TestAuthData()) { self.wrapped = wrapped } @@ -72,6 +72,7 @@ struct TestAuthDataBuilder { ) .extensions([UInt8](repeating: 0, count: 20)) } + /// Creates a valid authData /// diff --git a/Tests/WebAuthnTests/Utils/TestModels/TestCredentialPublicKey.swift b/Tests/WebAuthnTests/Utils/TestModels/TestCredentialPublicKey.swift index 13ac7c3e..8f556de8 100644 --- a/Tests/WebAuthnTests/Utils/TestModels/TestCredentialPublicKey.swift +++ b/Tests/WebAuthnTests/Utils/TestModels/TestCredentialPublicKey.swift @@ -21,6 +21,8 @@ struct TestCredentialPublicKey { var crv: CBOR? var xCoordinate: CBOR? var yCoordinate: CBOR? + var e: CBOR? + var n: CBOR? var byteArrayRepresentation: [UInt8] { var value: [CBOR: CBOR] = [:] @@ -39,6 +41,12 @@ struct TestCredentialPublicKey { if let yCoordinate { value[COSEKey.y.cbor] = yCoordinate } + if let n { + value[COSEKey.n.cbor] = n + } + if let e { + value[COSEKey.e.cbor] = e + } return CBOR.map(value).encode() } } @@ -62,7 +70,24 @@ struct TestCredentialPublicKeyBuilder { .xCoordinate(TestECCKeyPair.publicKeyXCoordinate) .yCoordinate(TestECCKeyPair.publicKeyYCoordinate) } + + func validMockRSA() -> Self { + return self + .kty(.rsaKey) + .alg(.algRS256) + .modulus(TestRSAKeyPair.publicKeyModulus) + .exponent(TestRSAKeyPair.publicKeyExponent) + } + func validMockEdDSA() -> Self { + return self + .kty(.octetKey) + .crv(.ed25519) + .alg(.algEdDSA) + .xCoordinate(TestECCKeyPair.publicKeyXCoordinate) + //.yCoordinate(TestECCKeyPair.publicKeyYCoordinate) + } + func kty(_ kty: COSEKeyType) -> Self { var temp = self temp.wrapped.kty = .unsignedInt(kty.rawValue) @@ -92,4 +117,16 @@ struct TestCredentialPublicKeyBuilder { temp.wrapped.yCoordinate = .byteString(yCoordinate) return temp } + + func modulus(_ modulus: [UInt8]) -> Self { + var temp = self + temp.wrapped.n = .byteString(modulus) + return temp + } + + func exponent(_ exponent: [UInt8]) -> Self { + var temp = self + temp.wrapped.e = .byteString(exponent) + return temp + } } diff --git a/Tests/WebAuthnTests/Utils/TestModels/TestECCKeyPair.swift b/Tests/WebAuthnTests/Utils/TestModels/TestECCKeyPair.swift index a227db92..3b383609 100644 --- a/Tests/WebAuthnTests/Utils/TestModels/TestECCKeyPair.swift +++ b/Tests/WebAuthnTests/Utils/TestModels/TestECCKeyPair.swift @@ -15,6 +15,8 @@ import Foundation import Crypto import WebAuthn +import X509 +import SwiftASN1 struct TestECCKeyPair { static let privateKeyPEM = """ @@ -55,4 +57,51 @@ struct TestECCKeyPair { return [UInt8](signature) } + + static func certificates() throws -> (leaf: Data, ca: Certificate) { + let caPrivateKey = P256.KeyAgreement.PrivateKey() + let ca = try Certificate.init( + version: .v3, + serialNumber: .init(), + publicKey: .init(pemEncoded: caPrivateKey.publicKey.pemRepresentation), + notValidBefore: Date(), + notValidAfter: Date().advanced(by: 3600), + issuer: DistinguishedName { CommonName("Example CA") }, + subject: DistinguishedName { CommonName("Example CA") }, + signatureAlgorithm: .ecdsaWithSHA256, + extensions: try .init{ + Critical(BasicConstraints.isCertificateAuthority(maxPathLength: 1)) + }, + issuerPrivateKey: .init(pemEncoded: caPrivateKey.pemRepresentation) + ) + + let privateKey = try P256.KeyAgreement.PrivateKey(pemRepresentation: privateKeyPEM) + let leaf = try Certificate.init( + version: .v3, + serialNumber: .init(), + publicKey: .init(pemEncoded: privateKey.publicKey.pemRepresentation), + notValidBefore: Date(), + notValidAfter: Date().advanced(by: 3600), + issuer: ca.subject, + subject: DistinguishedName { + CommonName("Example leaf certificate") + OrganizationalUnitName("Authenticator Attestation") + OrganizationName("Example vendor") + CountryName("US") + }, + signatureAlgorithm: .ecdsaWithSHA256, + extensions: try Certificate.Extensions { + Critical(BasicConstraints.notCertificateAuthority) + try ExtendedKeyUsage([ + .init(oid: .init(arrayLiteral: 2, 23, 133, 8, 3)) + ]) + }, + issuerPrivateKey: .init(pemEncoded: caPrivateKey.pemRepresentation) + ) + var leafSerializer = DER.Serializer() + try leafSerializer.serialize(leaf) + let leafDER = leafSerializer.serializedBytes + + return (leaf: Data(leafDER), ca: ca) + } } diff --git a/Tests/WebAuthnTests/Utils/TestModels/TestRSAKeyPair.swift b/Tests/WebAuthnTests/Utils/TestModels/TestRSAKeyPair.swift new file mode 100644 index 00000000..7caf89ef --- /dev/null +++ b/Tests/WebAuthnTests/Utils/TestModels/TestRSAKeyPair.swift @@ -0,0 +1,129 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the WebAuthn Swift open source project +// +// Copyright (c) 2023 the WebAuthn Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of WebAuthn Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation +import Crypto +import _CryptoExtras +import WebAuthn +import X509 +import SwiftASN1 + +struct TestRSAKeyPair { + static let privateKeyPEM = """ + -----BEGIN RSA PRIVATE KEY----- + MIIEowIBAAKCAQEA2VnofJn24NHyyGDU4tV1rGsuiI9FBSR7KKU7vkvxqA3GIWO1 + 3Wx8J3Nmcf+U/SXdgs+z9HdiHblvsMQSQxTwLyXxHCB6bYSSOLC+2nHGVxQqDEc2 + LwZ3gQnaGhwLuHCrffdB6tTCrblDDuCb3agMyRMFz8R0kOiu9+GGj0tLspA62uLx + etPSFNsjqdxK5YZEWnGULz/MNgqTR4LUVRaUM6F6o3JVi6UKy4dlXHEpxjTLr7y3 + 1W4AbQVC5M5FElwxMYmTAQrodhtRyGwqdzMxrWjxA7RckBGmMjXhk4ls0v0IAvyB + RUCR1zw3c6Swk2q5Sy1itNC0Y62d1Ru2jdLhxQIDAQABAoIBAAgmB8JMH2ZUWK7M + eo66g/vf1NHH1UWZFYjzkObUgA3V3ly4GURg4dK0z91sQJCxD7nswYljxGjq39YX + s7uSGGMcIAr26MAcXUME1VLpOw9esSjerphavLY4wVWDQak7iCJj17PPIDFVJb90 + CkPoHfqX3PrqGZipMI4YhWvv3bmm/uzvdMvNB1bWmYLg/zCYATPugZ+XthqiLGDI + lohsrN2S06uJYBc+nEGI8PU2OST00PK/qemhR/SNRfukaqzJbkEC23lG1s3U7pgj + ucLxb2Ss4I6naJboUFybTAGp/yJf4FYaThiW9v9KDfPpvilj2LNhnVFTNflevMVG + bQXktwECgYEA8QJ/Y/BBIGSrlbco16jFXelR4kpG6z5xZ2MQP1i8ktE/9N47TtlP + S4cU+JH0X1bx0516IUw0ib2+IH6ogl7AWM4tjEPzVeLwHds9sgtKZS8ZO/MwFlkl + TVYBrx6sTaF2OV/6TAlFfNzVmpPeXfn4E0GzLG+FM2cxSxwOOS7kLwUCgYEA5t60 + bxclfHW9iCrpahxvBr+BrozKcNIPbaRfe1pqVVZTo71lU4aa+5RXffd6xiTfXw73 + ZYj+82uHeChslACYgcIppqjYodI7hEBLIi9ab/WVSIuSNe16VTjDFbQbqHrgcUhm + G4KDMyrYQtsDecMFB27SzoBrxbFJ+9NJ49zS48ECgYEA0vQwtTVSjBwR5FYRtdLQ + DsdvGPeS484gTTJ0wj3VsVze8mKi3v1vXti6DmkS0XC081lf0U12nyoqBR9YN+Tf + z9uIGsJPd9nP+xIwCmu/jpmPKN5QNP+KmwqxJqtefgTaCpZr66oh3I0fmtHbTb7C + 2XgkcLychsXIa8n+2SamLFECgYA13qlDcqcwj1iWOU0VkWTmsjDURc3G3Xz0HHKb + GdHN78K8ZikKgFIRed+gaOqg6WGlkJxxeLHkoqaNhwEu16S+Qkvts2A5AhEZHtdp + NptnnGok70xCgRMWZ5Q9sDTz7xgH1tjcemuauNiVYP1CoBrATT+rJ5P+IQweUoLf + RFuBAQKBgCD7w9XOEvCuZNvMM+LtaxUQI5hProiVo8cke/rvu/cxM5EOTOXZGyzW + E7gerNu2WpoRLty1ps6XkwLUTcQ8UblceuYtqa2URFKig0HJRuIw5iqSxrlpVjfZ + Y2dC2Bo/X4j0M0bKwt/IbGFKNuyTKAtCDgQPUfmzHFhWKAb1Pd4R + -----END RSA PRIVATE KEY----- + """ + + static let publicKeyPEM = """ + -----BEGIN RSA PUBLIC KEY----- + MIIBCgKCAQEA2VnofJn24NHyyGDU4tV1rGsuiI9FBSR7KKU7vkvxqA3GIWO13Wx8 + J3Nmcf+U/SXdgs+z9HdiHblvsMQSQxTwLyXxHCB6bYSSOLC+2nHGVxQqDEc2LwZ3 + gQnaGhwLuHCrffdB6tTCrblDDuCb3agMyRMFz8R0kOiu9+GGj0tLspA62uLxetPS + FNsjqdxK5YZEWnGULz/MNgqTR4LUVRaUM6F6o3JVi6UKy4dlXHEpxjTLr7y31W4A + bQVC5M5FElwxMYmTAQrodhtRyGwqdzMxrWjxA7RckBGmMjXhk4ls0v0IAvyBRUCR + 1zw3c6Swk2q5Sy1itNC0Y62d1Ru2jdLhxQIDAQAB + -----END RSA PUBLIC KEY----- + """ + + static let publicKeyExponent = withUnsafeBytes(of: UInt32(65537).bigEndian, Array.init) + static let publicKeyModulus = "d959e87c99f6e0d1f2c860d4e2d575ac6b2e888f4505247b28a53bbe4bf1a80dc62163b5dd6c7c27736671ff94fd25dd82cfb3f477621db96fb0c4124314f02f25f11c207a6d849238b0beda71c657142a0c47362f06778109da1a1c0bb870ab7df741ead4c2adb9430ee09bdda80cc91305cfc47490e8aef7e1868f4b4bb2903adae2f17ad3d214db23a9dc4ae586445a71942f3fcc360a934782d455169433a17aa372558ba50acb87655c7129c634cbafbcb7d56e006d0542e4ce45125c31318993010ae8761b51c86c2a773331ad68f103b45c9011a63235e193896cd2fd0802fc81454091d73c3773a4b0936ab94b2d62b4d0b463ad9dd51bb68dd2e1c5".hexadecimal! + + static func signature(data: Data) throws -> _RSA.Signing.RSASignature { + let privateKey = try _RSA.Signing.PrivateKey(pemRepresentation: privateKeyPEM) + return try privateKey.signature(for: data, padding: .insecurePKCS1v1_5) + } + + static var signature: [UInt8] { + let authenticatorData = TestAuthDataBuilder() + .validAuthenticationMock() + .buildAsBase64URLEncoded() + + // Create a signature. This part is usually performed by the authenticator + let clientData: Data = TestClientDataJSON(type: "webauthn.get").jsonData + let clientDataHash = SHA256.hash(data: clientData) + let rawAuthenticatorData = authenticatorData.urlDecoded.decoded! + let signatureBase = rawAuthenticatorData + clientDataHash + // swiftlint:disable:next force_try + let signature = try! TestRSAKeyPair.signature(data: signatureBase).rawRepresentation + + return [UInt8](signature) + } + + static func certificates() throws -> (leaf: Data, ca: Certificate) { + let caPrivateKey = try _RSA.Encryption.PrivateKey.init(keySize: .bits2048) + let ca = try Certificate.init( + version: .v3, + serialNumber: .init(), + publicKey: .init(pemEncoded: caPrivateKey.publicKey.pemRepresentation), + notValidBefore: Date(), + notValidAfter: Date().advanced(by: 3600), + issuer: DistinguishedName { CommonName("Example CA") }, + subject: DistinguishedName { CommonName("Example CA") }, + signatureAlgorithm: .sha256WithRSAEncryption, + extensions: try .init{ + Critical(BasicConstraints.isCertificateAuthority(maxPathLength: 1)) + }, + issuerPrivateKey: .init(pemEncoded: caPrivateKey.pemRepresentation) + ) + + let privateKey = try _RSA.Encryption.PrivateKey(pemRepresentation: privateKeyPEM) + let leaf = try Certificate.init( + version: .v3, + serialNumber: .init(), + publicKey: .init(pemEncoded: privateKey.publicKey.pemRepresentation), + notValidBefore: Date(), + notValidAfter: Date().advanced(by: 3600), + issuer: ca.subject, + subject: DistinguishedName { + CommonName("Example leaf certificate") + OrganizationalUnitName("Authenticator Attestation") + OrganizationName("Example vendor") + CountryName("US") + }, + signatureAlgorithm: .sha256WithRSAEncryption, + extensions: try Certificate.Extensions {Critical(BasicConstraints.notCertificateAuthority)}, + issuerPrivateKey: .init(pemEncoded: caPrivateKey.pemRepresentation) + ) + var leafSerializer = DER.Serializer() + try leafSerializer.serialize(leaf) + let leafDER = leafSerializer.serializedBytes + + return (leaf: Data(leafDER), ca: ca) + } +} From 1f9233d04fdd9e53b34836b61c73ca400d197653 Mon Sep 17 00:00:00 2001 From: Matthieu Barthelemy Date: Sat, 11 May 2024 08:45:34 +1000 Subject: [PATCH 26/30] Android attetsation tests --- .../Registration/AttestationObject.swift | 2 +- .../AndroidKey/AndroidKeyAttestation.swift | 5 +- .../Formats/TPM/TPMAttestation.swift | 12 +- .../Shared/CredentialPublicKey.swift | 1 - .../AndroidKeyAttestationTests.swift | 212 ++++++++++++++++++ .../FidoU2FAttestationTests.swift | 5 +- .../PackedAttestationTests.swift | 53 ++++- .../Formats/TPMAttestationTests.swift | 110 --------- .../TPMAttestationTests.swift | 145 ++++++++++++ .../Utils/TestModels/TestAuthData.swift | 14 ++ .../Utils/TestModels/TestRSAKeyPair.swift | 7 +- 11 files changed, 438 insertions(+), 128 deletions(-) create mode 100644 Tests/WebAuthnTests/Formats/AndroidKeyAttestationTests/AndroidKeyAttestationTests.swift rename Tests/WebAuthnTests/Formats/{ => FidoU2FAttestationTests}/FidoU2FAttestationTests.swift (98%) rename Tests/WebAuthnTests/Formats/{ => PackedAttestationTests}/PackedAttestationTests.swift (86%) delete mode 100644 Tests/WebAuthnTests/Formats/TPMAttestationTests.swift create mode 100644 Tests/WebAuthnTests/Formats/TPMAttestationTests/TPMAttestationTests.swift diff --git a/Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift b/Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift index 2b5279ac..08157a87 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift @@ -61,7 +61,7 @@ public struct AttestationObject { let rootCertificates = rootCertificatesByFormat[format] ?? [] var attestationType: AttestationResult.AttestationType = .none var trustedPath: [Certificate] = [] - print("\n •••• \(Self.self).verify() format=\(format)") + switch format { case .none: // if format is `none` statement must be empty diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKey/AndroidKeyAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKey/AndroidKeyAttestation.swift index 0744e492..20276ae1 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKey/AndroidKeyAttestation.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKey/AndroidKeyAttestation.swift @@ -46,9 +46,6 @@ struct AndroidKeyAttestation: AttestationProtocol { } guard let leafCertificate = x5c.first else { throw WebAuthnError.invalidAttestationCertificate } - let intermediates = CertificateStore(x5c[1...]) - let rootCertificatesStore = CertificateStore(rootCertificates) - let verificationData = authenticatorData.rawData + clientDataHash // Verify signature let leafCertificatePublicKey: Certificate.PublicKey = leafCertificate.publicKey @@ -67,6 +64,8 @@ struct AndroidKeyAttestation: AttestationProtocol { throw WebAuthnError.attestationPublicKeyMismatch } + let intermediates = CertificateStore(x5c[1...]) + let rootCertificatesStore = CertificateStore(rootCertificates) var verifier = Verifier(rootCertificates: rootCertificatesStore) { AndroidKeyVerificationPolicy(clientDataHash: clientDataHash) } diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMAttestation.swift index 5c86382d..239492f0 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMAttestation.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMAttestation.swift @@ -57,6 +57,9 @@ struct TPMAttestation: AttestationProtocol { let verifierResult: VerificationResult = await verifier.validate( leafCertificate: aikCert, intermediates: intermediates + /*diagnosticCallback: { result in + print("\n •••• \(Self.self) result=\(result)") + }*/ ) guard case .validCertificate(let chain) = verifierResult else { throw WebAuthnError.invalidTrustPath @@ -79,7 +82,6 @@ struct TPMAttestation: AttestationProtocol { if let pubAreaCBOR = attStmt["pubArea"], case let .byteString(pubAreaRaw) = pubAreaCBOR { let pubArea = PubArea(from: Data(pubAreaRaw)) - print("\n••• \(Self.self) pubAreaRaw64=\(Data(pubAreaRaw).base64EncodedString())\npubArea=\(pubArea!)") } // Verify pubArea guard let pubAreaCBOR = attStmt["pubArea"], @@ -90,7 +92,6 @@ struct TPMAttestation: AttestationProtocol { switch pubArea.parameters { case let .rsa(rsaParameters): if case let .rsa(rsaPublicKeyData) = credentialPublicKey { - print("\n •••• \(Self.self) pubArea.unique.data=\(Array(pubArea.unique.data)), rsaPublicKeyData.n=\(rsaPublicKeyData.n)") } guard case let .rsa(rsaPublicKeyData) = credentialPublicKey, Array(pubArea.unique.data) == rsaPublicKeyData.n else { @@ -117,12 +118,7 @@ struct TPMAttestation: AttestationProtocol { throw WebAuthnError.tpmInvalidPubAreaCurve } } - - /*if let certInfoCBOR = attStmt["certInfo"], - case let .byteString(certInfo) = certInfoCBOR { - let parsedCertInfo = CertInfo(fromBytes: Data(certInfo)) - print("\n••• \(Self.self) certInfo64=\(Data(certInfo).base64EncodedString())\nparsedCertInfo=\(parsedCertInfo!)") - }*/ + // Verify certInfo guard let certInfoCBOR = attStmt["certInfo"], case let .byteString(certInfo) = certInfoCBOR, diff --git a/Sources/WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift b/Sources/WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift index 9c144715..6691e2ed 100644 --- a/Sources/WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift +++ b/Sources/WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift @@ -73,7 +73,6 @@ enum CredentialPublicKey { throw WebAuthnError.unsupportedCOSEAlgorithm } - print("\n•••• \(Self.self).init() keyType=\(keyType), algorithm=\(algorithm)") switch keyType { case .ellipticKey: self = try .ec2(EC2PublicKey(publicKeyObject: publicKeyObject, algorithm: algorithm)) diff --git a/Tests/WebAuthnTests/Formats/AndroidKeyAttestationTests/AndroidKeyAttestationTests.swift b/Tests/WebAuthnTests/Formats/AndroidKeyAttestationTests/AndroidKeyAttestationTests.swift new file mode 100644 index 00000000..9597b56c --- /dev/null +++ b/Tests/WebAuthnTests/Formats/AndroidKeyAttestationTests/AndroidKeyAttestationTests.swift @@ -0,0 +1,212 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the WebAuthn Swift open source project +// +// Copyright (c) 2023 the WebAuthn Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of WebAuthn Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@testable import WebAuthn +import XCTest +import SwiftCBOR +import Crypto +import X509 + +// swiftlint:disable:next type_body_length +final class RegistrationAndroidKeyAttestationTests: XCTestCase { + var webAuthnManager: WebAuthnManager! + + //let challenge: [UInt8] = Array(Data(base64Encoded: "kIgdIQmaAms56UNzw0DH8uOz3BDF2UJYaJP6zIQX1a8=")!) + + let relyingPartyDisplayName = "Testy test" + let relyingPartyID = "example.com" + let relyingPartyOrigin = "https://example.com" + let mockClientDataJSONBytes = TestClientDataJSON(challenge: TestConstants.mockChallenge.base64URLEncodedString()).jsonBytes + let mockCredentialPublicKeyECC = TestCredentialPublicKeyBuilder().validMock().buildAsByteArray() + let challenge: [UInt8] = [1, 0, 1] + + override func setUp() { + let configuration = WebAuthnManager.Configuration( + relyingPartyID: relyingPartyID, + relyingPartyName: relyingPartyDisplayName, + relyingPartyOrigin: relyingPartyOrigin + ) + webAuthnManager = .init(configuration: configuration, challengeGenerator: .mock(generate: challenge)) + } + + func testInvalidAlg() async throws { + //let authData = TestAuthDataBuilder().validMock() + let authData = TestAuthDataBuilder().validMock() + .attestedCredData(credentialPublicKey: mockCredentialPublicKeyECC) + .noExtensionData() + let mockAttestationObject = TestAttestationObjectBuilder() + .fmt(.androidKey) + .authData(authData) + .attStmt( + .map([.utf8String("alg"): .negativeInt(999)]) + ) + .build() + .cborEncoded + + await assertThrowsError( + try await finishRegistration( + attestationObject: mockAttestationObject, + rootCertificatesByFormat: [:] + ), + expect: WebAuthnError.invalidAttestationSignatureAlgorithm + ) + } + + func testInvalidSig() async throws { + let authData = TestAuthDataBuilder().validMock() + let mockAttestationObject = TestAttestationObjectBuilder() + .fmt(.androidKey) + .authData(authData) + .attStmt( + .map([ + .utf8String("alg"): .negativeInt(UInt64(abs(COSEAlgorithmIdentifier.algES256.rawValue) - 1)), + .utf8String("sig"): .negativeInt(999) + ]) + ) + .build() + .cborEncoded + + await assertThrowsError( + try await finishRegistration( + attestationObject: mockAttestationObject, + rootCertificatesByFormat: [:] + ), + expect: WebAuthnError.invalidSignature + ) + } + + func testInvalidCert() async throws { + let authData = TestAuthDataBuilder().validMock() + let mockAttestationObject = TestAttestationObjectBuilder() + .fmt(.androidKey) + .authData(authData) + .attStmt( + .map([ + .utf8String("alg"): .negativeInt(UInt64(abs(COSEAlgorithmIdentifier.algES256.rawValue) - 1)), + .utf8String("sig"): .byteString([0x00]), + .utf8String("x5c"): .byteString([0x00]) + ]) + ) + .build() + .cborEncoded + + await assertThrowsError( + try await finishRegistration( + attestationObject: mockAttestationObject, + rootCertificatesByFormat: [:] + ), + expect: WebAuthnError.invalidAttestationCertificate + ) + } + + func testInvalidVerificationData() async throws { + let mockCerts = try TestECCKeyPair.certificates() + let verificationData: [UInt8] = [0x01] + let authData = TestAuthDataBuilder().validMock() + let mockAttestationObject = TestAttestationObjectBuilder() + .fmt(.androidKey) + .authData(authData) + .attStmt( + .map([ + .utf8String("alg"): .negativeInt(UInt64(abs(COSEAlgorithmIdentifier.algES256.rawValue) - 1)), + .utf8String("sig"): .byteString(Array( + try TestECCKeyPair + .signature(data: Data(verificationData)) + .derRepresentation + )), + .utf8String("x5c"): .array([.byteString(Array(mockCerts.leaf))]) + ]) + ) + .build() + .cborEncoded + + await assertThrowsError( + try await finishRegistration( + attestationObject: mockAttestationObject, + rootCertificatesByFormat: [:] + ), + expect: WebAuthnError.invalidVerificationData + ) + } + + func testPublicKeysMismatch() async throws { + let mockCerts = try TestECCKeyPair.certificates() + let verificationData: [UInt8] = [0x01] + let authData = TestAuthDataBuilder().validMockRSA() + let clientDataHash = SHA256.hash(data: Data(mockClientDataJSONBytes)) + let mockAttestationObject = TestAttestationObjectBuilder() + .fmt(.androidKey) + .authData(authData) + .attStmt( + .map([ + .utf8String("alg"): .negativeInt(UInt64(abs(COSEAlgorithmIdentifier.algES256.rawValue) - 1)), + .utf8String("sig"): .byteString(Array( + try TestECCKeyPair + .signature(data: Data(authData.build().byteArrayRepresentation) + clientDataHash) + .derRepresentation + + )), + .utf8String("x5c"): .array([.byteString(Array(mockCerts.leaf))]) + ]) + ) + .build() + .cborEncoded + + await assertThrowsError( + try await finishRegistration( + attestationObject: mockAttestationObject, + rootCertificatesByFormat: [:] + ), + expect: WebAuthnError.attestationPublicKeyMismatch + ) + } + + /*func testAttCAAttestationRSASucceeds() async throws { + let credential = try await finishRegistration( + challenge: challenge, + attestationObject: Array(Data(base64Encoded: attestationObjectBase64.urlDecoded.asString())!), + rootCertificatesByFormat: [.tpm: [caCert]] + ) + XCTAssertEqual(credential.attestationResult.format, .tpm) + XCTAssertEqual(credential.attestationResult.type, .attCA) + XCTAssertEqual(credential.attestationResult.trustChain.count, 3) + }*/ + + private func finishRegistration( + challenge: [UInt8] = TestConstants.mockChallenge, + type: CredentialType = .publicKey, + rawID: [UInt8] = "e0fac9350509f71748d83782ccaf6b4c1462c615c70e255da1344e40887c8fcd".hexadecimal!, + attestationObject: [UInt8], + requireUserVerification: Bool = false, + rootCertificatesByFormat: [AttestationFormat: [Certificate]] = [:], + confirmCredentialIDNotRegisteredYet: (String) async throws -> Bool = { _ in true } + ) async throws -> Credential { + try await webAuthnManager.finishRegistration( + challenge: challenge, + credentialCreationData: RegistrationCredential( + id: rawID.base64URLEncodedString(), + type: type, + rawID: rawID, + attestationResponse: AuthenticatorAttestationResponse( + clientDataJSON: mockClientDataJSONBytes, + attestationObject: attestationObject + ) + ), + requireUserVerification: requireUserVerification, + rootCertificatesByFormat: rootCertificatesByFormat, + confirmCredentialIDNotRegisteredYet: confirmCredentialIDNotRegisteredYet + ) + } + +} diff --git a/Tests/WebAuthnTests/Formats/FidoU2FAttestationTests.swift b/Tests/WebAuthnTests/Formats/FidoU2FAttestationTests/FidoU2FAttestationTests.swift similarity index 98% rename from Tests/WebAuthnTests/Formats/FidoU2FAttestationTests.swift rename to Tests/WebAuthnTests/Formats/FidoU2FAttestationTests/FidoU2FAttestationTests.swift index 9a2bca59..597e549b 100644 --- a/Tests/WebAuthnTests/Formats/FidoU2FAttestationTests.swift +++ b/Tests/WebAuthnTests/Formats/FidoU2FAttestationTests/FidoU2FAttestationTests.swift @@ -69,8 +69,7 @@ final class RegistrationFidoU2FAttestationTests: XCTestCase { } func testAttestationMissingx5c() async throws { - let authData = TestAuthDataBuilder().validMock() - + let authData = TestAuthDataBuilder().validMock() let mockAttestationObject = TestAttestationObjectBuilder() .fmt(.fidoU2F) .authData(authData) @@ -98,7 +97,7 @@ final class RegistrationFidoU2FAttestationTests: XCTestCase { let mockCerts = try TestECCKeyPair.certificates() let credentialId: [UInt8] = [0b00000001] let authData = TestAuthDataBuilder() - .relyingPartyIDHash(fromRelyingPartyID: "example.com") + .relyingPartyIDHash(fromRelyingPartyID: relyingPartyID) .flags(0b11000101) .counter([0b00000000, 0b00000000, 0b00000000, 0b00000000]) .attestedCredData( diff --git a/Tests/WebAuthnTests/Formats/PackedAttestationTests.swift b/Tests/WebAuthnTests/Formats/PackedAttestationTests/PackedAttestationTests.swift similarity index 86% rename from Tests/WebAuthnTests/Formats/PackedAttestationTests.swift rename to Tests/WebAuthnTests/Formats/PackedAttestationTests/PackedAttestationTests.swift index e0407a77..a262898d 100644 --- a/Tests/WebAuthnTests/Formats/PackedAttestationTests.swift +++ b/Tests/WebAuthnTests/Formats/PackedAttestationTests/PackedAttestationTests.swift @@ -51,6 +51,33 @@ final class RegistrationPackedAttestationTests: XCTestCase { clientDataHash = SHA256.hash(data: Data(mockClientDataJSONBytes)) } + func testInvalidAlg() async throws { + let mockAttestationObject = TestAttestationObjectBuilder() + .fmt(.packed) + .authData(authDataECC) + .attStmt( + .map([ + .utf8String("alg"): .negativeInt(999), + .utf8String("sig"): .byteString(Array( + try TestECCKeyPair + .signature(data: Data(authDataECC.build().byteArrayRepresentation) + clientDataHash) + .derRepresentation + )), + ]) + + ) + .build() + .cborEncoded + + await assertThrowsError( + try await finishRegistration( + attestationObject: mockAttestationObject, + rootCertificatesByFormat: [:] + ), + expect: WebAuthnError.invalidAttestationSignatureAlgorithm + ) + } + func testSelfAttestationAlgMismatch() async throws { let mockAttestationObject = TestAttestationObjectBuilder() .fmt(.packed) @@ -75,7 +102,31 @@ final class RegistrationPackedAttestationTests: XCTestCase { expect: WebAuthnError.attestationPublicKeyAlgorithmMismatch ) } - + + func testInvalidCert() async throws { + let authData = TestAuthDataBuilder().validMock() + let mockAttestationObject = TestAttestationObjectBuilder() + .fmt(.packed) + .authData(authData) + .attStmt( + .map([ + .utf8String("alg"): .negativeInt(UInt64(abs(COSEAlgorithmIdentifier.algES256.rawValue) - 1)), + .utf8String("sig"): .byteString([0x00]), + .utf8String("x5c"): .byteString([0x00]) + ]) + ) + .build() + .cborEncoded + + await assertThrowsError( + try await finishRegistration( + attestationObject: mockAttestationObject, + rootCertificatesByFormat: [:] + ), + expect: WebAuthnError.invalidAttestationCertificate + ) + } + func testBasicAttestationInvalidVerifData() async throws { let verificationData: [UInt8] = [0x01] let mockCerts = try TestECCKeyPair.certificates() diff --git a/Tests/WebAuthnTests/Formats/TPMAttestationTests.swift b/Tests/WebAuthnTests/Formats/TPMAttestationTests.swift deleted file mode 100644 index 0f6b1d3c..00000000 --- a/Tests/WebAuthnTests/Formats/TPMAttestationTests.swift +++ /dev/null @@ -1,110 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the WebAuthn Swift open source project -// -// Copyright (c) 2023 the WebAuthn Swift project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of WebAuthn Swift project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -@testable import WebAuthn -import XCTest -import SwiftCBOR -import Crypto -import _CryptoExtras -import X509 - -// swiftlint:disable:next type_body_length -final class RegistrationTPMAttestationTests: XCTestCase { - var webAuthnManager: WebAuthnManager! - - let challenge: [UInt8] = [1, 0, 1] - let relyingPartyDisplayName = "Testy test" - let relyingPartyID = "example.com" - let relyingPartyOrigin = "https://example.com" - let mockClientDataJSONBytes = TestClientDataJSON(challenge: TestConstants.mockChallenge.base64URLEncodedString()).jsonBytes - - override func setUp() { - let configuration = WebAuthnManager.Configuration( - relyingPartyID: relyingPartyID, - relyingPartyName: relyingPartyDisplayName, - relyingPartyOrigin: relyingPartyOrigin - ) - webAuthnManager = .init(configuration: configuration, challengeGenerator: .mock(generate: challenge)) - } - - func testAttCAAttestationRSASucceeds() async throws { - let mockCredentialPublicKey = TestCredentialPublicKeyBuilder().validMockRSA().buildAsByteArray() - let authData = TestAuthDataBuilder().validMock() - .attestedCredData(credentialPublicKey: mockCredentialPublicKey) - .noExtensionData() - let hash = SHA256.hash(data: Data(mockClientDataJSONBytes)) - let mockCerts = try TestECCKeyPair.certificates() - - //let certInfoBytes: [UInt8] = [0xFF, 0x54, 0x43, 0x47] + [UInt8](repeating: 0, count: 80) - // RSA PubArea - let pubArea = Data(base64Encoded: "AAEACwAGBHIAIJ3/y/NsODrmmfuYaNxty4nXFTiEvigDkiwSQVi/rSKuABAAEAgAAAAAAAEAus2NNibjf6n9vIlQiEmunemlDObEtj7Cr2TDtD//tvJS1//fsW5mxHEz7wo+WiBKlVHwm9O1OKggQVHWlsoAG4QHJL82KwApuSPIYzKBpMTJGS8OZF9Eo7R8elX4JLBJcZ7uA3AtoPaai/zHIHXWVdMzRq2DY9Ymps6MU8jnGMr2Y0L/+IFPrfZhHNLqhx7/h+pNt6eJnp7MmhgIZBk1fKHcgcbDaXZ0fCD511jzu7QQ025OJoN1bXJai4UtPkwof0J2epXBJdu8ExPBY8KlXUBvRdTrsp/njQAKtBLn288I0jabg65Y/io+cWP5UuQTBI0FF6j/lOZ81ttk3oV/FQ==")! - let certInfo = Data(base64Encoded: "/1RDR4AXACIAC7fjlRE/X84oQtXc8hucRu9DFXUZD6UhJFkNJ57OM2mJABRqOl417tyWPsLqfhByWFhLi6W+OQAAAAjIGcaOvjsE9fnCGcEBo17KdHVMLNYAIgAL8wrHq55UwHsBdEMDTgPolqcoRsQvpP8QUY07Rjc/ZuoAIgALOeLcnM1NWggIfzd1ct6nAJwvcxnjsbUECgnvAgGp22w=")! - let mockAttestationObject = TestAttestationObjectBuilder() - .validMock() - .fmt(.tpm) - .authData(authData) - .attStmt( - .map([ - .utf8String("ver"): .utf8String("2.0"), - .utf8String("alg"): .negativeInt(UInt64(abs(COSEAlgorithmIdentifier.algRS256.rawValue) - 1)), - .utf8String("sig"): .byteString(Array( - try TestECCKeyPair - .signature(data: Data(authData.build().byteArrayRepresentation) + hash) - .derRepresentation - )), - .utf8String("x5c"): .array([.byteString(Array(mockCerts.leaf))]), - .utf8String("aikCert"): .byteString(Array(mockCerts.leaf)), - .utf8String("pubArea"): .byteString(Array(pubArea)), - .utf8String("certInfo"): .byteString(Array(certInfo)), - ]) - ) - .build() - .cborEncoded - - let credential = try await finishRegistration( - attestationObject: mockAttestationObject, - rootCertificatesByFormat: [.tpm: [mockCerts.ca]] - ) - XCTAssertEqual(credential.attestationResult.format, .tpm) - XCTAssertEqual(credential.attestationResult.type, .attCA) - XCTAssertEqual(credential.attestationResult.trustChain.count, 2) - } - - private func finishRegistration( - challenge: [UInt8] = TestConstants.mockChallenge, - type: CredentialType = .publicKey, - rawID: [UInt8] = "e0fac9350509f71748d83782ccaf6b4c1462c615c70e255da1344e40887c8fcd".hexadecimal!, - attestationObject: [UInt8], - requireUserVerification: Bool = false, - rootCertificatesByFormat: [AttestationFormat: [Certificate]] = [:], - confirmCredentialIDNotRegisteredYet: (String) async throws -> Bool = { _ in true } - ) async throws -> Credential { - try await webAuthnManager.finishRegistration( - challenge: challenge, - credentialCreationData: RegistrationCredential( - id: rawID.base64URLEncodedString(), - type: type, - rawID: rawID, - attestationResponse: AuthenticatorAttestationResponse( - clientDataJSON: mockClientDataJSONBytes, - attestationObject: attestationObject - ) - ), - requireUserVerification: requireUserVerification, - rootCertificatesByFormat: rootCertificatesByFormat, - confirmCredentialIDNotRegisteredYet: confirmCredentialIDNotRegisteredYet - ) - } - -} diff --git a/Tests/WebAuthnTests/Formats/TPMAttestationTests/TPMAttestationTests.swift b/Tests/WebAuthnTests/Formats/TPMAttestationTests/TPMAttestationTests.swift new file mode 100644 index 00000000..7b009449 --- /dev/null +++ b/Tests/WebAuthnTests/Formats/TPMAttestationTests/TPMAttestationTests.swift @@ -0,0 +1,145 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the WebAuthn Swift open source project +// +// Copyright (c) 2023 the WebAuthn Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of WebAuthn Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@testable import WebAuthn +import XCTest +import SwiftCBOR +import Crypto +import X509 + +// swiftlint:disable:next type_body_length +final class RegistrationTPMAttestationTests: XCTestCase { + var webAuthnManager: WebAuthnManager! + + let challenge: [UInt8] = Array(Data(base64Encoded: "kIgdIQmaAms56UNzw0DH8uOz3BDF2UJYaJP6zIQX1a8=")!) + let relyingPartyDisplayName = "Testy test" + let relyingPartyID = "d2urpypvrhb05x.amplifyapp.com" + let relyingPartyOrigin = "https://dev.d2urpypvrhb05x.amplifyapp.com" + + // Generating data, or mocking it, for a TPM registration, would be excruciatingly painful. + // Here we're using a "known good payload" for a Windows Hello authenticator. + let clientDataJSONBase64 = "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoia0lnZElRbWFBbXM1NlVOencwREg4dU96M0JERjJVSllhSlA2eklRWDFhOCIsIm9yaWdpbiI6Imh0dHBzOi8vZGV2LmQydXJweXB2cmhiMDV4LmFtcGxpZnlhcHAuY29tIiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ==" + let attestationObjectBase64 = URLEncodedBase64("o2NmbXRjdHBtZ2F0dFN0bXSmY2FsZzn__mNzaWdZAQBFEaTe-uZvbZBNsIMtJa26eigMUxEM1mBtddR7gdEBH5Hyeo9hFCqiJwYVKUq_iP9hvFaiLzoGbAWDgiG-fa3F-S71c8w83756dyRBMXNHYEvYjfv0TqGyky73V4xyKpf1iHiO_g4t31UjQiyTfypdP_rRcm42KVKgVyRPZzx_AKweN9XKEFfT2Ym3fmqD_scaIeKSyGs9qwH1MbILLUVnRK6fKK6sAA4ZaDVz4gUiSUoK9ZycCC2hfLBq5GjiTLgQF_Q2O3gRTqmU8VfwVsmtN5OMaGOyaFrUk97-RvZVrARXhNzrUAJT7KjTLDZeIA96F3pB_F_q3xd_dgvwVpWHY3ZlcmMyLjBjeDVjglkFuzCCBbcwggOfoAMCAQICEHHcna7VCE3QpRyKgi2uvXYwDQYJKoZIhvcNAQELBQAwQTE_MD0GA1UEAxM2RVVTLU5UQy1LRVlJRC04ODJGMDQ3Qjg3MTIxQ0Y5ODg1RjMxMTYwQkM3QkI1NTg2QUY0NzFCMB4XDTIyMDEyMDE5NTQxNloXDTI3MDYwMzE3NTE0OFowADCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALtT5frDB-WUq6N0VYnlYEalJzSut0JQx3vP29_ub-kZ7csJrm8uGXQGUkPlf4EFehFTnQ1jX_oZ8jNPw1m3rV5ijcuCe3r5GICFD6gpbuErmGS2mDVfe3fl_p0gPvhtulqatb1uYkWfW5SIKix1XWRvm92s3lRQvd-6vX_ExPIP-pEf0tkeINpBNNWgdtx3VdW4KVFTcv-q2FKhqfqXiAdOMHmwmWyXYulppYqW2XC7Pw9QmHZR_C5Urpc5UMmABz4zWSAYOyBMkKsX8koAsk8RgLtus07wW3FhJqi-BYczIe0IxG0q9UL295lkaxreTkfWYZHMMcU4M-Tm1w7QvKsCAwEAAaOCAeowggHmMA4GA1UdDwEB_wQEAwIHgDAMBgNVHRMBAf8EAjAAMG0GA1UdIAEB_wRjMGEwXwYJKwYBBAGCNxUfMFIwUAYIKwYBBQUHAgIwRB5CAFQAQwBQAEEAIAAgAFQAcgB1AHMAdABlAGQAIAAgAFAAbABhAHQAZgBvAHIAbQAgACAASQBkAGUAbgB0AGkAdAB5MBAGA1UdJQQJMAcGBWeBBQgDMFAGA1UdEQEB_wRGMESkQjBAMT4wEAYFZ4EFAgIMB05QQ1Q3NXgwFAYFZ4EFAgEMC2lkOjRFNTQ0MzAwMBQGBWeBBQIDDAtpZDowMDA3MDAwMjAfBgNVHSMEGDAWgBSMmnF_AA0xD8rW7i0pqjSXJYwSHjAdBgNVHQ4EFgQUFmUMIda76eb7Whi8CweaWhe7yNMwgbIGCCsGAQUFBwEBBIGlMIGiMIGfBggrBgEFBQcwAoaBkmh0dHA6Ly9hemNzcHJvZGV1c2Fpa3B1Ymxpc2guYmxvYi5jb3JlLndpbmRvd3MubmV0L2V1cy1udGMta2V5aWQtODgyZjA0N2I4NzEyMWNmOTg4NWYzMTE2MGJjN2JiNTU4NmFmNDcxYi84ODIzMGNhMi0yN2U1LTQxNTEtOWJhMi01OWI1ODJjMzlhYWEuY2VyMA0GCSqGSIb3DQEBCwUAA4ICAQCxsTbR5V8qnw6H6HEWJvrqcRy8fkY_vFUSjUq27hRl0t9D6LuS20l65FFm48yLwCkQbIf-aOBjwWafAbSVnEMig3KP-2Ml8IFtH63Msq9lwDlnXx2PNi7ISOemHNzBNeOG7pd_Zs69XUTq9zCriw9gAILCVCYllBluycdT7wZdjf0Bb5QJtTMuhwNXnOWmjv0VBOfsclWo-SEnnufaIDi0Vcf_TzbgmNn408Ej7R4Njy4qLnhPk64ruuWNJt3xlLMjbJXe_VKdO3lhM7JVFWSNAn8zfvEIwrrgCPhp1k2mFUGxJEvpSTnuZtNF35z4_54K6cEqZiqO-qd4FKt4KYs1GYJDyxttuUySGtnYyZg2aYB6hamg3asRDjBMPqoURsdVJcWQh3dFnD88cbs7Qt4_ytqAY61qfPE7bJ6E33o0X7OtxmECPd3aBJk6nsyXEXNF2vIww1UCrRC0OEr1HsTqA4bQU8KCWV6kduUnvkUWPT8CF0d2ER4wnszb053Tlcf2ebcytTMf_Nd95g520Hhqb2FZALCErijBi04Bu6SNeND1NQ3nxDSKC-CamOYW0ODch05Xzi1V0_sq0zmdKTxMSpg1jOZ1Q9924D4lJkruCB3zcsIBTUxV0EgAM1zGuoqwWjwYXr_8tO4_kEO1Lw8DckZIrk1s3ySsMVC89TRrIVkG7zCCBuswggTToAMCAQICEzMAAAQI5W53M7IUDf4AAAAABAgwDQYJKoZIhvcNAQELBQAwgYwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xNjA0BgNVBAMTLU1pY3Jvc29mdCBUUE0gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAxNDAeFw0yMTA2MDMxNzUxNDhaFw0yNzA2MDMxNzUxNDhaMEExPzA9BgNVBAMTNkVVUy1OVEMtS0VZSUQtODgyRjA0N0I4NzEyMUNGOTg4NUYzMTE2MEJDN0JCNTU4NkFGNDcxQjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMEye3QdCUOGeseHj_QjLMJVbHBEJKkRbXFsmZi6Mob_3IsVfxO-mQQC-xfY9tDyB1jhxfFAG-rjUfRVBwYYPfaVo2W58Q09Kcpf-43Iw9SRxx2ThP-KPFJPFBofZroloNaTNz3DRaWZ2ha-_PUG2nwTXR7LoIpqMVW1PDzGxb47SNpRmKJxVZhQ2_wRhZZvHRHJpZmrCmHRpTRqWSzQT1jn7Zo9VuMYvp_OFj7-LFpkqi4BYyhi0kTBPDQTpYrBi7RtmF1MhZBmm1HGDhoXHcPSZkN5vq5at4g03R15KWyRDgBcckCAgtewd6Dtd_Zwaejlm57xyGqP6T-AE-N8udh1NPv_PZVlSc4CnCayUTPORuaJ7N-v7Y4wpNSIdipq29hw19WVuO_z7q6GpQbn17arYf6LSoDZfwO8GHXPrtBOYYSZCNKuZ_IK8nomBLJPtN5AzwEZNyLCZIkg0U0sJ-oVr2UEYxlwwZQm5RSDxProaKU-OXq4f_j_0pEu5_DbJx9syR3Nsv6Lt9Zkf3JSJTVtWXoM0-R_82vAJ669PX0LLr603PKWBZbW7zQvtGojT_Pc1FDGfwhcdckxd3MGpEjZwh_1D8elYcxj3Ndw5jClWosZKr33pUcjqeFtSZSur0lbm6vyCfS16XzSMn8IkHmbbXcpgGKHumUCFD8CHJIBAgMBAAGjggGOMIIBijAOBgNVHQ8BAf8EBAMCAoQwGwYDVR0lBBQwEgYJKwYBBAGCNxUkBgVngQUIAzAWBgNVHSAEDzANMAsGCSsGAQQBgjcVHzASBgNVHRMBAf8ECDAGAQH_AgEAMB0GA1UdDgQWBBSMmnF_AA0xD8rW7i0pqjSXJYwSHjAfBgNVHSMEGDAWgBR6jArOL0hiF-KU0a5VwVLscXSkVjBwBgNVHR8EaTBnMGWgY6Bhhl9odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNyb3NvZnQlMjBUUE0lMjBSb290JTIwQ2VydGlmaWNhdGUlMjBBdXRob3JpdHklMjAyMDE0LmNybDB9BggrBgEFBQcBAQRxMG8wbQYIKwYBBQUHMAKGYWh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2VydHMvTWljcm9zb2Z0JTIwVFBNJTIwUm9vdCUyMENlcnRpZmljYXRlJTIwQXV0aG9yaXR5JTIwMjAxNC5jcnQwDQYJKoZIhvcNAQELBQADggIBAHG-1grb-6xpObMtxfFScl8PRLd_GjLFaeAd0kVPls0jzKplG2Am4O87Qg0OUY0VhQ-uD39590gGrWEWnOmdrVJ-R1lJc1yrIZFEASBEedJSvxw9YTNknD59uXtznIP_4Glk-4NpqpcYov2OkBdV59V4dTL5oWFH0vzkZQfvGFxEHwtB9O6Bh0Lk142zXAh5_vf_-hSw3t9adloBAnA0AtPUVkzmgNRGeTpfPm-Iud-MAoUXaccFn2EChjKb9ApbS1ww8ZvX4x2kFU6qctu32g7Vf6CgACc1i-UYDT_E-h6c1O4n7JK2OxVS-DVwybps-cALU0gj-ZMauNMej0_x_NerzvDuQ77eFMTDMY4ZTzYOzlg4Nj0K8y1Bx_KqeTBO0N9CdEG3dBxWCUUFQzAx-i38xJL-dtYTCCFATVhc9FFJ0CQgU07JAGAeuNm_GL8kN46bMXd_ApQYFzDQUWYYXvRIt9mCw0Zd45lpAMuiDKT9TgjUVDNu8LQ8FPK0KeiQVrGHFMhHgg2pbVH9Pvc1jNEeRpCo0BLpZQwuIgEt90mepkt6Va-C9krHsU4y2oalG2LUu-jOC3NWNK8LssYVUCFWtaKh5d-xdTQmjx4uO-1sq9GFntVJ94QnEDhldz0XMopQ8srTGLMqR3MT-GkSNb5X1UFC-X1udXI8YvB3ADr_Z3B1YkFyZWFZATYAAQALAAYEcgAgnf_L82w4OuaZ-5ho3G3LidcVOIS-KAOSLBJBWL-tIq4AEAAQCAAAAAAAAQC6zY02JuN_qf28iVCISa6d6aUM5sS2PsKvZMO0P_-28lLX_9-xbmbEcTPvCj5aIEqVUfCb07U4qCBBUdaWygAbhAckvzYrACm5I8hjMoGkxMkZLw5kX0SjtHx6VfgksElxnu4DcC2g9pqL_McgddZV0zNGrYNj1iamzoxTyOcYyvZjQv_4gU-t9mEc0uqHHv-H6k23p4mensyaGAhkGTV8odyBxsNpdnR8IPnXWPO7tBDTbk4mg3VtclqLhS0-TCh_QnZ6lcEl27wTE8FjwqVdQG9F1Ouyn-eNAAq0EufbzwjSNpuDrlj-Kj5xY_lS5BMEjQUXqP-U5nzW22TehX8VaGNlcnRJbmZvWKH_VENHgBcAIgALt-OVET9fzihC1dzyG5xG70MVdRkPpSEkWQ0nns4zaYkAFGo6XjXu3JY-wup-EHJYWEuLpb45AAAACMgZxo6-OwT1-cIZwQGjXsp0dUws1gAiAAvzCsernlTAewF0QwNOA-iWpyhGxC-k_xBRjTtGNz9m6gAiAAs54tyczU1aCAh_N3Vy3qcAnC9zGeOxtQQKCe8CAanbbGhhdXRoRGF0YVkBZ-MWwK8fdtoeGn4DEn0TAUu4IUP_PMiBiJd4lDRznbBDRQAAAAAImHBYytxLgbbhMN5Q3L6WACBxLUIzn9ngKAM11_UwWG7kCiAvVyO1mYGSsEhfWeyhDaQBAwM5AQAgWQEAus2NNibjf6n9vIlQiEmunemlDObEtj7Cr2TDtD__tvJS1__fsW5mxHEz7wo-WiBKlVHwm9O1OKggQVHWlsoAG4QHJL82KwApuSPIYzKBpMTJGS8OZF9Eo7R8elX4JLBJcZ7uA3AtoPaai_zHIHXWVdMzRq2DY9Ymps6MU8jnGMr2Y0L_-IFPrfZhHNLqhx7_h-pNt6eJnp7MmhgIZBk1fKHcgcbDaXZ0fCD511jzu7QQ025OJoN1bXJai4UtPkwof0J2epXBJdu8ExPBY8KlXUBvRdTrsp_njQAKtBLn288I0jabg65Y_io-cWP5UuQTBI0FF6j_lOZ81ttk3oV_FSFDAQAB") + + // Windows Hello CA + let caCert = try! Certificate( + derEncoded: Array(Data(base64Encoded: "MIIF9TCCA92gAwIBAgIQXbYwTgy/J79JuMhpUB5dyzANBgkqhkiG9w0BAQsFADCBjDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjE2MDQGA1UEAxMtTWljcm9zb2Z0IFRQTSBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDE0MB4XDTE0MTIxMDIxMzExOVoXDTM5MTIxMDIxMzkyOFowgYwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xNjA0BgNVBAMTLU1pY3Jvc29mdCBUUE0gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAxNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJ+n+bnKt/JHIRC/oI/xgkgsYdPzP0gpvduDA2GbRtth+L4WUyoZKGBw7uz5bjjP8Aql4YExyjR3EZQ4LqnZChMpoCofbeDR4MjCE1TGwWghGpS0mM3GtWD9XiME4rE2K0VW3pdN0CLzkYbvZbs2wQTFfE62yNQiDjyHFWAZ4BQH4eWa8wrDMUxIAneUCpU6zCwM+l6Qh4ohX063BHzXlTSTc1fDsiPaKuMMjWjK9vp5UHFPa+dMAWr6OljQZPFIg3aZ4cUfzS9y+n77Hs1NXPBn6E4Db679z4DThIXyoKeZTv1aaWOWl/exsDLGt2mTMTyykVV8uD1eRjYriFpmoRDwJKAEMOfaURarzp7hka9TOElGyD2gOV4Fscr2MxAYCywLmOLzA4VDSYLuKAhPSp7yawET30AvY1HRfMwBxetSqWP2+yZRNYJlHpor5QTuRDgzR+Zej+aWx6rWNYx43kLthozeVJ3QCsD5iEI/OZlmWn5WYf7O8LB/1A7scrYv44FD8ck3Z+hxXpkklAsjJMsHZa9mBqh+VR1AicX4uZG8m16x65ZU2uUpBa3rn8CTNmw17ZHOiuSWJtS9+PrZVA8ljgf4QgA1g6NPOEiLG2fn8Gm+r5Ak+9tqv72KDd2FPBJ7Xx4stYj/WjNPtEUhW4rcLK3ktLfcy6ea7Rocw5y5AgMBAAGjUTBPMAsGA1UdDwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR6jArOL0hiF+KU0a5VwVLscXSkVjAQBgkrBgEEAYI3FQEEAwIBADANBgkqhkiG9w0BAQsFAAOCAgEAW4ioo1+J9VWC0UntSBXcXRm1ePTVamtsxVy/GpP4EmJd3Ub53JzNBfYdgfUL51CppS3ZY6BoagB+DqoA2GbSL+7sFGHBl5ka6FNelrwsH6VVw4xV/8klIjmqOyfatPYsz0sUdZev+reeiGpKVoXrK6BDnUU27/mgPtem5YKWvHB/soofUrLKzZV3WfGdx9zBr8V0xW6vO3CKaqkqU9y6EsQw34n7eJCbEVVQ8VdFd9iV1pmXwaBAfBwkviPTKEP9Cm+zbFIOLr3V3CL9hJj+gkTUuXWlJJ6wVXEG5i4rIbLAV59UrW4LonP+seqvWMJYUFxu/niF0R3fSGM+NU11DtBVkhRZt1u0kFhZqjDz1dWyfT/N7Hke3WsDqUFsBi+8SEw90rWx2aUkLvKo83oU4Mx4na+2I3l9F2a2VNGk4K7l3a00g51miPiq0Da0jqw30PaLluTMTGY5+RnZVh50JD6nk+Ea3wRkU8aiYFnpIxfKBZ72whmYYa/egj9IKeqpR0vuLebbU0fJBf880K1jWD3Z5SFyJXo057Mv0OPw5mttytE585ZIy5JsaRXlsOoWGRXE3kUT/MKR1UoAgR54c8Bsh+9Dq2wqIK9mRn15zvBDeyHG6+czurLopziOUeWokxZN1syrEdKlhFoPYavm6t+PzIcpdxZwHA+V3jLJPfI=")!) + ) + + override func setUp() { + let configuration = WebAuthnManager.Configuration( + relyingPartyID: relyingPartyID, + relyingPartyName: relyingPartyDisplayName, + relyingPartyOrigin: relyingPartyOrigin + ) + webAuthnManager = .init(configuration: configuration, challengeGenerator: .mock(generate: challenge)) + } + + func testInvalidVersion() async throws { + let mockCredentialPublicKey = TestCredentialPublicKeyBuilder().validMockRSA().buildAsByteArray() + let authData = TestAuthDataBuilder() + .relyingPartyIDHash(fromRelyingPartyID: relyingPartyID) + .flags(0b11000101) + .counter([0b00000000, 0b00000000, 0b00000000, 0b00000000]) + .attestedCredData(credentialPublicKey: mockCredentialPublicKey) + .noExtensionData() + let mockAttestationObject = TestAttestationObjectBuilder() + .fmt(.tpm) + .authData(authData) + .attStmt( + .map([.utf8String("ver"): .utf8String("1.0")]) + ) + .build() + .cborEncoded + + await assertThrowsError( + try await finishRegistration( + challenge: challenge, + attestationObject: mockAttestationObject, + rootCertificatesByFormat: [.tpm: [caCert]] + ), + expect: WebAuthnError.tpmInvalidVersion + ) + } + + func testInvalidPubArea() async throws { + let mockCredentialPublicKey = TestCredentialPublicKeyBuilder().validMockRSA().buildAsByteArray() + let mockCerts = try TestECCKeyPair.certificates() + let authData = TestAuthDataBuilder() + .relyingPartyIDHash(fromRelyingPartyID: relyingPartyID) + .flags(0b11000101) + .counter([0b00000000, 0b00000000, 0b00000000, 0b00000000]) + .attestedCredData(credentialPublicKey: mockCredentialPublicKey) + .noExtensionData() + let mockAttestationObject = TestAttestationObjectBuilder() + .fmt(.tpm) + .authData(authData) + .attStmt( + .map([ + .utf8String("ver"): .utf8String("2.0"), + .utf8String("x5c"): .array([.byteString(Array(mockCerts.leaf))]), + .utf8String("pubArea"): .byteString([0x01]) + ]) + ) + .build() + .cborEncoded + + await assertThrowsError( + try await finishRegistration( + challenge: challenge, + attestationObject: mockAttestationObject, + rootCertificatesByFormat: [.tpm: [mockCerts.ca]] + ), + expect: WebAuthnError.tpmInvalidPubArea + ) + } + + func testAttCAAttestationRSASucceeds() async throws { + let credential = try await finishRegistration( + challenge: challenge, + attestationObject: Array(Data(base64Encoded: attestationObjectBase64.urlDecoded.asString())!), + rootCertificatesByFormat: [.tpm: [caCert]] + ) + XCTAssertEqual(credential.attestationResult.format, .tpm) + XCTAssertEqual(credential.attestationResult.type, .attCA) + XCTAssertEqual(credential.attestationResult.trustChain.count, 3) + } + + private func finishRegistration( + challenge: [UInt8], + type: CredentialType = .publicKey, + rawID: [UInt8] = "e0fac9350509f71748d83782ccaf6b4c1462c615c70e255da1344e40887c8fcd".hexadecimal!, + attestationObject: [UInt8], + requireUserVerification: Bool = false, + rootCertificatesByFormat: [AttestationFormat: [Certificate]] = [:], + confirmCredentialIDNotRegisteredYet: (String) async throws -> Bool = { _ in true } + ) async throws -> Credential { + try await webAuthnManager.finishRegistration( + challenge: challenge, + credentialCreationData: RegistrationCredential( + id: rawID.base64URLEncodedString(), + type: type, + rawID: rawID, + attestationResponse: AuthenticatorAttestationResponse( + clientDataJSON: Array(Data(base64Encoded: clientDataJSONBase64)!), + attestationObject: attestationObject + ) + ), + requireUserVerification: requireUserVerification, + rootCertificatesByFormat: rootCertificatesByFormat, + confirmCredentialIDNotRegisteredYet: confirmCredentialIDNotRegisteredYet + ) + } + +} diff --git a/Tests/WebAuthnTests/Utils/TestModels/TestAuthData.swift b/Tests/WebAuthnTests/Utils/TestModels/TestAuthData.swift index d0566a47..2ab55522 100644 --- a/Tests/WebAuthnTests/Utils/TestModels/TestAuthData.swift +++ b/Tests/WebAuthnTests/Utils/TestModels/TestAuthData.swift @@ -73,6 +73,20 @@ struct TestAuthDataBuilder { .extensions([UInt8](repeating: 0, count: 20)) } + func validMockRSA() -> Self { + self + .relyingPartyIDHash(fromRelyingPartyID: "example.com") + .flags(0b11000101) + .counter([0b00000000, 0b00000000, 0b00000000, 0b00000000]) + .attestedCredData( + aaguid: [UInt8](repeating: 0, count: 16), + credentialIDLength: [0b00000000, 0b00000001], + credentialID: [0b00000001], + credentialPublicKey: TestCredentialPublicKeyBuilder().validMockRSA().buildAsByteArray() + ) + .extensions([UInt8](repeating: 0, count: 20)) + } + /// Creates a valid authData /// diff --git a/Tests/WebAuthnTests/Utils/TestModels/TestRSAKeyPair.swift b/Tests/WebAuthnTests/Utils/TestModels/TestRSAKeyPair.swift index 7caf89ef..da8e25c2 100644 --- a/Tests/WebAuthnTests/Utils/TestModels/TestRSAKeyPair.swift +++ b/Tests/WebAuthnTests/Utils/TestModels/TestRSAKeyPair.swift @@ -117,7 +117,12 @@ struct TestRSAKeyPair { CountryName("US") }, signatureAlgorithm: .sha256WithRSAEncryption, - extensions: try Certificate.Extensions {Critical(BasicConstraints.notCertificateAuthority)}, + extensions: try Certificate.Extensions { + Critical(BasicConstraints.notCertificateAuthority) + try ExtendedKeyUsage([ + .init(oid: .init(arrayLiteral: 2, 23, 133, 8, 3)) + ]) + }, issuerPrivateKey: .init(pemEncoded: caPrivateKey.pemRepresentation) ) var leafSerializer = DER.Serializer() From 7e298de2c6aec91b09cf56d45a887eb9cc3a744f Mon Sep 17 00:00:00 2001 From: Matthieu Barthelemy Date: Sat, 11 May 2024 09:49:09 +1000 Subject: [PATCH 27/30] Cleanup --- .../PackedAttestationTests/PackedAttestationTests.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/WebAuthnTests/Formats/PackedAttestationTests/PackedAttestationTests.swift b/Tests/WebAuthnTests/Formats/PackedAttestationTests/PackedAttestationTests.swift index a262898d..3d779846 100644 --- a/Tests/WebAuthnTests/Formats/PackedAttestationTests/PackedAttestationTests.swift +++ b/Tests/WebAuthnTests/Formats/PackedAttestationTests/PackedAttestationTests.swift @@ -255,7 +255,7 @@ final class RegistrationPackedAttestationTests: XCTestCase { ) .build() .cborEncoded - + let credential = try await finishRegistration(attestationObject: mockAttestationObject) XCTAssertEqual(credential.attestationResult.format, .packed) @@ -284,7 +284,7 @@ final class RegistrationPackedAttestationTests: XCTestCase { ) .build() .cborEncoded - + let credential = try await finishRegistration( attestationObject: mockAttestationObject, rootCertificatesByFormat: [.packed: [mockCerts.ca]] @@ -294,7 +294,7 @@ final class RegistrationPackedAttestationTests: XCTestCase { XCTAssertEqual(credential.attestationResult.type, .basicFull) XCTAssertEqual(credential.attestationResult.trustChain.count, 2) } - + private func finishRegistration( challenge: [UInt8] = TestConstants.mockChallenge, type: CredentialType = .publicKey, From 0966ce1421fc5d33d9cc3df6b8496f009df582c4 Mon Sep 17 00:00:00 2001 From: Matthieu Barthelemy Date: Sat, 11 May 2024 10:12:54 +1000 Subject: [PATCH 28/30] Address warnings --- .../Ceremonies/Registration/AttestedCredentialData.swift | 2 +- .../Registration/Formats/PublicKey+verifySignature.swift | 1 - .../Ceremonies/Registration/Formats/TPM/TPMAttestation.swift | 5 ----- .../AndroidKeyAttestationTests.swift | 1 - 4 files changed, 1 insertion(+), 8 deletions(-) diff --git a/Sources/WebAuthn/Ceremonies/Registration/AttestedCredentialData.swift b/Sources/WebAuthn/Ceremonies/Registration/AttestedCredentialData.swift index 432ab03c..44f783e5 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/AttestedCredentialData.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/AttestedCredentialData.swift @@ -13,7 +13,7 @@ //===----------------------------------------------------------------------===// // Contains the new public key created by the authenticator. -public struct AttestedCredentialData: Equatable { +public struct AttestedCredentialData: Equatable, Sendable { public let aaguid: [UInt8] public let credentialID: [UInt8] public let publicKey: [UInt8] diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/PublicKey+verifySignature.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/PublicKey+verifySignature.swift index f8a50ee2..1089417a 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/PublicKey+verifySignature.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/PublicKey+verifySignature.swift @@ -20,7 +20,6 @@ import _CryptoExtras extension Certificate.PublicKey { func verifySignature(_ signature: Data, algorithm: COSEAlgorithmIdentifier, data: Data) throws -> Bool { - print("\n •••• \(Self.self).verifySignature() 1, algorithm=\(algorithm)") switch algorithm { case .algES256: guard let key = P256.Signing.PublicKey(self) else { diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMAttestation.swift index 239492f0..0463ea70 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMAttestation.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMAttestation.swift @@ -80,9 +80,6 @@ struct TPMAttestation: AttestationProtocol { } } - if let pubAreaCBOR = attStmt["pubArea"], case let .byteString(pubAreaRaw) = pubAreaCBOR { - let pubArea = PubArea(from: Data(pubAreaRaw)) - } // Verify pubArea guard let pubAreaCBOR = attStmt["pubArea"], case let .byteString(pubAreaRaw) = pubAreaCBOR, @@ -91,8 +88,6 @@ struct TPMAttestation: AttestationProtocol { } switch pubArea.parameters { case let .rsa(rsaParameters): - if case let .rsa(rsaPublicKeyData) = credentialPublicKey { - } guard case let .rsa(rsaPublicKeyData) = credentialPublicKey, Array(pubArea.unique.data) == rsaPublicKeyData.n else { throw WebAuthnError.tpmInvalidPubAreaPublicKey diff --git a/Tests/WebAuthnTests/Formats/AndroidKeyAttestationTests/AndroidKeyAttestationTests.swift b/Tests/WebAuthnTests/Formats/AndroidKeyAttestationTests/AndroidKeyAttestationTests.swift index 9597b56c..aa738673 100644 --- a/Tests/WebAuthnTests/Formats/AndroidKeyAttestationTests/AndroidKeyAttestationTests.swift +++ b/Tests/WebAuthnTests/Formats/AndroidKeyAttestationTests/AndroidKeyAttestationTests.swift @@ -142,7 +142,6 @@ final class RegistrationAndroidKeyAttestationTests: XCTestCase { func testPublicKeysMismatch() async throws { let mockCerts = try TestECCKeyPair.certificates() - let verificationData: [UInt8] = [0x01] let authData = TestAuthDataBuilder().validMockRSA() let clientDataHash = SHA256.hash(data: Data(mockClientDataJSONBytes)) let mockAttestationObject = TestAttestationObjectBuilder() From 17bbee9fd009ac7519768060fc850ecc2abfe234 Mon Sep 17 00:00:00 2001 From: Matthieu Barthelemy Date: Sat, 11 May 2024 11:03:34 +1000 Subject: [PATCH 29/30] Cleanup --- .../AttestationConveyancePreference.swift | 2 +- .../AndroidKeyAttestationTests.swift | 11 +---------- .../Utils/TestModels/TestCredentialPublicKey.swift | 9 --------- 3 files changed, 2 insertions(+), 20 deletions(-) diff --git a/Sources/WebAuthn/Ceremonies/Registration/AttestationConveyancePreference.swift b/Sources/WebAuthn/Ceremonies/Registration/AttestationConveyancePreference.swift index 69de1e0d..765c4f14 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/AttestationConveyancePreference.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/AttestationConveyancePreference.swift @@ -18,7 +18,7 @@ public enum AttestationConveyancePreference: String, Encodable { /// Indicates the Relying Party is not interested in authenticator attestation. case none - //case indirect + // case indirect case direct // case enterprise } diff --git a/Tests/WebAuthnTests/Formats/AndroidKeyAttestationTests/AndroidKeyAttestationTests.swift b/Tests/WebAuthnTests/Formats/AndroidKeyAttestationTests/AndroidKeyAttestationTests.swift index aa738673..9efb0dc3 100644 --- a/Tests/WebAuthnTests/Formats/AndroidKeyAttestationTests/AndroidKeyAttestationTests.swift +++ b/Tests/WebAuthnTests/Formats/AndroidKeyAttestationTests/AndroidKeyAttestationTests.swift @@ -171,16 +171,7 @@ final class RegistrationAndroidKeyAttestationTests: XCTestCase { ) } - /*func testAttCAAttestationRSASucceeds() async throws { - let credential = try await finishRegistration( - challenge: challenge, - attestationObject: Array(Data(base64Encoded: attestationObjectBase64.urlDecoded.asString())!), - rootCertificatesByFormat: [.tpm: [caCert]] - ) - XCTAssertEqual(credential.attestationResult.format, .tpm) - XCTAssertEqual(credential.attestationResult.type, .attCA) - XCTAssertEqual(credential.attestationResult.trustChain.count, 3) - }*/ + // TODO: add test for successful attestation verification private func finishRegistration( challenge: [UInt8] = TestConstants.mockChallenge, diff --git a/Tests/WebAuthnTests/Utils/TestModels/TestCredentialPublicKey.swift b/Tests/WebAuthnTests/Utils/TestModels/TestCredentialPublicKey.swift index 8f556de8..1156ac7a 100644 --- a/Tests/WebAuthnTests/Utils/TestModels/TestCredentialPublicKey.swift +++ b/Tests/WebAuthnTests/Utils/TestModels/TestCredentialPublicKey.swift @@ -78,15 +78,6 @@ struct TestCredentialPublicKeyBuilder { .modulus(TestRSAKeyPair.publicKeyModulus) .exponent(TestRSAKeyPair.publicKeyExponent) } - - func validMockEdDSA() -> Self { - return self - .kty(.octetKey) - .crv(.ed25519) - .alg(.algEdDSA) - .xCoordinate(TestECCKeyPair.publicKeyXCoordinate) - //.yCoordinate(TestECCKeyPair.publicKeyYCoordinate) - } func kty(_ kty: COSEKeyType) -> Self { var temp = self From db54c7a175320b82d7f4184581c3d343cdca089b Mon Sep 17 00:00:00 2001 From: Matthieu Barthelemy Date: Sun, 12 May 2024 09:20:58 +1000 Subject: [PATCH 30/30] error type --- .../Registration/Formats/PublicKey+verifySignature.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/PublicKey+verifySignature.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/PublicKey+verifySignature.swift index 1089417a..b178c371 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/PublicKey+verifySignature.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/PublicKey+verifySignature.swift @@ -57,7 +57,7 @@ extension Certificate.PublicKey { return key.isValidSignature(signature, for: data, padding: .PSS) default: - throw WebAuthnError.unsupported + throw WebAuthnError.unsupportedCOSEAlgorithm } } }