diff --git a/Package.swift b/Package.swift index ac532b52..6e186a0b 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"), ], swiftSettings: [.enableExperimentalFeature("StrictConcurrency=complete")] diff --git a/Sources/WebAuthn/Ceremonies/Registration/AttestationConveyancePreference.swift b/Sources/WebAuthn/Ceremonies/Registration/AttestationConveyancePreference.swift index cba6e10a..765c4f14 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/AttestationConveyancePreference.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/AttestationConveyancePreference.swift @@ -19,6 +19,6 @@ public enum AttestationConveyancePreference: String, Encodable { /// Indicates the Relying Party is not interested in authenticator attestation. case none // case indirect - // case direct + case direct // case enterprise } diff --git a/Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift b/Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift index d03bb646..08157a87 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,14 +23,15 @@ public struct AttestationObject { let rawAuthenticatorData: [UInt8] let format: AttestationFormat let attestationStatement: CBOR + var trustPath: [Certificate] = [] func verify( relyingPartyID: String, verificationRequired: Bool, clientDataHash: SHA256.Digest, supportedPublicKeyAlgorithms: [PublicKeyCredentialParameters], - pemRootCertificatesByFormat: [AttestationFormat: [Data]] = [:] - ) async throws -> AttestedCredentialData { + rootCertificatesByFormat: [AttestationFormat: [Certificate]] = [:] + ) async throws -> AttestationResult { let relyingPartyIDHash = SHA256.hash(data: relyingPartyID.data(using: .utf8)!) guard relyingPartyIDHash == authenticatorData.relyingPartyIDHash else { @@ -56,34 +58,58 @@ public struct AttestationObject { throw WebAuthnError.unsupportedCredentialPublicKeyAlgorithm } - // let pemRootCertificates = pemRootCertificatesByFormat[format] ?? [] + let rootCertificates = rootCertificatesByFormat[format] ?? [] + var attestationType: AttestationResult.AttestationType = .none + var trustedPath: [Certificate] = [] + 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 .tpm: - // try TPMAttestation.verify( - // attStmt: attestationStatement, - // authenticatorData: rawAuthenticatorData, - // attestedCredentialData: attestedCredentialData, - // clientDataHash: Data(clientDataHash), - // credentialPublicKey: credentialPublicKey, - // pemRootCertificates: pemRootCertificates - // ) + case .packed: + (attestationType, trustedPath) = try await PackedAttestation.verify( + attStmt: attestationStatement, + authenticatorData: authenticatorData, + clientDataHash: Data(clientDataHash), + credentialPublicKey: credentialPublicKey, + rootCertificates: rootCertificates + ) + case .tpm: + (attestationType, trustedPath) = try await TPMAttestation.verify( + attStmt: attestationStatement, + authenticatorData: authenticatorData, + clientDataHash: Data(clientDataHash), + credentialPublicKey: credentialPublicKey, + rootCertificates: rootCertificates + ) + case .androidKey: + (attestationType, trustedPath) = try await AndroidKeyAttestation.verify( + attStmt: attestationStatement, + authenticatorData: authenticatorData, + clientDataHash: Data(clientDataHash), + credentialPublicKey: credentialPublicKey, + rootCertificates: rootCertificates + ) + // Legacy format used mostly by older authenticators + case .fidoU2F: + (attestationType, trustedPath) = try await FidoU2FAttestation.verify( + attStmt: attestationStatement, + authenticatorData: authenticatorData, + clientDataHash: Data(clientDataHash), + credentialPublicKey: credentialPublicKey, + rootCertificates: rootCertificates + ) default: throw WebAuthnError.attestationVerificationNotSupported } - - return attestedCredentialData + + 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 new file mode 100644 index 00000000..6d11c893 --- /dev/null +++ b/Sources/WebAuthn/Ceremonies/Registration/AttestationResult.swift @@ -0,0 +1,33 @@ +//===----------------------------------------------------------------------===// +// +// 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 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 format: AttestationFormat + public let type: AttestationType + public let trustChain: [Certificate] + + public let attestedCredentialData: AttestedCredentialData +} diff --git a/Sources/WebAuthn/Ceremonies/Registration/AttestedCredentialData.swift b/Sources/WebAuthn/Ceremonies/Registration/AttestedCredentialData.swift index d264e977..44f783e5 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, Sendable { + public let aaguid: [UInt8] + public let credentialID: [UInt8] + public let publicKey: [UInt8] } diff --git a/Sources/WebAuthn/Ceremonies/Registration/Credential.swift b/Sources/WebAuthn/Ceremonies/Registration/Credential.swift index 1c656f4d..4ceaaffa 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/Credential.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/Credential.swift @@ -41,7 +41,7 @@ public struct Credential { // MARK: Optional content - public let attestationObject: AttestationObject + 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 new file mode 100644 index 00000000..20276ae1 --- /dev/null +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKey/AndroidKeyAttestation.swift @@ -0,0 +1,83 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +// https://www.w3.org/TR/webauthn-2/#sctn-android-key-attestation +struct AndroidKeyAttestation: AttestationProtocol { + static func verify( + attStmt: CBOR, + authenticatorData: AuthenticatorData, + clientDataHash: Data, + 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 + } + + guard let x5cCBOR = attStmt["x5c"], case let .array(x5cCBOR) = x5cCBOR else { + throw WebAuthnError.invalidAttestationCertificate + } + + let x5c: [Certificate] = try x5cCBOR.map { + guard case let .byteString(certificate) = $0 else { + throw WebAuthnError.invalidAttestationCertificate + } + return try Certificate(derEncoded: certificate) + } + + guard let leafCertificate = x5c.first else { throw WebAuthnError.invalidAttestationCertificate } + let verificationData = authenticatorData.rawData + clientDataHash + // Verify signature + let leafCertificatePublicKey: Certificate.PublicKey = leafCertificate.publicKey + guard try leafCertificatePublicKey.verifySignature( + Data(sig), + algorithm: alg, + data: verificationData) else { + throw WebAuthnError.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 WebAuthnError.attestationPublicKeyMismatch + } + + let intermediates = CertificateStore(x5c[1...]) + let rootCertificatesStore = CertificateStore(rootCertificates) + var verifier = Verifier(rootCertificates: rootCertificatesStore) { + AndroidKeyVerificationPolicy(clientDataHash: clientDataHash) + } + let verifierResult: VerificationResult = await verifier.validate( + leafCertificate: leafCertificate, + intermediates: intermediates + ) + guard case .validCertificate(let chain) = verifierResult else { + throw WebAuthnError.invalidTrustPath + } + + return (.basicFull, chain) + } +} + diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKey/AndroidKeyVerificationPolicy.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKey/AndroidKeyVerificationPolicy.swift new file mode 100644 index 00000000..3e09f9cf --- /dev/null +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/AndroidKey/AndroidKeyVerificationPolicy.swift @@ -0,0 +1,302 @@ +//===----------------------------------------------------------------------===// +// +// 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-android-key-attestation +struct AndroidKeyVerificationPolicy: VerifierPolicy { + let verifyingCriticalExtensions: [ASN1ObjectIdentifier] = [ + .X509ExtensionID.basicConstraints, + .X509ExtensionID.nameConstraints, + .X509ExtensionID.subjectAlternativeName, + .X509ExtensionID.keyUsage, + ] + + private let clientDataHash: [UInt8] + + init(clientDataHash: Data) { + self.clientDataHash = Array(clientDataHash) + } + + 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)" + ) + } + + 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/AttestationProtocol.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/AttestationProtocol.swift new file mode 100644 index 00000000..c770b4a6 --- /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, + 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 new file mode 100644 index 00000000..5959e5c3 --- /dev/null +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2F/FidoU2FAttestation.swift @@ -0,0 +1,92 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +// https://www.w3.org/TR/webauthn-2/#sctn-fido-u2f-attestation +struct FidoU2FAttestation: AttestationProtocol { + static func verify( + attStmt: CBOR, + authenticatorData: AuthenticatorData, + clientDataHash: Data, + credentialPublicKey: CredentialPublicKey, + rootCertificates: [Certificate] + ) async throws -> (AttestationResult.AttestationType, [Certificate]) { + guard let sigCBOR = attStmt["sig"], case let .byteString(sig) = sigCBOR else { + throw WebAuthnError.invalidSignature + } + + guard case let .ec2(key) = credentialPublicKey, key.algorithm == .algES256 else { + throw WebAuthnError.invalidAttestationPublicKeyType + } + + guard let x5cCBOR = attStmt["x5c"], case let .array(x5cCBOR) = x5cCBOR else { + throw WebAuthnError.invalidAttestationCertificate + } + + let x5c: [Certificate] = try x5cCBOR.map { + guard case let .byteString(certificate) = $0 else { + throw WebAuthnError.invalidAttestationCertificate + } + 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 rootCertificatesStore = CertificateStore(rootCertificates) + + var verifier = Verifier(rootCertificates: rootCertificatesStore) { + RFC5280Policy(validationTime: Date()) + FidoU2FVerificationPolicy() + } + let verifierResult: VerificationResult = await verifier.validate( + leafCertificate: leafCertificate, + intermediates: .init() + ) + guard case .validCertificate(let chain) = verifierResult else { + throw WebAuthnError.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://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) + // This has been verified as not nil in AttestationObject + + authenticatorData.attestedData!.credentialID + + ansiPublicKey + ) + + // Verify signature + let leafCertificatePublicKey: Certificate.PublicKey = leafCertificate.publicKey + guard try leafCertificatePublicKey.verifySignature( + Data(sig), + algorithm: .algES256, + data: verificationData) else { + throw WebAuthnError.invalidVerificationData + } + + return (.basicFull, chain) + } +} + diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2F/FidoU2FVerificationPolicy.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2F/FidoU2FVerificationPolicy.swift new file mode 100644 index 00000000..197a3cd8 --- /dev/null +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/FidoU2F/FidoU2FVerificationPolicy.swift @@ -0,0 +1,38 @@ +//===----------------------------------------------------------------------===// +// +// 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 +struct FidoU2FVerificationPolicy: VerifierPolicy { + let verifyingCriticalExtensions: [ASN1ObjectIdentifier] = [ + .X509ExtensionID.basicConstraints, + .X509ExtensionID.nameConstraints, + .X509ExtensionID.subjectAlternativeName, + .X509ExtensionID.keyUsage, + ] + + func chainMeetsPolicyRequirements(chain: UnverifiedCertificateChain) -> PolicyEvaluationResult { + 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/Packed/PackedAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/Packed/PackedAttestation.swift new file mode 100644 index 00000000..ff29f18d --- /dev/null +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/Packed/PackedAttestation.swift @@ -0,0 +1,103 @@ +//===----------------------------------------------------------------------===// +// +// 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-packed-attestation +struct PackedAttestation: AttestationProtocol { + static func verify( + attStmt: CBOR, + authenticatorData: AuthenticatorData, + clientDataHash: Data, + 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 + } + + let verificationData = authenticatorData.rawData + clientDataHash + + if let x5cCBOR = attStmt["x5c"] { + guard case let .array(x5cCBOR) = x5cCBOR else { + throw WebAuthnError.invalidAttestationCertificate + } + + let x5c: [Certificate] = try x5cCBOR.map { + guard case let .byteString(certificate) = $0 else { + throw WebAuthnError.invalidAttestationCertificate + } + return try Certificate(derEncoded: certificate) + } + guard let attestnCert = x5c.first else { throw WebAuthnError.invalidAttestationCertificate } + + let intermediates = CertificateStore(x5c[1...]) + let rootCertificatesStore = CertificateStore(rootCertificates) + + var verifier = Verifier(rootCertificates: rootCertificatesStore) { + RFC5280Policy(validationTime: Date()) + PackedVerificationPolicy() + } + let verifierResult: VerificationResult = await verifier.validate( + leafCertificate: attestnCert, + intermediates: intermediates + ) + guard case .validCertificate(let chain) = verifierResult else { + throw WebAuthnError.invalidTrustPath + } + + // 2. Verify signature + let leafCertificatePublicKey: Certificate.PublicKey = attestnCert.publicKey + guard try leafCertificatePublicKey.verifySignature( + Data(sig), + 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} + ) { + // The AAGUID is wrapped in two OCTET STRINGS + let derValue = try DER.parse(certAAGUID.value) + 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 + guard credentialPublicKey.key.algorithm == alg else { + throw WebAuthnError.attestationPublicKeyAlgorithmMismatch + } + + guard (try? credentialPublicKey.verify(signature: Data(sig), data: verificationData)) != nil else { + throw WebAuthnError.invalidVerificationData + } + + return (.`self`, []) + } + } +} diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/Packed/PackedVerificationPolicy.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/Packed/PackedVerificationPolicy.swift new file mode 100644 index 00000000..4140be69 --- /dev/null +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/Packed/PackedVerificationPolicy.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-packed-attestation-cert-requirements +struct PackedVerificationPolicy: VerifierPolicy { + let verifyingCriticalExtensions: [ASN1ObjectIdentifier] = [ + .X509ExtensionID.basicConstraints, + .X509ExtensionID.nameConstraints, + .X509ExtensionID.subjectAlternativeName, + .X509ExtensionID.keyUsage, + ] + + 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/PackedAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedAttestation.swift deleted file mode 100644 index 4e835994..00000000 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/PackedAttestation.swift +++ /dev/null @@ -1,135 +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 -// -//===----------------------------------------------------------------------===// - -// // 🚨 WIP - -// import Foundation -// import SwiftCBOR -// import X509 -// import Crypto - -// /// 🚨 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 - -// if let x5cCBOR = attStmt["x5c"] { -// guard case let .array(x5cCBOR) = x5cCBOR else { -// throw PackedAttestationError.invalidX5C -// } - -// 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)) } -// ) - -// 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 -// } - -// // 2. Verify signature -// // let leafCertificatePublicKey: Certificate.PublicKey = leafCertificate.publicKey - -// // 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: 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) -// // } -// // } -// } 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..b178c371 --- /dev/null +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/PublicKey+verifySignature.swift @@ -0,0 +1,78 @@ +//===----------------------------------------------------------------------===// +// +// 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 SwiftASN1 +import Crypto +import _CryptoExtras + +extension Certificate.PublicKey { + func verifySignature(_ signature: Data, algorithm: COSEAlgorithmIdentifier, data: Data) throws -> Bool { + switch algorithm { + 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 .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 .algRS1, .algRS256, .algRS384, .algRS512: + guard let key = _RSA.Signing.PublicKey(self) else { + return false + } + 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.unsupportedCOSEAlgorithm + } + } +} + +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) + } + 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/TPM/TPMAttestation+Structs.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMAttestation+Structs.swift new file mode 100644 index 00000000..11434cd5 --- /dev/null +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMAttestation+Structs.swift @@ -0,0 +1,440 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +extension TPMAttestation { + enum CertInfoError: Error { + case magicInvalid + case typeInvalid + case dataTooShort + case nameAlgInvalid + case pubAreaHashInvalid + 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(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 + } + + // Verify pubArea hash + guard let nameAlg = self.attested.name[safe: 0..<2], + let nameAlg = TPMAlg(from: nameAlg), + let nameHash = self.attested.name[safe: 2..= 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/TPM/TPMAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMAttestation.swift new file mode 100644 index 00000000..0463ea70 --- /dev/null +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMAttestation.swift @@ -0,0 +1,140 @@ +//===----------------------------------------------------------------------===// +// +// 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-tpm-attestation +struct TPMAttestation: AttestationProtocol { + static func verify( + attStmt: CBOR, + authenticatorData: AuthenticatorData, + clientDataHash: Data, + credentialPublicKey: CredentialPublicKey, + rootCertificates: [Certificate] + ) async throws -> (AttestationResult.AttestationType, [Certificate]) { + // Verify version + guard let verCBOR = attStmt["ver"], + case let .utf8String(ver) = verCBOR, + ver == "2.0" else { + throw WebAuthnError.tpmInvalidVersion + } + + guard let x5cCBOR = attStmt["x5c"], + case let .array(x5cCBOR) = x5cCBOR else { + throw WebAuthnError.invalidAttestationCertificate + } + + // Verify certificate chain + let x5c: [Certificate] = try x5cCBOR.map { + guard case let .byteString(certificate) = $0 else { + throw WebAuthnError.invalidAttestationCertificate + } + return try Certificate(derEncoded: certificate) + } + + guard let aikCert = x5c.first else { throw WebAuthnError.invalidAttestationCertificate } + let intermediates = CertificateStore(x5c[1...]) + let rootCertificatesStore = CertificateStore(rootCertificates) + + var verifier = Verifier(rootCertificates: rootCertificatesStore) { + RFC5280Policy(validationTime: Date()) + TPMVerificationPolicy() + } + 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 + } + + // 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 WebAuthnError.tpmInvalidCertAaguid + } + + guard authenticatorData.attestedData?.aaguid == Array(certAaguidValue) else { + throw WebAuthnError.aaguidMismatch + } + } + + // Verify pubArea + guard let pubAreaCBOR = attStmt["pubArea"], + case let .byteString(pubAreaRaw) = pubAreaCBOR, + let pubArea = PubArea(from: Data(pubAreaRaw)) else { + throw WebAuthnError.tpmInvalidPubArea + } + 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 + } + + 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 let pubAreaCrv = COSECurve(from: eccParameters.curveID), + pubAreaCrv == ec2PublicKeyData.curve else { + throw WebAuthnError.tpmInvalidPubAreaCurve + } + } + + // Verify certInfo + guard let certInfoCBOR = attStmt["certInfo"], + case let .byteString(certInfo) = certInfoCBOR, + let parsedCertInfo = CertInfo(fromBytes: Data(certInfo)) else { + throw WebAuthnError.tpmCertInfoInvalid + } + + try parsedCertInfo.verify(pubArea: Data(pubAreaRaw)) + + guard let algCBOR = attStmt["alg"], + case let .negativeInt(algorithmNegative) = algCBOR, + let alg = COSEAlgorithmIdentifier(rawValue: -1 - Int(algorithmNegative)) else { + throw WebAuthnError.invalidAttestationSignatureAlgorithm + } + + // 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 WebAuthnError.tpmExtraDataDoesNotMatchAttToBeSignedHash + } + + return (.attCA, chain) + } +} diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMVerificationPolicy.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/TPMVerificationPolicy.swift new file mode 100644 index 00000000..2307fb6a --- /dev/null +++ b/Sources/WebAuthn/Ceremonies/Registration/Formats/TPM/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 +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. + .X509ExtensionID.subjectAlternativeName, + .X509ExtensionID.keyUsage, + .certificatePolicies, + ] + + func chainMeetsPolicyRequirements(chain: UnverifiedCertificateChain) -> PolicyEvaluationResult { + let leaf = chain.leaf + + // Version MUST be set to 3 + guard leaf.version == .v3 else { + return .failsToMeetPolicy( + reason: "Authenticator certificate 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 neither subject nor SAN. + // 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)" + ) + }*/ + + // 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 + } +} diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/TPMAttestation+Structs.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/TPMAttestation+Structs.swift deleted file mode 100644 index 0c75674e..00000000 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/TPMAttestation+Structs.swift +++ /dev/null @@ -1,410 +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 -// -//===----------------------------------------------------------------------===// - -// 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 diff --git a/Sources/WebAuthn/Ceremonies/Registration/Formats/TPMAttestation.swift b/Sources/WebAuthn/Ceremonies/Registration/Formats/TPMAttestation.swift deleted file mode 100644 index 917d97fa..00000000 --- a/Sources/WebAuthn/Ceremonies/Registration/Formats/TPMAttestation.swift +++ /dev/null @@ -1,120 +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 -// -//===----------------------------------------------------------------------===// - -// 🚨 WIP - -// import Foundation -// import SwiftCBOR - -// /// 🚨 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] -// ) throws { -// // Verify version -// guard let verCBOR = attStmt["ver"], -// case let .utf8String(ver) = verCBOR, -// ver == "2.0" else { -// throw TPMAttestationError.invalidVersion -// } - -// // 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 -// } - -// // 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, -// 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 -// } -// } - -// // 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 - -// 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 -// } -// } -// } diff --git a/Sources/WebAuthn/Ceremonies/Registration/RegistrationCredential.swift b/Sources/WebAuthn/Ceremonies/Registration/RegistrationCredential.swift index 30bc5816..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,8 +87,8 @@ struct ParsedCredentialCreationResponse { relyingPartyID: String, relyingPartyOrigin: String, supportedPublicKeyAlgorithms: [PublicKeyCredentialParameters], - pemRootCertificatesByFormat: [AttestationFormat: [Data]] - ) async throws -> AttestedCredentialData { + rootCertificatesByFormat: [AttestationFormat: [Certificate]] + ) async throws -> AttestationResult { // Step 7. - 9. try response.clientData.verify( storedChallenge: storedChallenge, @@ -101,12 +102,12 @@ 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, supportedPublicKeyAlgorithms: supportedPublicKeyAlgorithms, - pemRootCertificatesByFormat: pemRootCertificatesByFormat + rootCertificatesByFormat: rootCertificatesByFormat ) // Step 23. @@ -114,6 +115,6 @@ struct ParsedCredentialCreationResponse { throw WebAuthnError.credentialRawIDTooLong } - return attestedCredentialData + return attestationResult } } diff --git a/Sources/WebAuthn/Ceremonies/Shared/AuthenticatorData.swift b/Sources/WebAuthn/Ceremonies/Shared/AuthenticatorData.swift index d39203b3..f3ca0fe0 100644 --- a/Sources/WebAuthn/Ceremonies/Shared/AuthenticatorData.swift +++ b/Sources/WebAuthn/Ceremonies/Shared/AuthenticatorData.swift @@ -25,6 +25,7 @@ struct AuthenticatorData: Equatable, Sendable { /// 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/COSE/COSEAlgorithmIdentifier.swift b/Sources/WebAuthn/Ceremonies/Shared/COSE/COSEAlgorithmIdentifier.swift index d9c79eff..b5e74902 100644 --- a/Sources/WebAuthn/Ceremonies/Shared/COSE/COSEAlgorithmIdentifier.swift +++ b/Sources/WebAuthn/Ceremonies/Shared/COSE/COSEAlgorithmIdentifier.swift @@ -26,33 +26,36 @@ public enum COSEAlgorithmIdentifier: Int, RawRepresentable, CaseIterable, Encoda /// AlgES512 ECDSA with SHA-512 case algES512 = -36 - // We don't support RSA yet + /// AlgRS1 RSASSA-PKCS1-v1_5 with SHA-1 + case algRS1 = -65535 + /// AlgRS256 RSASSA-PKCS1-v1_5 with SHA-256 + case algRS256 = -257 + /// AlgRS384 RSASSA-PKCS1-v1_5 with SHA-384 + case algRS384 = -258 + /// AlgRS512 RSASSA-PKCS1-v1_5 with SHA-512 + case algRS512 = -259 + /// AlgPS256 RSASSA-PSS with SHA-256 + case algPS256 = -37 + /// AlgPS384 RSASSA-PSS with SHA-384 + case algPS384 = -38 + /// AlgPS512 RSASSA-PSS with SHA-512 + case algPS512 = -39 + // AlgEdDSA EdDSA + case algEdDSA = -8 - // /// AlgRS1 RSASSA-PKCS1-v1_5 with SHA-1 - // case algRS1 = -65535 - // /// AlgRS256 RSASSA-PKCS1-v1_5 with SHA-256 - // case algRS256 = -257 - // /// AlgRS384 RSASSA-PKCS1-v1_5 with SHA-384 - // case algRS384 = -258 - // /// AlgRS512 RSASSA-PKCS1-v1_5 with SHA-512 - // case algRS512 = -259 - // /// AlgPS256 RSASSA-PSS with SHA-256 - // case algPS256 = -37 - // /// AlgPS384 RSASSA-PSS with SHA-384 - // case algPS384 = -38 - // /// AlgPS512 RSASSA-PSS with SHA-512 - // case algPS512 = -39 - // // AlgEdDSA EdDSA - // 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: + case .algES256, .algRS256, .algPS256: return SHA256.hash(data: data) == compareHash - case .algES384: + case .algES384, .algRS384, .algPS384: return SHA384.hash(data: data) == compareHash - case .algES512: + case .algES512, .algRS512, .algPS512: 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 ae776172..bf3a074d 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: Sendable { var algorithm: COSEAlgorithmIdentifier { get } @@ -72,16 +73,13 @@ enum CredentialPublicKey: Sendable { throw WebAuthnError.unsupportedCOSEAlgorithm } - // Currently we only support elliptic curve algorithms switch keyType { 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)) + self = try .okp(OKPPublicKey(publicKeyObject: publicKeyObject, algorithm: algorithm)) } } @@ -154,11 +152,12 @@ struct EC2PublicKey: PublicKey, Sendable { .isValidSignature(ecdsaSignature, for: data) else { throw WebAuthnError.invalidSignature } + default: + throw WebAuthnError.unsupportedCOSEAlgorithm } } } -/// Currently not in use struct RSAPublicKeyData: PublicKey, Sendable { let algorithm: COSEAlgorithmIdentifier // swiftlint:disable:next identifier_name @@ -184,31 +183,51 @@ struct RSAPublicKeyData: PublicKey, Sendable { 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 { - 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 + } + + 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 + ) else { + throw WebAuthnError.invalidSignature + } } } -/// Currently not in use + struct OKPPublicKey: PublicKey, Sendable { let algorithm: COSEAlgorithmIdentifier let curve: UInt64 @@ -230,6 +249,15 @@ struct OKPPublicKey: PublicKey, Sendable { } func verify(signature: some DataProtocol, data: some DataProtocol) throws { - throw WebAuthnError.unsupported + 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 + } + } } 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/Sources/WebAuthn/WebAuthnError.swift b/Sources/WebAuthn/WebAuthnError.swift index 64a9749d..8c593d0d 100644 --- a/Sources/WebAuthn/WebAuthnError.swift +++ b/Sources/WebAuthn/WebAuthnError.swift @@ -67,6 +67,24 @@ public struct WebAuthnError: Error, Hashable, Sendable { case invalidExponent case unsupportedCOSEAlgorithmForRSAPublicKey case unsupported + + // MARK: Attestation + case invalidAttestationCertificate + case invalidTrustPath + case invalidAttestationSignatureAlgorithm + case invalidAttestationPublicKeyType + case invalidVerificationData + case attestationPublicKeyAlgorithmMismatch + case aaguidMismatch + case attestationPublicKeyMismatch + case tpmInvalidVersion + case tpmInvalidPubArea + case tpmInvalidPubAreaPublicKey + case tpmInvalidPubAreaCurve + case tpmCertInfoInvalid + case tpmInvalidCertAaguid + case tpmPubAreaExponentDoesNotMatchPubKeyExponent + case tpmExtraDataDoesNotMatchAttToBeSignedHash } let reason: Reason @@ -127,4 +145,29 @@ public struct WebAuthnError: Error, Hashable, Sendable { 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) + /// 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) + 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) } diff --git a/Sources/WebAuthn/WebAuthnManager.swift b/Sources/WebAuthn/WebAuthnManager.swift index 8cdc8e15..b875537b 100644 --- a/Sources/WebAuthn/WebAuthnManager.swift +++ b/Sources/WebAuthn/WebAuthnManager.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import Foundation +import X509 /// Main entrypoint for WebAuthn operations. /// @@ -94,17 +95,17 @@ public struct WebAuthnManager: Sendable { 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) - let attestedCredentialData = try await parsedData.verify( + let attestationResult = try await parsedData.verify( storedChallenge: challenge, verifyUser: requireUserVerification, relyingPartyID: configuration.relyingPartyID, relyingPartyOrigin: configuration.relyingPartyOrigin, supportedPublicKeyAlgorithms: supportedPublicKeyAlgorithms, - pemRootCertificatesByFormat: pemRootCertificatesByFormat + rootCertificatesByFormat: rootCertificatesByFormat ) // TODO: Step 18. -> Verify client extensions @@ -118,11 +119,11 @@ public struct WebAuthnManager: Sendable { 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, - attestationObject: parsedData.response.attestationObject, + attestationResult: attestationResult, attestationClientDataJSON: parsedData.response.clientData ) } diff --git a/Tests/WebAuthnTests/Formats/AndroidKeyAttestationTests/AndroidKeyAttestationTests.swift b/Tests/WebAuthnTests/Formats/AndroidKeyAttestationTests/AndroidKeyAttestationTests.swift new file mode 100644 index 00000000..9efb0dc3 --- /dev/null +++ b/Tests/WebAuthnTests/Formats/AndroidKeyAttestationTests/AndroidKeyAttestationTests.swift @@ -0,0 +1,202 @@ +//===----------------------------------------------------------------------===// +// +// 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 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 + ) + } + + // TODO: add test for successful attestation verification + + 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/FidoU2FAttestationTests.swift b/Tests/WebAuthnTests/Formats/FidoU2FAttestationTests/FidoU2FAttestationTests.swift new file mode 100644 index 00000000..597e549b --- /dev/null +++ b/Tests/WebAuthnTests/Formats/FidoU2FAttestationTests/FidoU2FAttestationTests.swift @@ -0,0 +1,167 @@ +//===----------------------------------------------------------------------===// +// +// 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: relyingPartyID) + .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/PackedAttestationTests.swift b/Tests/WebAuthnTests/Formats/PackedAttestationTests/PackedAttestationTests.swift new file mode 100644 index 00000000..3d779846 --- /dev/null +++ b/Tests/WebAuthnTests/Formats/PackedAttestationTests/PackedAttestationTests.swift @@ -0,0 +1,323 @@ +//===----------------------------------------------------------------------===// +// +// 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 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) + .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 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() + + 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/CertInfoTests.swift b/Tests/WebAuthnTests/Formats/TPMAttestationTests/CertInfoTests.swift index 7f9b139a..a4f1da04 100644 --- a/Tests/WebAuthnTests/Formats/TPMAttestationTests/CertInfoTests.swift +++ b/Tests/WebAuthnTests/Formats/TPMAttestationTests/CertInfoTests.swift @@ -12,23 +12,24 @@ // //===----------------------------------------------------------------------===// -// @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) + } +} 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/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..2ab55522 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,21 @@ 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/TestCredentialPublicKey.swift b/Tests/WebAuthnTests/Utils/TestModels/TestCredentialPublicKey.swift index 91599228..1156ac7a 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() } } @@ -60,9 +68,17 @@ struct TestCredentialPublicKeyBuilder { .crv(.p256) .alg(.algES256) .xCoordinate(TestECCKeyPair.publicKeyXCoordinate) - .yCoordiante(TestECCKeyPair.publicKeyYCoordinate) + .yCoordinate(TestECCKeyPair.publicKeyYCoordinate) } - + + func validMockRSA() -> Self { + return self + .kty(.rsaKey) + .alg(.algRS256) + .modulus(TestRSAKeyPair.publicKeyModulus) + .exponent(TestRSAKeyPair.publicKeyExponent) + } + func kty(_ kty: COSEKeyType) -> Self { var temp = self temp.wrapped.kty = .unsignedInt(kty.rawValue) @@ -87,9 +103,21 @@ struct TestCredentialPublicKeyBuilder { return temp } - func yCoordiante(_ yCoordinate: [UInt8]) -> Self { + func yCoordinate(_ yCoordinate: [UInt8]) -> Self { var temp = self 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..da8e25c2 --- /dev/null +++ b/Tests/WebAuthnTests/Utils/TestModels/TestRSAKeyPair.swift @@ -0,0 +1,134 @@ +//===----------------------------------------------------------------------===// +// +// 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) + 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/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 } )