Skip to content

Attestation verification #69

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 33 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
fb89843
WIP attestation
m-barthelemy Apr 25, 2024
d40d26f
WIP
m-barthelemy Apr 27, 2024
f399a3f
FidoU2FAttestation
m-barthelemy Apr 27, 2024
bce79f0
Cleanuo
m-barthelemy Apr 27, 2024
a316f13
Cleanuo
m-barthelemy Apr 27, 2024
c0b9c7d
Move verifySignature to dedicated extension file
m-barthelemy Apr 27, 2024
9733363
AttestationResult.swift
m-barthelemy Apr 28, 2024
7275dc3
Update TPM attestation; validate cert extension for .packed and .tpm
m-barthelemy Apr 28, 2024
5d10ea5
WIP TPM attestation
m-barthelemy Apr 29, 2024
3fafcf3
Certs veerifications
m-barthelemy Apr 30, 2024
1804502
Protocol for attestation verify(); add WIP AndroidKey attestation sup…
m-barthelemy May 1, 2024
c987f42
AndroidKey attestation support
m-barthelemy May 3, 2024
760f7a6
1 folder per attestation format
m-barthelemy May 3, 2024
124e615
Return attestation type
m-barthelemy May 3, 2024
1488ec0
Use X509 Certificate from swift-certificates
m-barthelemy May 4, 2024
57d05ad
Throw proper WebAuthnErrors
m-barthelemy May 4, 2024
ff2174d
Merge branch 'swift-server:main' into feature/attestation-improvements
m-barthelemy May 4, 2024
60b9211
Enable EdDSA/Ed25519 credential public keys
m-barthelemy May 4, 2024
8d0d3c3
Throw proper WebAuthnErrors
m-barthelemy May 5, 2024
609eb69
Merge branch 'feature/attestation-improvements' of github.com:nuw-run…
m-barthelemy May 5, 2024
ff94cd6
Enable EdDSA/Ed25519 credential public keys
m-barthelemy May 5, 2024
c63796c
Comment out public key verifySignature for RSA since unable to test it
m-barthelemy May 5, 2024
e6465cf
Validate EDDSA algorithm
m-barthelemy May 5, 2024
fe72a7f
Throw proper WebAuthnErrors
m-barthelemy May 5, 2024
e637a9c
Throw proper WebAuthnErrors
m-barthelemy May 5, 2024
f10b5da
Fix verifySignature
m-barthelemy May 5, 2024
d2c83eb
WIP tests
m-barthelemy May 9, 2024
1f9233d
Android attetsation tests
m-barthelemy May 10, 2024
7e298de
Cleanup
m-barthelemy May 10, 2024
6979837
Merge branch 'main' into feature/attestation-improvements
m-barthelemy May 10, 2024
0966ce1
Address warnings
m-barthelemy May 11, 2024
17bbee9
Cleanup
m-barthelemy May 11, 2024
db54c7a
error type
m-barthelemy May 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -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")]
Original file line number Diff line number Diff line change
@@ -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
}
70 changes: 48 additions & 22 deletions Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift
Original file line number Diff line number Diff line change
@@ -15,21 +15,23 @@
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 {
let authenticatorData: AuthenticatorData
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
)
}
}
33 changes: 33 additions & 0 deletions Sources/WebAuthn/Ceremonies/Registration/AttestationResult.swift
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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]
}
2 changes: 1 addition & 1 deletion Sources/WebAuthn/Ceremonies/Registration/Credential.swift
Original file line number Diff line number Diff line change
@@ -41,7 +41,7 @@ public struct Credential {

// MARK: Optional content

public let attestationObject: AttestationObject
public let attestationResult: AttestationResult

public let attestationClientDataJSON: CollectedClientData
}
Original file line number Diff line number Diff line change
@@ -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)
}
}

Original file line number Diff line number Diff line change
@@ -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)
}
}
}
Original file line number Diff line number Diff line change
@@ -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])
}
Original file line number Diff line number Diff line change
@@ -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)
}
}

Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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`, [])
}
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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)
}
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}

This file was deleted.

120 changes: 0 additions & 120 deletions Sources/WebAuthn/Ceremonies/Registration/Formats/TPMAttestation.swift

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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,19 +102,19 @@ 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.
guard rawID.count <= 1023 else {
throw WebAuthnError.credentialRawIDTooLong
}

return attestedCredentialData
return attestationResult
}
}
3 changes: 2 additions & 1 deletion Sources/WebAuthn/Ceremonies/Shared/AuthenticatorData.swift
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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
}
}
}
84 changes: 56 additions & 28 deletions Sources/WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift
Original file line number Diff line number Diff line change
@@ -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<UInt8>
var e: ArraySlice<UInt8>

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
}

}
}
3 changes: 2 additions & 1 deletion Sources/WebAuthn/Helpers/Data+safeSubscript.swift
Original file line number Diff line number Diff line change
@@ -18,8 +18,9 @@ extension Data {
struct IndexOutOfBounds: Error {}

subscript(safe range: Range<Int>) -> Data? {
let actualRange = range.lowerBound + self.startIndex..<range.upperBound+self.startIndex
guard count >= range.upperBound else { return nil }
return self[range]
return self[actualRange]
}

/// Safely slices bytes from `pointer` to `pointer` + `length`. Updates the pointer afterwards.
43 changes: 43 additions & 0 deletions Sources/WebAuthn/WebAuthnError.swift
Original file line number Diff line number Diff line change
@@ -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)
}
11 changes: 6 additions & 5 deletions Sources/WebAuthn/WebAuthnManager.swift
Original file line number Diff line number Diff line change
@@ -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
)
}
Original file line number Diff line number Diff line change
@@ -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
)
}

}
Original file line number Diff line number Diff line change
@@ -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
)
}
}
Original file line number Diff line number Diff line change
@@ -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
)
}
}
35 changes: 18 additions & 17 deletions Tests/WebAuthnTests/Formats/TPMAttestationTests/CertInfoTests.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading