diff --git a/Package.swift b/Package.swift index 8c9ec3f..bccf38e 100644 --- a/Package.swift +++ b/Package.swift @@ -26,8 +26,9 @@ let package = Package( .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-log.git", from: "1.0.0"), - .package(url: "https://github.com/swiftlang/swift-docc-plugin.git", from: "1.1.0") - ], + .package(url: "https://github.com/swiftlang/swift-docc-plugin.git", from: "1.1.0"), + .package(url: "https://github.com/dankinsoid/VaporToOpenAPI.git", from: "4.5.0"), + ], targets: [ .target( name: "WebAuthn", @@ -36,7 +37,8 @@ let package = Package( .product(name: "Crypto", package: "swift-crypto"), .product(name: "_CryptoExtras", package: "swift-crypto"), .product(name: "Logging", package: "swift-log"), - ], + .product(name:"VaporToOpenAPI",package:"VaporToOpenAPI"), + ], swiftSettings: [.enableExperimentalFeature("StrictConcurrency=complete")] ), .testTarget( diff --git a/Sources/WebAuthn/Ceremonies/Authentication/AuthenticationCredential.swift b/Sources/WebAuthn/Ceremonies/Authentication/AuthenticationCredential.swift index 88dc884..166099d 100644 --- a/Sources/WebAuthn/Ceremonies/Authentication/AuthenticationCredential.swift +++ b/Sources/WebAuthn/Ceremonies/Authentication/AuthenticationCredential.swift @@ -34,7 +34,7 @@ public struct AuthenticationCredential: Sendable { public let type: CredentialType } -extension AuthenticationCredential: Decodable { +extension AuthenticationCredential: Codable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) @@ -44,6 +44,17 @@ extension AuthenticationCredential: Decodable { authenticatorAttachment = try container.decodeIfPresent(AuthenticatorAttachment.self, forKey: .authenticatorAttachment) type = try container.decode(CredentialType.self, forKey: .type) } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(id, forKey: .id) + try container.encode(rawID.base64URLEncodedString(), forKey: .rawID) + try container.encode(response, forKey: .response) + try container.encodeIfPresent(authenticatorAttachment, forKey: .authenticatorAttachment) + try container.encode(type, forKey: .type) + } + private enum CodingKeys: String, CodingKey { case id diff --git a/Sources/WebAuthn/Ceremonies/Authentication/AuthenticatorAssertionResponse.swift b/Sources/WebAuthn/Ceremonies/Authentication/AuthenticatorAssertionResponse.swift index 979ba08..932b154 100644 --- a/Sources/WebAuthn/Ceremonies/Authentication/AuthenticatorAssertionResponse.swift +++ b/Sources/WebAuthn/Ceremonies/Authentication/AuthenticatorAssertionResponse.swift @@ -48,7 +48,7 @@ public struct AuthenticatorAssertionResponse: Sendable { public let attestationObject: [UInt8]? } -extension AuthenticatorAssertionResponse: Decodable { +extension AuthenticatorAssertionResponse: Codable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) @@ -58,6 +58,17 @@ extension AuthenticatorAssertionResponse: Decodable { userHandle = try container.decodeBytesFromURLEncodedBase64IfPresent(forKey: .userHandle) attestationObject = try container.decodeBytesFromURLEncodedBase64IfPresent(forKey: .attestationObject) } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(clientDataJSON.base64URLEncodedString(), forKey: .clientDataJSON) + try container.encode(authenticatorData.base64URLEncodedString(), forKey: .authenticatorData) + try container.encode(signature.base64URLEncodedString(), forKey: .signature) + try container.encodeIfPresent(userHandle?.base64URLEncodedString(),forKey: .userHandle) + try container.encodeIfPresent(attestationObject?.base64URLEncodedString(),forKey: .attestationObject) + } + private enum CodingKeys: String, CodingKey { case clientDataJSON diff --git a/Sources/WebAuthn/Ceremonies/Authentication/PublicKeyCredentialRequestOptions.swift b/Sources/WebAuthn/Ceremonies/Authentication/PublicKeyCredentialRequestOptions.swift index e142aa3..4c80e24 100644 --- a/Sources/WebAuthn/Ceremonies/Authentication/PublicKeyCredentialRequestOptions.swift +++ b/Sources/WebAuthn/Ceremonies/Authentication/PublicKeyCredentialRequestOptions.swift @@ -12,36 +12,38 @@ //===----------------------------------------------------------------------===// import Foundation +import SwiftOpenAPI /// The `PublicKeyCredentialRequestOptions` gets passed to the WebAuthn API (`navigator.credentials.get()`) /// /// When encoding using `Encodable`, the byte arrays are encoded as base64url. /// /// - SeeAlso: https://www.w3.org/TR/webauthn-2/#dictionary-assertion-options -public struct PublicKeyCredentialRequestOptions: Encodable, Sendable { +@OpenAPIDescriptable +public struct PublicKeyCredentialRequestOptions: Codable, Sendable { /// A challenge that the authenticator signs, along with other data, when producing an authentication assertion /// /// When encoding using `Encodable` this is encoded as base64url. - public let challenge: [UInt8] + public var challenge: [UInt8] /// A time, in seconds, that the caller is willing to wait for the call to complete. This is treated as a /// hint, and may be overridden by the client. /// /// - Note: When encoded, this value is represented in milleseconds as a ``UInt32``. /// See https://www.w3.org/TR/webauthn-2/#dictionary-assertion-options - public let timeout: Duration? + public var timeout: Duration? /// The ID of the Relying Party making the request. /// /// This is configured on ``WebAuthnManager`` before its ``WebAuthnManager/beginAuthentication(timeout:allowCredentials:userVerification:)`` method is called. /// - Note: When encoded, this field appears as `rpId` to match the expectations of `navigator.credentials.get()`. - public let relyingPartyID: String + public var relyingPartyID: String /// Optionally used by the client to find authenticators eligible for this authentication ceremony. - public let allowCredentials: [PublicKeyCredentialDescriptor]? + public var allowCredentials: [PublicKeyCredentialDescriptor]? /// Specifies whether the user should be verified during the authentication ceremony. - public let userVerification: UserVerificationRequirement? + public var userVerification: UserVerificationRequirement? // let extensions: [String: Any] @@ -50,15 +52,41 @@ public struct PublicKeyCredentialRequestOptions: Encodable, Sendable { try container.encode(challenge.base64URLEncodedString(), forKey: .challenge) try container.encodeIfPresent(timeout?.milliseconds, forKey: .timeout) - try container.encode(relyingPartyID, forKey: .rpID) + try container.encode(relyingPartyID, forKey: .relyingPartyID) try container.encodeIfPresent(allowCredentials, forKey: .allowCredentials) try container.encodeIfPresent(userVerification, forKey: .userVerification) } + + public init( + challenge: [UInt8], + timeout: Duration?, + relyingPartyID: String, + allowCredentials: [PublicKeyCredentialDescriptor]?, + userVerification: UserVerificationRequirement?) { + self.challenge = challenge + self.timeout = timeout + self.relyingPartyID = relyingPartyID + self.allowCredentials = allowCredentials + self.userVerification = userVerification + } + + public init(from decoder: any Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + + self.challenge = try values.decodeBytesFromURLEncodedBase64(forKey: .challenge) + + if let timeout = try values.decodeIfPresent(UInt32.self, forKey:.timeout) { + self.timeout=Duration.milliseconds(timeout) + } + self.relyingPartyID=try values.decode(String.self, forKey:.relyingPartyID) + self.allowCredentials=try values.decodeIfPresent([PublicKeyCredentialDescriptor].self,forKey: .allowCredentials) + self.userVerification=try values.decodeIfPresent(UserVerificationRequirement.self,forKey: .userVerification) + } private enum CodingKeys: String, CodingKey { case challenge case timeout - case rpID = "rpId" + case relyingPartyID = "rpId" case allowCredentials case userVerification } @@ -67,10 +95,10 @@ public struct PublicKeyCredentialRequestOptions: Encodable, Sendable { /// Information about a generated credential. /// /// When encoding using `Encodable`, `id` is encoded as base64url. -public struct PublicKeyCredentialDescriptor: Equatable, Encodable, Sendable { +public struct PublicKeyCredentialDescriptor: Equatable, Codable, Sendable { /// Defines hints as to how clients might communicate with a particular authenticator in order to obtain an /// assertion for a specific credential - public enum AuthenticatorTransport: String, Equatable, Encodable, Sendable { + public enum AuthenticatorTransport: String, Equatable, Codable, Sendable { /// Indicates the respective authenticator can be contacted over removable USB. case usb /// Indicates the respective authenticator can be contacted over Near Field Communication (NFC). @@ -114,6 +142,14 @@ public struct PublicKeyCredentialDescriptor: Equatable, Encodable, Sendable { try container.encode(id.base64URLEncodedString(), forKey: .id) try container.encodeIfPresent(transports, forKey: .transports) } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let type = try container.decode(CredentialType.self,forKey: .type) + let id = try container.decodeBytesFromURLEncodedBase64( forKey: .id) + let transports = try container.decodeIfPresent([AuthenticatorTransport].self,forKey:.transports) ?? [] + self.init(type: type, id:id, transports: transports) + } private enum CodingKeys: String, CodingKey { case type @@ -124,7 +160,7 @@ public struct PublicKeyCredentialDescriptor: Equatable, Encodable, Sendable { /// The Relying Party may require user verification for some of its operations but not for others, and may use this /// type to express its needs. -public enum UserVerificationRequirement: String, Encodable, Sendable { +public enum UserVerificationRequirement: String, Codable, Sendable { /// The Relying Party requires user verification for the operation and will fail the overall ceremony if the /// user wasn't verified. case required diff --git a/Sources/WebAuthn/Ceremonies/Registration/AttestationConveyancePreference.swift b/Sources/WebAuthn/Ceremonies/Registration/AttestationConveyancePreference.swift index 770af12..a720bc8 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/AttestationConveyancePreference.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/AttestationConveyancePreference.swift @@ -14,7 +14,7 @@ /// Options to specify the Relying Party's preference regarding attestation conveyance during credential generation. /// /// Currently only supports `none`. -public enum AttestationConveyancePreference: String, Encodable, Sendable { +public enum AttestationConveyancePreference: String, Codable, Sendable { /// Indicates the Relying Party is not interested in authenticator attestation. case none // case indirect diff --git a/Sources/WebAuthn/Ceremonies/Registration/AuthenticatorAttestationResponse.swift b/Sources/WebAuthn/Ceremonies/Registration/AuthenticatorAttestationResponse.swift index 0d45439..b986485 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/AuthenticatorAttestationResponse.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/AuthenticatorAttestationResponse.swift @@ -29,13 +29,21 @@ public struct AuthenticatorAttestationResponse: Sendable { public let attestationObject: [UInt8] } -extension AuthenticatorAttestationResponse: Decodable { +extension AuthenticatorAttestationResponse: Codable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) clientDataJSON = try container.decodeBytesFromURLEncodedBase64(forKey: .clientDataJSON) attestationObject = try container.decodeBytesFromURLEncodedBase64(forKey: .attestationObject) } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(clientDataJSON.base64URLEncodedString(), forKey: .clientDataJSON) + try container.encode(attestationObject.base64URLEncodedString(), forKey: .attestationObject) + } + private enum CodingKeys: String, CodingKey { case clientDataJSON diff --git a/Sources/WebAuthn/Ceremonies/Registration/PublicKeyCredentialCreationOptions.swift b/Sources/WebAuthn/Ceremonies/Registration/PublicKeyCredentialCreationOptions.swift index a069c4a..d2f1458 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/PublicKeyCredentialCreationOptions.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/PublicKeyCredentialCreationOptions.swift @@ -12,6 +12,7 @@ //===----------------------------------------------------------------------===// import Foundation +import SwiftOpenAPI /// The `PublicKeyCredentialCreationOptions` gets passed to the WebAuthn API (`navigator.credentials.create()`) /// @@ -19,33 +20,34 @@ import Foundation /// `Encodable` byte arrays are base64url encoded. /// /// - SeeAlso: https://www.w3.org/TR/webauthn-2/#dictionary-makecredentialoptions -public struct PublicKeyCredentialCreationOptions: Encodable, Sendable { +@OpenAPIDescriptable +public struct PublicKeyCredentialCreationOptions: Codable, Sendable { /// A byte array randomly generated by the Relying Party. Should be at least 16 bytes long to ensure sufficient /// entropy. /// /// The Relying Party should store the challenge temporarily until the registration flow is complete. When /// encoding using `Encodable`, the challenge is base64url encoded. - public let challenge: [UInt8] + public var challenge: [UInt8] /// Contains names and an identifier for the user account performing the registration - public let user: PublicKeyCredentialUserEntity + public var user: PublicKeyCredentialUserEntity /// Contains a name and an identifier for the Relying Party responsible for the request - public let relyingParty: PublicKeyCredentialRelyingPartyEntity + public var relyingParty: PublicKeyCredentialRelyingPartyEntity /// A list of key types and signature algorithms the Relying Party supports. Ordered from most preferred to least /// preferred. - public let publicKeyCredentialParameters: [PublicKeyCredentialParameters] + public var publicKeyCredentialParameters: [PublicKeyCredentialParameters] /// A time, in seconds, that the caller is willing to wait for the call to complete. This is treated as a /// hint, and may be overridden by the client. /// /// - Note: When encoded, this value is represented in milleseconds as a ``UInt32``. - public let timeout: Duration? + public var timeout: Duration? /// Sets the Relying Party's preference for attestation conveyance. At the time of writing only `none` is /// supported. - public let attestation: AttestationConveyancePreference + public var attestation: AttestationConveyancePreference public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) @@ -57,6 +59,29 @@ public struct PublicKeyCredentialCreationOptions: Encodable, Sendable { try container.encodeIfPresent(timeout?.milliseconds, forKey: .timeout) try container.encode(attestation, forKey: .attestation) } + + public init(from decoder: any Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + + self.challenge = try values.decodeBytesFromURLEncodedBase64(forKey: .challenge) + self.user = try values.decode(PublicKeyCredentialUserEntity.self, forKey: .user) + self.relyingParty = try values.decode(PublicKeyCredentialRelyingPartyEntity.self, forKey:.relyingParty) + self.publicKeyCredentialParameters = try values.decode([PublicKeyCredentialParameters].self, forKey:.publicKeyCredentialParameters) + if let timeout = try values.decodeIfPresent(UInt32.self,forKey:.timeout) { + self.timeout = Duration.milliseconds(timeout) + } + self.attestation = try values.decode(AttestationConveyancePreference.self, forKey:.attestation) + } + + public init(challenge: [UInt8],user: PublicKeyCredentialUserEntity,relyingParty: PublicKeyCredentialRelyingPartyEntity,publicKeyCredentialParameters: [PublicKeyCredentialParameters], + timeout: Duration?,attestation: AttestationConveyancePreference) { + self.challenge = challenge + self.user = user + self.relyingParty = relyingParty + self.publicKeyCredentialParameters = publicKeyCredentialParameters + self.timeout = timeout + self.attestation = attestation + } private enum CodingKeys: String, CodingKey { case challenge @@ -70,7 +95,7 @@ public struct PublicKeyCredentialCreationOptions: Encodable, Sendable { // MARK: - Credential parameters /// From §5.3 (https://w3c.github.io/TR/webauthn/#dictionary-credential-params) -public struct PublicKeyCredentialParameters: Equatable, Encodable, Sendable { +public struct PublicKeyCredentialParameters: Equatable, Codable, Sendable { /// The type of credential to be created. At the time of writing always ``CredentialType/publicKey``. public let type: CredentialType /// The cryptographic signature algorithm with which the newly generated credential will be used, and thus also @@ -87,6 +112,18 @@ public struct PublicKeyCredentialParameters: Equatable, Encodable, Sendable { self.type = type self.alg = alg } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let type = try container.decode(CredentialType.self,forKey: .type) + let alg = try container.decode(COSEAlgorithmIdentifier.self, forKey: .alg) + self.init(type:type,alg:alg) + } + + private enum CodingKeys: String, CodingKey { + case type + case alg + } } extension Array where Element == PublicKeyCredentialParameters { @@ -103,7 +140,7 @@ extension Array where Element == PublicKeyCredentialParameters { /// From §5.4.2 (https://www.w3.org/TR/webauthn/#sctn-rp-credential-params). /// The PublicKeyCredentialRelyingPartyEntity dictionary is used to supply additional Relying Party attributes when /// creating a new credential. -public struct PublicKeyCredentialRelyingPartyEntity: Encodable, Sendable { +public struct PublicKeyCredentialRelyingPartyEntity: Codable, Sendable { /// A unique identifier for the Relying Party entity. public let id: String @@ -111,6 +148,15 @@ public struct PublicKeyCredentialRelyingPartyEntity: Encodable, Sendable { /// "Wonderful Widgets, Inc." or "ОАО Примертех". public let name: String + public init(_ src : PublicKeyCredentialRelyingPartyEntity) { + self.id = src.id + self.name = src.name + } + + public init(id : String, name : String) { + self.id = id + self.name = name + } } /// From §5.4.3 (https://www.w3.org/TR/webauthn/#dictionary-user-credential-params) @@ -118,7 +164,7 @@ public struct PublicKeyCredentialRelyingPartyEntity: Encodable, Sendable { /// creating a new credential. /// /// When encoding using `Encodable`, `id` is base64url encoded. -public struct PublicKeyCredentialUserEntity: Encodable, Sendable { +public struct PublicKeyCredentialUserEntity: Codable, Sendable { /// Generated by the Relying Party, unique to the user account, and must not contain personally identifying /// information about the user. /// @@ -149,6 +195,15 @@ public struct PublicKeyCredentialUserEntity: Encodable, Sendable { try container.encode(name, forKey: .name) try container.encode(displayName, forKey: .displayName) } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let id = try container.decodeBytesFromURLEncodedBase64(forKey: .id) + let name = try container.decode(String.self, forKey: .name) + let displayName = try container.decode(String.self, forKey: .displayName) + self.init(id: id, name: name, displayName: displayName) + } + private enum CodingKeys: String, CodingKey { case id diff --git a/Sources/WebAuthn/Ceremonies/Registration/RegistrationCredential.swift b/Sources/WebAuthn/Ceremonies/Registration/RegistrationCredential.swift index fe8a88f..112be8e 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/RegistrationCredential.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/RegistrationCredential.swift @@ -31,7 +31,7 @@ public struct RegistrationCredential: Sendable { public let attestationResponse: AuthenticatorAttestationResponse } -extension RegistrationCredential: Decodable { +extension RegistrationCredential: Codable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) @@ -47,6 +47,15 @@ extension RegistrationCredential: Decodable { self.rawID = rawID attestationResponse = try container.decode(AuthenticatorAttestationResponse.self, forKey: .attestationResponse) } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(id, forKey: .id) + try container.encode(rawID.base64URLEncodedString(), forKey: .rawID) + try container.encode(attestationResponse, forKey: .attestationResponse) + } + private enum CodingKeys: String, CodingKey { case id diff --git a/Sources/WebAuthn/Ceremonies/Shared/COSE/COSEAlgorithmIdentifier.swift b/Sources/WebAuthn/Ceremonies/Shared/COSE/COSEAlgorithmIdentifier.swift index 50ecee2..29e43a7 100644 --- a/Sources/WebAuthn/Ceremonies/Shared/COSE/COSEAlgorithmIdentifier.swift +++ b/Sources/WebAuthn/Ceremonies/Shared/COSE/COSEAlgorithmIdentifier.swift @@ -17,7 +17,7 @@ import Crypto /// COSEAlgorithmIdentifier From §5.10.5. A number identifying a cryptographic algorithm. The algorithm /// identifiers SHOULD be values registered in the IANA COSE Algorithms registry /// [https://www.w3.org/TR/webauthn/#biblio-iana-cose-algs-reg], for instance, -7 for "ES256" and -257 for "RS256". -public enum COSEAlgorithmIdentifier: Int, RawRepresentable, CaseIterable, Encodable, Sendable { +public enum COSEAlgorithmIdentifier: Int, RawRepresentable, CaseIterable, Codable, Sendable { /// AlgES256 ECDSA with SHA-256 case algES256 = -7 /// AlgES384 ECDSA with SHA-384 @@ -25,33 +25,33 @@ 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 + /// 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 { 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 + } } } diff --git a/Sources/WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift b/Sources/WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift index 307b0ce..da77bb8 100644 --- a/Sources/WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift +++ b/Sources/WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift @@ -76,8 +76,7 @@ enum CredentialPublicKey: Sendable { 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)) @@ -153,6 +152,8 @@ struct EC2PublicKey: PublicKey, Sendable { .isValidSignature(ecdsaSignature, for: data) else { throw WebAuthnError.invalidSignature } + default: + throw WebAuthnError.unsupportedCredentialPublicKeyAlgorithm } } } @@ -184,26 +185,26 @@ struct RSAPublicKeyData: PublicKey, Sendable { } 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 - // } + //throw WebAuthnError.unsupported + let rsaSignature = _RSA.Signing.RSASignature(rawRepresentation: signature) + + var rsaPadding: _RSA.Signing.Padding + switch algorithm { + case .algRS1, .algRS256, .algRS384, .algRS512: + rsaPadding = .insecurePKCS1v1_5 + case .algPS256, .algPS384, .algPS512: + rsaPadding = .PSS + default: + throw WebAuthnError.unsupportedCOSEAlgorithmForRSAPublicKey + } + + guard try _RSA.Signing.PublicKey(n:n, e:e).isValidSignature( + rsaSignature, + for: data, + padding: rsaPadding) + else { + throw WebAuthnError.invalidSignature + } } } diff --git a/Sources/WebAuthn/Helpers/ChallengeGenerator.swift b/Sources/WebAuthn/Helpers/ChallengeGenerator.swift index ecbaa69..af5dfb4 100644 --- a/Sources/WebAuthn/Helpers/ChallengeGenerator.swift +++ b/Sources/WebAuthn/Helpers/ChallengeGenerator.swift @@ -11,10 +11,14 @@ // //===----------------------------------------------------------------------===// -package struct ChallengeGenerator: Sendable { - var generate: @Sendable () -> [UInt8] +public protocol ChallengeGenerator : Sendable { + func generate() -> [UInt8] +} - package static var live: Self { - .init(generate: { [UInt8].random(count: 32) }) +public struct DefaultChallengeGenerator: Sendable, ChallengeGenerator { + public func generate() -> [UInt8] { + return [UInt8].random(count: 32) } + + public static let live = DefaultChallengeGenerator() } diff --git a/Sources/WebAuthn/Helpers/KeyedDecodingContainer+decodeURLEncoded.swift b/Sources/WebAuthn/Helpers/KeyedDecodingContainer+decodeURLEncoded.swift index e6af1b5..fe59a59 100644 --- a/Sources/WebAuthn/Helpers/KeyedDecodingContainer+decodeURLEncoded.swift +++ b/Sources/WebAuthn/Helpers/KeyedDecodingContainer+decodeURLEncoded.swift @@ -13,7 +13,7 @@ import Foundation -extension KeyedDecodingContainer { +public extension KeyedDecodingContainer { func decodeBytesFromURLEncodedBase64(forKey key: KeyedDecodingContainer.Key) throws -> [UInt8] { guard let bytes = try decode( URLEncodedBase64.self, diff --git a/Sources/WebAuthn/WebAuthnManager.swift b/Sources/WebAuthn/WebAuthnManager.swift index 1da063b..1e65295 100644 --- a/Sources/WebAuthn/WebAuthnManager.swift +++ b/Sources/WebAuthn/WebAuthnManager.swift @@ -36,10 +36,10 @@ public struct WebAuthnManager: Sendable { /// - Parameters: /// - configuration: The configuration to use for this manager. public init(configuration: Configuration) { - self.init(configuration: configuration, challengeGenerator: .live) + self.init(configuration: configuration, challengeGenerator: DefaultChallengeGenerator.live) } - package init(configuration: Configuration, challengeGenerator: ChallengeGenerator) { + public init(configuration: Configuration, challengeGenerator: ChallengeGenerator) { self.configuration = configuration self.challengeGenerator = challengeGenerator } @@ -59,9 +59,10 @@ public struct WebAuthnManager: Sendable { user: PublicKeyCredentialUserEntity, timeout: Duration? = .seconds(5*60), attestation: AttestationConveyancePreference = .none, - publicKeyCredentialParameters: [PublicKeyCredentialParameters] = .supported + publicKeyCredentialParameters: [PublicKeyCredentialParameters] = .supported, + challenge : [UInt8]? = nil ) -> PublicKeyCredentialCreationOptions { - let challenge = challengeGenerator.generate() + let challenge = challenge ?? challengeGenerator.generate() return PublicKeyCredentialCreationOptions( challenge: challenge, @@ -138,9 +139,10 @@ public struct WebAuthnManager: Sendable { public func beginAuthentication( timeout: Duration? = .seconds(60), allowCredentials: [PublicKeyCredentialDescriptor]? = nil, - userVerification: UserVerificationRequirement = .preferred + userVerification: UserVerificationRequirement = .preferred, + challenge : [UInt8]? = nil ) throws -> PublicKeyCredentialRequestOptions { - let challenge = challengeGenerator.generate() + let challenge = challenge ?? challengeGenerator.generate() return PublicKeyCredentialRequestOptions( challenge: challenge, diff --git a/Tests/WebAuthnTests/Mocks/MockChallengeGenerator.swift b/Tests/WebAuthnTests/Mocks/MockChallengeGenerator.swift index a1aa4c1..63ae29b 100644 --- a/Tests/WebAuthnTests/Mocks/MockChallengeGenerator.swift +++ b/Tests/WebAuthnTests/Mocks/MockChallengeGenerator.swift @@ -13,8 +13,16 @@ @testable import WebAuthn +public struct MockChallengeGenerator : ChallengeGenerator { + + let challenge : [UInt8] + public func generate() -> [UInt8] { + return challenge + } + +} extension ChallengeGenerator { - static func mock(generate: [UInt8]) -> Self { - ChallengeGenerator(generate: { generate }) + static func mock(generate: [UInt8]) -> ChallengeGenerator { + MockChallengeGenerator(challenge: generate) } } diff --git a/Tests/WebAuthnTests/Utils/TestModels/TestAttestationObject.swift b/Tests/WebAuthnTests/Utils/TestModels/TestAttestationObject.swift index a60813a..94fd623 100644 --- a/Tests/WebAuthnTests/Utils/TestModels/TestAttestationObject.swift +++ b/Tests/WebAuthnTests/Utils/TestModels/TestAttestationObject.swift @@ -44,11 +44,19 @@ struct TestAttestationObjectBuilder { self.wrapped = wrapped } - func validMock() -> Self { + func validMockECDSA() -> Self { var temp = self temp.wrapped.fmt = .utf8String("none") temp.wrapped.attStmt = .map([:]) - temp.wrapped.authData = .byteString(TestAuthDataBuilder().validMock().build().byteArrayRepresentation) + temp.wrapped.authData = .byteString(TestAuthDataBuilder().validMockECDSA().build().byteArrayRepresentation) + return temp + } + + func validMockRSA() -> Self { + var temp = self + temp.wrapped.fmt = .utf8String("none") + temp.wrapped.attStmt = .map([:]) + temp.wrapped.authData = .byteString(TestAuthDataBuilder().validMockRSA().build().byteArrayRepresentation) return temp } diff --git a/Tests/WebAuthnTests/Utils/TestModels/TestAuthData.swift b/Tests/WebAuthnTests/Utils/TestModels/TestAuthData.swift index 05ba4ea..6f93912 100644 --- a/Tests/WebAuthnTests/Utils/TestModels/TestAuthData.swift +++ b/Tests/WebAuthnTests/Utils/TestModels/TestAuthData.swift @@ -58,7 +58,7 @@ struct TestAuthDataBuilder { build().byteArrayRepresentation.base64URLEncodedString() } - func validMock() -> Self { + func validMockECDSA() -> Self { self .relyingPartyIDHash(fromRelyingPartyID: "example.com") .flags(0b11000101) @@ -66,7 +66,20 @@ struct TestAuthDataBuilder { .attestedCredData( credentialIDLength: [0b00000000, 0b00000001], credentialID: [0b00000001], - credentialPublicKey: TestCredentialPublicKeyBuilder().validMock().buildAsByteArray() + credentialPublicKey: TestCredentialPublicKeyBuilder().validMockECDSA().buildAsByteArray() + ) + .extensions([UInt8](repeating: 0, count: 20)) + } + + func validMockRSA() -> Self { + self + .relyingPartyIDHash(fromRelyingPartyID: "example.com") + .flags(0b11000101) + .counter([0b00000000, 0b00000000, 0b00000000, 0b00000000]) + .attestedCredData( + credentialIDLength: [0b00000000, 0b00000001], + credentialID: [0b00000001], + credentialPublicKey: TestCredentialPublicKeyBuilder().validMockRSA().buildAsByteArray() ) .extensions([UInt8](repeating: 0, count: 20)) } diff --git a/Tests/WebAuthnTests/Utils/TestModels/TestCredentialPublicKey.swift b/Tests/WebAuthnTests/Utils/TestModels/TestCredentialPublicKey.swift index 7b6402f..ae38bd5 100644 --- a/Tests/WebAuthnTests/Utils/TestModels/TestCredentialPublicKey.swift +++ b/Tests/WebAuthnTests/Utils/TestModels/TestCredentialPublicKey.swift @@ -17,10 +17,17 @@ import SwiftCBOR struct TestCredentialPublicKey { var kty: CBOR? var alg: CBOR? + // EC2, OKP var crv: CBOR? var xCoordinate: CBOR? + + //EC2 var yCoordinate: CBOR? + // RSA + var nCoordinate: CBOR? + var eCoordinate: CBOR? + var byteArrayRepresentation: [UInt8] { var value: [CBOR: CBOR] = [:] if let kty { @@ -38,6 +45,15 @@ struct TestCredentialPublicKey { if let yCoordinate { value[COSEKey.y.cbor] = yCoordinate } + + if let nCoordinate { + value[COSEKey.n.cbor] = nCoordinate + } + + if let eCoordinate { + value[COSEKey.e.cbor] = eCoordinate + } + return CBOR.map(value).encode() } } @@ -53,7 +69,7 @@ struct TestCredentialPublicKeyBuilder { return wrapped.byteArrayRepresentation } - func validMock() -> Self { + func validMockECDSA() -> Self { return self .kty(.ellipticKey) .crv(.p256) @@ -61,6 +77,15 @@ struct TestCredentialPublicKeyBuilder { .xCoordinate(TestECCKeyPair.publicKeyXCoordinate) .yCoordiante(TestECCKeyPair.publicKeyYCoordinate) } + + func validMockRSA() -> Self { + return self + .kty(.rsaKey) + .alg(.algRS256) + .nCoordinate(TestRSAKeyPair.publicKeyNCoordinate) + .eCoordiante(TestRSAKeyPair.publicKeyECoordinate) + } + func kty(_ kty: COSEKeyType) -> Self { var temp = self @@ -91,4 +116,16 @@ struct TestCredentialPublicKeyBuilder { temp.wrapped.yCoordinate = .byteString(yCoordinate) return temp } + + func nCoordinate(_ nCoordinate: [UInt8]) -> Self { + var temp = self + temp.wrapped.nCoordinate = .byteString(nCoordinate) + return temp + } + + func eCoordiante(_ eCoordinate: [UInt8]) -> Self { + var temp = self + temp.wrapped.eCoordinate = .byteString(eCoordinate) + return temp + } } diff --git a/Tests/WebAuthnTests/Utils/TestModels/TestRSAKeyPair.swift b/Tests/WebAuthnTests/Utils/TestModels/TestRSAKeyPair.swift new file mode 100644 index 0000000..fa04868 --- /dev/null +++ b/Tests/WebAuthnTests/Utils/TestModels/TestRSAKeyPair.swift @@ -0,0 +1,81 @@ +// +// TestRSAKeyPair.swift +// swift-webauthn +// +// Created by David Scrève on 27/01/2025. +// + +import Foundation +import Crypto +import WebAuthn +import _CryptoExtras + + +struct TestRSAKeyPair { + static let privateKeyPEM = """ + -----BEGIN RSA PRIVATE KEY----- + MIIEpQIBAAKCAQEAngCfNRz1D1HvyvWxURSKpGtymY/qUOW0JfQ77jc8S6p1D/78 + w886pOdcPkfWQbR/qN7PbwfVDHFSJW1wbMSVdwwcUa9ELMpgQIqkLoBEjohWyAT2 + PGKfpEskSTZfq0K/CZ+ZZ4YwNNt/IH7mZhKGQHS5SHpgRAXJuATxQmt4vFSwBp+8 + aN4Wmbzl+S3w2vLY2JaEPT3rL0t5WNQa2QLhH4JWBpSywe0Jl1LxWj+gOZJdZJeN + c1dZtvwnhHXrwg0EjLILFf8V3GglWj8Gg6xuPo8+IQi+gjQEnDiOJpm7uhK4h7qZ + iK2FzUlu4PYm/4oha+LvK7IKcjjFgyAuwq6sKQIDAQABAoIBAEoE5JDPRgatTfb4 + 7t6bDvBD3eYOw6iuU5zMNB8/BSI1cq3RuLxKoqCKOm563ObfFkcYSnkrZCV2GROr + l1V9KsAgjku+HeQV0s2ppYybToKvYGhH2ssjMMKY6SDbNipXFIP/nrAe7wp0IbQp + fuoml3outHY9zkdPptZsilGhY2hmT6oAcoOt2ZWj8mITiQgxGzbT5vQBjkyppFMm + k5h+C64nS6EiJuJUUDUbvkD5hE+nHFr+165oPUmPCXGYGBiayGh8j9j7AHfGAdH3 + zSW+PWDMX9vApJZZauQ4FA7FDXzzFjiT3Xcqyl+yyqL/D5YA+GsSE3pbPZJcLJi2 + ZQ0ShbkCgYEAzhItmWbRILbXULoDfqSXYxW2XXt6Z0JFy3AaOfhbAHYUazraqByY + l0AKVMVp5cH9qk2Z/s/oqIcfsBa3P8H73yBRrDJuPQpcTWuT71qjGfgqQABqzInc + 71IB2F6WZ2Dnkh1uPczS+wiy8kv7Z4nM3hJhvgJ/ZGInQffYNSoiQisCgYEAxEjt + gTZPwXn9PyVmwW19CN1ZNR22nzTqMDZQZMvDYCbLcMEJ+Ls3TvVX5IA5hrGWGa6v + n18CdvAWebmLBww7w0FX2KF4Ug0+YGEOH15wg352dtBQ1Jx8fqeX+z+OoCjNVdiH + YDpTXd7xjcs2umotisH+vo6NHnQLuBnOcGc9ZPsCgYEAiiMtZhPCRIfMtlS7Wv3C + ba10Xh4T43xNhR5Utl+BwUFmVqtRQDhLIbjQNBtR7a6o+KykemessqxB1aykkpza + 1qu3lBMKSujTDyL6PA0qIJJ24Ahnj00rSVJT4lMlx47yLMSFze+rzpP6QOomUTXS + m1r/InxSIVyarGIUES95X5kCgYEAvnct0FZNaibfsSiv3z5JOBLh/4LHtRF5tjLe + LBD1kxXSD6Wh8XRppPq5wQcTyzoDtwQlcvaUw6kRhiifWcVrMHr1rUZyJNypDIjh + VVskvtQ2S/C0nrsCqzwhZDI2Sf+N0KF+K8gtIUe3CaqJfraNXroEYhCdq1FcFdck + 1Tm4/4UCgYEAj+raCSTOBazoE8Z+53WUJ8Y/ZrbqEc7y7ltl6FgbZLWArETglNCD + FmTawde5HZJza2x+BUJpy+31ChbaIctdu6O2tZZCa2FwdtAXf86ZJe0By4fhmK9v + m0Eq9qinAmFyVbkuIzqCJMGeC1FxUYIf/DkpAMOb/ACTyig+YFgFjdU= + -----END RSA PRIVATE KEY----- + """ + + static let publicKeyPEM = """ + -----BEGIN PUBLIC KEY----- + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAngCfNRz1D1HvyvWxURSK + pGtymY/qUOW0JfQ77jc8S6p1D/78w886pOdcPkfWQbR/qN7PbwfVDHFSJW1wbMSV + dwwcUa9ELMpgQIqkLoBEjohWyAT2PGKfpEskSTZfq0K/CZ+ZZ4YwNNt/IH7mZhKG + QHS5SHpgRAXJuATxQmt4vFSwBp+8aN4Wmbzl+S3w2vLY2JaEPT3rL0t5WNQa2QLh + H4JWBpSywe0Jl1LxWj+gOZJdZJeNc1dZtvwnhHXrwg0EjLILFf8V3GglWj8Gg6xu + Po8+IQi+gjQEnDiOJpm7uhK4h7qZiK2FzUlu4PYm/4oha+LvK7IKcjjFgyAuwq6s + KQIDAQAB + -----END PUBLIC KEY----- + """ + static let publicKeyNCoordinate = [UInt8](try! _RSA.Signing.PublicKey(pemRepresentation: publicKeyPEM).getKeyPrimitives().modulus) + static let publicKeyECoordinate = [UInt8](try! _RSA.Signing.PublicKey(pemRepresentation: publicKeyPEM).getKeyPrimitives().publicExponent) + + static func signature(data: Data) throws -> _RSA.Signing.RSASignature { + let privateKey = try _RSA.Signing.PrivateKey(pemRepresentation: privateKeyPEM) + let rsaSignature = try privateKey.signature(for: data,padding:_RSA.Signing.Padding.insecurePKCS1v1_5) + return rsaSignature + } + + static var signature: [UInt8] { + let authenticatorData = TestAuthDataBuilder() + .validAuthenticationMock() + // .counter([0, 0, 0, 1]) + .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) + } +} diff --git a/Tests/WebAuthnTests/WebAuthnManagerAuthenticationTests.swift b/Tests/WebAuthnTests/WebAuthnManagerAuthenticationECDSATests.swift similarity index 97% rename from Tests/WebAuthnTests/WebAuthnManagerAuthenticationTests.swift rename to Tests/WebAuthnTests/WebAuthnManagerAuthenticationECDSATests.swift index 52f3752..c54d41e 100644 --- a/Tests/WebAuthnTests/WebAuthnManagerAuthenticationTests.swift +++ b/Tests/WebAuthnTests/WebAuthnManagerAuthenticationECDSATests.swift @@ -16,7 +16,7 @@ import XCTest import SwiftCBOR import Crypto -final class WebAuthnManagerAuthenticationTests: XCTestCase { +final class WebAuthnManagerAuthenticationECSDATests: XCTestCase { var webAuthnManager: WebAuthnManager! let challenge: [UInt8] = [1, 0, 1] @@ -30,7 +30,7 @@ final class WebAuthnManagerAuthenticationTests: XCTestCase { relyingPartyName: relyingPartyName, relyingPartyOrigin: relyingPartyOrigin ) - webAuthnManager = .init(configuration: configuration, challengeGenerator: .mock(generate: challenge)) + webAuthnManager = .init(configuration: configuration, challengeGenerator: MockChallengeGenerator.mock(generate: challenge)) } func testBeginAuthentication() async throws { @@ -167,7 +167,7 @@ final class WebAuthnManagerAuthenticationTests: XCTestCase { authenticatorAttachment: AuthenticatorAttachment? = .platform, type: CredentialType = .publicKey, expectedChallenge: [UInt8] = TestConstants.mockChallenge, - credentialPublicKey: [UInt8] = TestCredentialPublicKeyBuilder().validMock().buildAsByteArray(), + credentialPublicKey: [UInt8] = TestCredentialPublicKeyBuilder().validMockECDSA().buildAsByteArray(), credentialCurrentSignCount: UInt32 = 0, requireUserVerification: Bool = false ) throws -> VerifiedAuthentication { diff --git a/Tests/WebAuthnTests/WebAuthnManagerAuthenticationRSATests.swift b/Tests/WebAuthnTests/WebAuthnManagerAuthenticationRSATests.swift new file mode 100644 index 0000000..20cd291 --- /dev/null +++ b/Tests/WebAuthnTests/WebAuthnManagerAuthenticationRSATests.swift @@ -0,0 +1,194 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift WebAuthn open source project +// +// Copyright (c) 2022 the Swift WebAuthn project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@testable import WebAuthn +import XCTest +import SwiftCBOR +import Crypto + +final class WebAuthnManagerAuthenticationRSATests: XCTestCase { + var webAuthnManager: WebAuthnManager! + + let challenge: [UInt8] = [1, 0, 1] + let relyingPartyID = "example.com" + let relyingPartyName = "Testy test" + let relyingPartyOrigin = "https://example.com" + + override func setUp() { + let configuration = WebAuthnManager.Configuration( + relyingPartyID: relyingPartyID, + relyingPartyName: relyingPartyName, + relyingPartyOrigin: relyingPartyOrigin + ) + webAuthnManager = .init(configuration: configuration, challengeGenerator: MockChallengeGenerator.mock(generate: challenge)) + } + + func testBeginAuthentication() async throws { + let allowCredentials: [PublicKeyCredentialDescriptor] = [.init(type: .publicKey, id: [1, 0, 2, 30])] + let options = try webAuthnManager.beginAuthentication( + timeout: .seconds(1234), + allowCredentials: allowCredentials, + userVerification: .preferred + ) + + XCTAssertEqual(options.challenge, challenge) + XCTAssertEqual(options.timeout, .seconds(1234)) + XCTAssertEqual(options.relyingPartyID, relyingPartyID) + XCTAssertEqual(options.allowCredentials, allowCredentials) + XCTAssertEqual(options.userVerification, .preferred) + } + + func testFinishAuthenticationFailsIfCredentialTypeIsInvalid() throws { + try assertThrowsError( + finishAuthentication(type: "invalid"), + expect: WebAuthnError.invalidAssertionCredentialType + ) + } + + func testFinishAuthenticationFailsIfClientDataJSONDecodingFails() throws { + try assertThrowsError(finishAuthentication(clientDataJSON: [0])) { (_: DecodingError) in + return + } + } + + func testFinishAuthenticationFailsIfCeremonyTypeDoesNotMatch() throws { + var clientDataJSON = TestClientDataJSON() + clientDataJSON.type = "webauthn.create" + try assertThrowsError( + finishAuthentication(clientDataJSON: clientDataJSON.jsonBytes), + expect: CollectedClientData.CollectedClientDataVerifyError.ceremonyTypeDoesNotMatch + ) + } + + func testFinishAuthenticationFailsIfRelyingPartyIDHashDoesNotMatch() throws { + try assertThrowsError( + finishAuthentication( + authenticatorData: TestAuthDataBuilder() + .validAuthenticationMock() + .relyingPartyIDHash(fromRelyingPartyID: "wrong-id.org") + .build() + .byteArrayRepresentation + ), + expect: WebAuthnError.relyingPartyIDHashDoesNotMatch + ) + } + + func testFinishAuthenticationFailsIfUserPresentFlagIsNotSet() throws { + try assertThrowsError( + finishAuthentication( + authenticatorData: TestAuthDataBuilder() + .validAuthenticationMock() + .flags(0b10000000) + .build() + .byteArrayRepresentation + ), + expect: WebAuthnError.userPresentFlagNotSet + ) + } + + func testFinishAuthenticationFailsIfUserIsNotVerified() throws { + try assertThrowsError( + finishAuthentication( + authenticatorData: TestAuthDataBuilder() + .validAuthenticationMock() + .flags(0b10000001) + .build() + .byteArrayRepresentation, + requireUserVerification: true + ), + expect: WebAuthnError.userVerifiedFlagNotSet + ) + } + + func testFinishAuthenticationFailsIfCredentialCounterIsNotUpToDate() throws { + try assertThrowsError( + finishAuthentication( + authenticatorData: TestAuthDataBuilder() + .validAuthenticationMock() + .counter([0, 0, 0, 1]) // signCount = 1 + .build() + .byteArrayRepresentation, + credentialCurrentSignCount: 2 + ), + expect: WebAuthnError.potentialReplayAttack + ) + } + + func testFinishAuthenticationSucceeds() throws { + let credentialID = TestConstants.mockCredentialID + let oldSignCount: UInt32 = 0 + + let authenticatorData = TestAuthDataBuilder() + .validAuthenticationMock() + .counter([0, 0, 0, 1]) + .build() + .byteArrayRepresentation + + // Create a signature. This part is usually performed by the authenticator + + // ATTENTION: It is very important that we encode `TestClientDataJSON` only once!!! Subsequent calls to + // `jsonBytes` will result in different json (and thus the signature verification will fail) + // This has already cost me hours of troubleshooting twice + let clientData = TestClientDataJSON(type: "webauthn.get").jsonBytes + let clientDataHash = SHA256.hash(data: clientData) + let signatureBase = Data(authenticatorData) + clientDataHash + let signature = try TestRSAKeyPair.signature(data: signatureBase).rawRepresentation + + let verifiedAuthentication = try finishAuthentication( + credentialID: credentialID, + clientDataJSON: clientData, + authenticatorData: authenticatorData, + signature: [UInt8](signature), + credentialCurrentSignCount: oldSignCount + ) + + XCTAssertEqual(verifiedAuthentication.credentialID, credentialID.base64URLEncodedString()) + XCTAssertEqual(verifiedAuthentication.newSignCount, oldSignCount + 1) + } + + /// Using the default parameters `finishAuthentication` should succeed. + private func finishAuthentication( + credentialID: [UInt8] = TestConstants.mockCredentialID, + clientDataJSON: [UInt8] = TestClientDataJSON(type: "webauthn.get").jsonBytes, + authenticatorData: [UInt8] = TestAuthDataBuilder().validAuthenticationMock().build().byteArrayRepresentation, + signature: [UInt8] = TestRSAKeyPair.signature, + userHandle: [UInt8]? = "36323638424436452d303831452d344331312d413743332d334444304146333345433134".hexadecimal!, + attestationObject: [UInt8]? = nil, + authenticatorAttachment: AuthenticatorAttachment? = .platform, + type: CredentialType = .publicKey, + expectedChallenge: [UInt8] = TestConstants.mockChallenge, + credentialPublicKey: [UInt8] = TestCredentialPublicKeyBuilder().validMockRSA().buildAsByteArray(), + credentialCurrentSignCount: UInt32 = 0, + requireUserVerification: Bool = false + ) throws -> VerifiedAuthentication { + try webAuthnManager.finishAuthentication( + credential: AuthenticationCredential( + id: credentialID.base64URLEncodedString(), + rawID: credentialID, + response: AuthenticatorAssertionResponse( + clientDataJSON: clientDataJSON, + authenticatorData: authenticatorData, + signature: signature, + userHandle: userHandle, + attestationObject: attestationObject + ), + authenticatorAttachment: authenticatorAttachment, + type: type + ), + expectedChallenge: expectedChallenge, + credentialPublicKey: credentialPublicKey, + credentialCurrentSignCount: credentialCurrentSignCount, + requireUserVerification: requireUserVerification + ) + } +} diff --git a/Tests/WebAuthnTests/WebAuthnManagerIntegrationTests.swift b/Tests/WebAuthnTests/WebAuthnManagerIntegrationECDSATests.swift similarity index 94% rename from Tests/WebAuthnTests/WebAuthnManagerIntegrationTests.swift rename to Tests/WebAuthnTests/WebAuthnManagerIntegrationECDSATests.swift index 0102045..91c3742 100644 --- a/Tests/WebAuthnTests/WebAuthnManagerIntegrationTests.swift +++ b/Tests/WebAuthnTests/WebAuthnManagerIntegrationECDSATests.swift @@ -15,7 +15,7 @@ import XCTest import Crypto -final class WebAuthnManagerIntegrationTests: XCTestCase { +final class WebAuthnManagerIntegrationECDSATests: XCTestCase { // swiftlint:disable:next function_body_length func testRegistrationAndAuthenticationSucceeds() async throws { let configuration = WebAuthnManager.Configuration( @@ -25,7 +25,7 @@ final class WebAuthnManagerIntegrationTests: XCTestCase { ) let mockChallenge = [UInt8](repeating: 0, count: 5) - let challengeGenerator = ChallengeGenerator(generate: { mockChallenge }) + let challengeGenerator = MockChallengeGenerator(challenge: mockChallenge) let webAuthnManager = WebAuthnManager(configuration: configuration, challengeGenerator: challengeGenerator) // Step 1.: Begin Registration @@ -55,9 +55,9 @@ final class WebAuthnManagerIntegrationTests: XCTestCase { // The following lines reflect what an authenticator normally produces let mockCredentialID = [UInt8](repeating: 1, count: 10) let mockClientDataJSON = TestClientDataJSON(challenge: mockChallenge.base64URLEncodedString()) - let mockCredentialPublicKey = TestCredentialPublicKeyBuilder().validMock().buildAsByteArray() - let mockAttestationObject = TestAttestationObjectBuilder().validMock().authData( - TestAuthDataBuilder().validMock() + let mockCredentialPublicKey = TestCredentialPublicKeyBuilder().validMockECDSA().buildAsByteArray() + let mockAttestationObject = TestAttestationObjectBuilder().validMockECDSA().authData( + TestAuthDataBuilder().validMockECDSA() .attestedCredData(credentialPublicKey: mockCredentialPublicKey) .noExtensionData() ).build().cborEncoded @@ -83,12 +83,12 @@ final class WebAuthnManagerIntegrationTests: XCTestCase { ) XCTAssertEqual(credential.id, mockCredentialID.base64EncodedString().asString()) - XCTAssertEqual(credential.attestationClientDataJSON.type, .create) + XCTAssertEqual(credential.attestationClientDataJSON.type, CollectedClientData.CeremonyType.create) XCTAssertEqual(credential.attestationClientDataJSON.origin, mockClientDataJSON.origin) XCTAssertEqual(credential.attestationClientDataJSON.challenge, mockChallenge.base64URLEncodedString()) XCTAssertEqual(credential.isBackedUp, false) XCTAssertEqual(credential.signCount, 0) - XCTAssertEqual(credential.type, .publicKey) + XCTAssertEqual(credential.type, CredentialType.publicKey) XCTAssertEqual(credential.publicKey, mockCredentialPublicKey) // Step 3.: Begin Authentication @@ -158,7 +158,7 @@ final class WebAuthnManagerIntegrationTests: XCTestCase { XCTAssertEqual(successfullAuthentication.newSignCount, 1) XCTAssertEqual(successfullAuthentication.credentialBackedUp, false) - XCTAssertEqual(successfullAuthentication.credentialDeviceType, .singleDevice) + XCTAssertEqual(successfullAuthentication.credentialDeviceType, VerifiedAuthentication.CredentialDeviceType.singleDevice) XCTAssertEqual(successfullAuthentication.credentialID, mockCredentialID.base64URLEncodedString()) // We did it! diff --git a/Tests/WebAuthnTests/WebAuthnManagerIntegrationRSATests.swift b/Tests/WebAuthnTests/WebAuthnManagerIntegrationRSATests.swift new file mode 100644 index 0000000..1d46865 --- /dev/null +++ b/Tests/WebAuthnTests/WebAuthnManagerIntegrationRSATests.swift @@ -0,0 +1,166 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift WebAuthn open source project +// +// Copyright (c) 2023 the Swift WebAuthn project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@testable import WebAuthn +import XCTest +import Crypto + +final class WebAuthnManagerIntegrationRSATests: XCTestCase { + // swiftlint:disable:next function_body_length + func testRegistrationAndAuthenticationSucceeds() async throws { + let configuration = WebAuthnManager.Configuration( + relyingPartyID: "example.com", + relyingPartyName: "Example RP", + relyingPartyOrigin: "https://example.com" + ) + + let mockChallenge = [UInt8](repeating: 0, count: 5) + let challengeGenerator = MockChallengeGenerator(challenge: mockChallenge) + let webAuthnManager = WebAuthnManager(configuration: configuration, challengeGenerator: challengeGenerator) + + // Step 1.: Begin Registration + let mockUser = PublicKeyCredentialUserEntity.mock + let timeout: Duration = .seconds(1234) + let attestationPreference = AttestationConveyancePreference.none + let publicKeyCredentialParameters: [PublicKeyCredentialParameters] = .supported + + let registrationOptions = webAuthnManager.beginRegistration( + user: mockUser, + timeout: timeout, + attestation: attestationPreference, + publicKeyCredentialParameters: publicKeyCredentialParameters + ) + + XCTAssertEqual(registrationOptions.challenge, mockChallenge) + XCTAssertEqual(registrationOptions.user.id, mockUser.id) + XCTAssertEqual(registrationOptions.user.name, mockUser.name) + XCTAssertEqual(registrationOptions.user.displayName, mockUser.displayName) + XCTAssertEqual(registrationOptions.attestation, attestationPreference) + XCTAssertEqual(registrationOptions.relyingParty.id, configuration.relyingPartyID) + XCTAssertEqual(registrationOptions.relyingParty.name, configuration.relyingPartyName) + XCTAssertEqual(registrationOptions.timeout, timeout) + XCTAssertEqual(registrationOptions.publicKeyCredentialParameters, publicKeyCredentialParameters) + + // Now send `registrationOptions` to client, which in turn will send the authenticator's response back to us: + // The following lines reflect what an authenticator normally produces + let mockCredentialID = [UInt8](repeating: 1, count: 10) + let mockClientDataJSON = TestClientDataJSON(challenge: mockChallenge.base64URLEncodedString()) + let mockCredentialPublicKey = TestCredentialPublicKeyBuilder().validMockRSA().buildAsByteArray() + let mockAttestationObject = TestAttestationObjectBuilder().validMockRSA().authData( + TestAuthDataBuilder().validMockRSA() + .attestedCredData(credentialPublicKey: mockCredentialPublicKey) + .noExtensionData() + ).build().cborEncoded + + let registrationResponse = RegistrationCredential( + id: mockCredentialID.base64URLEncodedString(), + type: .publicKey, + rawID: mockCredentialID, + attestationResponse: AuthenticatorAttestationResponse( + clientDataJSON: mockClientDataJSON.jsonBytes, + attestationObject: mockAttestationObject + ) + ) + + // Step 2.: Finish Registration + let credential = try await webAuthnManager.finishRegistration( + challenge: mockChallenge, + credentialCreationData: registrationResponse, + requireUserVerification: true, + supportedPublicKeyAlgorithms: publicKeyCredentialParameters, + pemRootCertificatesByFormat: [:], + confirmCredentialIDNotRegisteredYet: { _ in true } + ) + + XCTAssertEqual(credential.id, mockCredentialID.base64EncodedString().asString()) + XCTAssertEqual(credential.attestationClientDataJSON.type, CollectedClientData.CeremonyType.create) + XCTAssertEqual(credential.attestationClientDataJSON.origin, mockClientDataJSON.origin) + XCTAssertEqual(credential.attestationClientDataJSON.challenge, mockChallenge.base64URLEncodedString()) + XCTAssertEqual(credential.isBackedUp, false) + XCTAssertEqual(credential.signCount, 0) + XCTAssertEqual(credential.type, CredentialType.publicKey) + XCTAssertEqual(credential.publicKey, mockCredentialPublicKey) + + // Step 3.: Begin Authentication + let authenticationTimeout: Duration = .seconds(4567) + let userVerification: UserVerificationRequirement = .preferred + let rememberedCredentials = [PublicKeyCredentialDescriptor( + type: .publicKey, + id: [UInt8](URLEncodedBase64(credential.id).urlDecoded.decoded!) + )] + + let authenticationOptions = try webAuthnManager.beginAuthentication( + timeout: authenticationTimeout, + allowCredentials: rememberedCredentials, + userVerification: userVerification + ) + + XCTAssertEqual(authenticationOptions.relyingPartyID, configuration.relyingPartyID) + XCTAssertEqual(authenticationOptions.timeout, authenticationTimeout) + XCTAssertEqual(authenticationOptions.challenge, mockChallenge) + XCTAssertEqual(authenticationOptions.userVerification, userVerification) + XCTAssertEqual(authenticationOptions.allowCredentials, rememberedCredentials) + + // Now send `authenticationOptions` to client, which in turn will send the authenticator's response back to us: + // The following lines reflect what an authenticator normally produces + let authenticatorData = TestAuthDataBuilder().validAuthenticationMock() + .relyingPartyIDHash(fromRelyingPartyID: configuration.relyingPartyID) + .counter([0, 0, 0, 1]) // we authenticated once now, so authenticator likely increments the sign counter + .build() + .byteArrayRepresentation + + // Authenticator creates a signature with private key + + // ATTENTION: It is very important that we encode `TestClientDataJSON` only once!!! Subsequent calls to + // `jsonBytes` will result in different json (and thus the signature verification will fail) + // This has already cost me hours of troubleshooting twice + let clientData = TestClientDataJSON( + type: "webauthn.get", + challenge: mockChallenge.base64URLEncodedString() + ).jsonBytes + let clientDataHash = SHA256.hash(data: clientData) + let signatureBase = Data(authenticatorData + clientDataHash) + let signature = try TestRSAKeyPair.signature(data: signatureBase).rawRepresentation + + let authenticationCredential = AuthenticationCredential( + id: mockCredentialID.base64URLEncodedString(), + rawID: mockCredentialID, + response: AuthenticatorAssertionResponse( + clientDataJSON: clientData, + authenticatorData: authenticatorData, + signature: [UInt8](signature), + userHandle: mockUser.id, + attestationObject: nil + ), + authenticatorAttachment: .platform, + type: .publicKey + ) + + // Step 4.: Finish Authentication + let oldSignCount: UInt32 = 0 + let successfullAuthentication = try webAuthnManager.finishAuthentication( + credential: authenticationCredential, + expectedChallenge: mockChallenge, + credentialPublicKey: mockCredentialPublicKey, + credentialCurrentSignCount: oldSignCount, + requireUserVerification: false + ) + + XCTAssertEqual(successfullAuthentication.newSignCount, 1) + XCTAssertEqual(successfullAuthentication.credentialBackedUp, false) + XCTAssertEqual(successfullAuthentication.credentialDeviceType, VerifiedAuthentication.CredentialDeviceType.singleDevice) + XCTAssertEqual(successfullAuthentication.credentialID, mockCredentialID.base64URLEncodedString()) + + // We did it! + } +} diff --git a/Tests/WebAuthnTests/WebAuthnManagerRegistrationECDSATests.swift b/Tests/WebAuthnTests/WebAuthnManagerRegistrationECDSATests.swift new file mode 100644 index 0000000..618db64 --- /dev/null +++ b/Tests/WebAuthnTests/WebAuthnManagerRegistrationECDSATests.swift @@ -0,0 +1,389 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift WebAuthn open source project +// +// Copyright (c) 2022 the Swift WebAuthn project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@testable import WebAuthn +import XCTest +import SwiftCBOR + +// swiftlint:disable:next type_body_length +final class WebAuthnManagerRegistrationECDSATests: XCTestCase { + var webAuthnManager: WebAuthnManager! + + let challenge: [UInt8] = [1, 0, 1] + let relyingPartyDisplayName = "Testy test" + let relyingPartyID = "example.com" + let relyingPartyOrigin = "https://example.com" + + override func setUp() { + let configuration = WebAuthnManager.Configuration( + relyingPartyID: relyingPartyID, + relyingPartyName: relyingPartyDisplayName, + relyingPartyOrigin: relyingPartyOrigin + ) + webAuthnManager = .init(configuration: configuration, challengeGenerator: MockChallengeGenerator.mock(generate: challenge)) + } + + // MARK: - beginRegistration() + + func testBeginRegistrationReturns() throws { + let user = PublicKeyCredentialUserEntity.mock + let publicKeyCredentialParameter = PublicKeyCredentialParameters(type: .publicKey, alg: .algES256) + let options = webAuthnManager.beginRegistration( + user: user, + publicKeyCredentialParameters: [publicKeyCredentialParameter] + ) + + XCTAssertEqual(options.challenge, challenge) + XCTAssertEqual(options.relyingParty.id, relyingPartyID) + XCTAssertEqual(options.relyingParty.name, relyingPartyDisplayName) + XCTAssertEqual(options.user.id, user.id) + XCTAssertEqual(options.user.displayName, user.displayName) + XCTAssertEqual(options.user.name, user.name) + XCTAssertEqual(options.publicKeyCredentialParameters, [publicKeyCredentialParameter]) + } + + // MARK: - finishRegistration() + + func testFinishRegistrationFailsIfCeremonyTypeDoesNotMatch() async throws { + var clientDataJSON = TestClientDataJSON() + clientDataJSON.type = "webauthn.get" + try await assertThrowsError( + await finishRegistration(clientDataJSON: clientDataJSON.jsonBytes), + expect: CollectedClientData.CollectedClientDataVerifyError.ceremonyTypeDoesNotMatch + ) + } + + func testFinishRegistrationFailsIfChallengeDoesNotMatch() async throws { + var clientDataJSON = TestClientDataJSON() + clientDataJSON.challenge = [0, 2, 4].base64URLEncodedString() + try await assertThrowsError( + await finishRegistration( + challenge: [UInt8]("definitely another challenge".utf8), + clientDataJSON: clientDataJSON.jsonBytes + ), + expect: CollectedClientData.CollectedClientDataVerifyError.challengeDoesNotMatch + ) + } + + func testFinishRegistrationFailsIfOriginDoesNotMatch() async throws { + var clientDataJSON = TestClientDataJSON() + clientDataJSON.origin = "https://random-origin.org" + // `webAuthnManager` is configured with origin = https://example.com + try await assertThrowsError( + await finishRegistration(clientDataJSON: clientDataJSON.jsonBytes), + expect: CollectedClientData.CollectedClientDataVerifyError.originDoesNotMatch + ) + } + + func testFinishRegistrationFailsWithInvalidCredentialCreationType() async throws { + try await assertThrowsError( + await finishRegistration(type: "foo"), + expect: WebAuthnError.invalidCredentialCreationType + ) + } + + func testFinishRegistrationFailsIfClientDataJSONDecodingFails() async throws { + try await assertThrowsError(await finishRegistration(clientDataJSON: [0])) { (_: DecodingError) in + return + } + } + + func testFinishRegistrationFailsIfAttestationObjectIsNotBase64() async throws { + try await assertThrowsError( + await finishRegistration(attestationObject: []), + expect: WebAuthnError.invalidAttestationObject + ) + } + + func testFinishRegistrationFailsIfAuthDataIsInvalid() async throws { + try await assertThrowsError( + await finishRegistration( + attestationObject: TestAttestationObjectBuilder() + .validMockECDSA() + .invalidAuthData() + .build() + .cborEncoded + ), + expect: WebAuthnError.invalidAuthData + ) + } + + func testFinishRegistrationFailsIfFmtIsInvalid() async throws { + try await assertThrowsError( + await finishRegistration( + attestationObject: TestAttestationObjectBuilder() + .validMockECDSA() + .invalidFmt() + .build() + .cborEncoded + ), + expect: WebAuthnError.invalidFmt + ) + } + + func testFinishRegistrationFailsIfAttStmtIsMissing() async throws { + try await assertThrowsError( + await finishRegistration( + attestationObject: TestAttestationObjectBuilder() + .validMockECDSA() + .missingAttStmt() + .build() + .cborEncoded + ), + expect: WebAuthnError.missingAttStmt + ) + } + + func testFinishRegistrationFailsIfAuthDataIsTooShort() async throws { + try await assertThrowsError( + await finishRegistration( + attestationObject: TestAttestationObjectBuilder() + .validMockECDSA() + .zeroAuthData(byteCount: 36) + .build() + .cborEncoded + ), + expect: WebAuthnError.authDataTooShort + ) + } + + func testFinishRegistrationFailsIfAttestedCredentialDataFlagIsSetButThereIsNoCredentialData() async throws { + try await assertThrowsError( + await finishRegistration( + attestationObject: TestAttestationObjectBuilder() + .validMockECDSA() + .authData( + TestAuthDataBuilder() + .validMockECDSA() + .flags(0b01000001) + .noAttestedCredentialData() + .noExtensionData() + ) + .build() + .cborEncoded + ), + expect: WebAuthnError.attestedCredentialDataMissing + ) + } + + func testFinishRegistrationFailsIfAttestedCredentialDataFlagIsNotSetButThereIsCredentialData() async throws { + try await assertThrowsError( + await finishRegistration( + attestationObject: TestAttestationObjectBuilder() + .validMockECDSA() + .authData( + TestAuthDataBuilder() + .validMockECDSA() + .flags(0b00000001) + .attestedCredData(credentialPublicKey: []) + ) + .build() + .cborEncoded + ), + expect: WebAuthnError.attestedCredentialFlagNotSet + ) + } + + func testFinishRegistrationFailsIfExtensionDataFlagIsSetButThereIsNoExtensionData() async throws { + try await assertThrowsError( + await finishRegistration( + attestationObject: TestAttestationObjectBuilder() + .validMockECDSA() + .authData(TestAuthDataBuilder().validMockECDSA().noExtensionData().flags(0b11000001)) + .build() + .cborEncoded + ), + expect: WebAuthnError.extensionDataMissing + ) + } + + func testFinishRegistrationFailsIfCredentialIdIsTooShort() async throws { + try await assertThrowsError( + await finishRegistration( + attestationObject: TestAttestationObjectBuilder() + .validMockECDSA() + .authData( + TestAuthDataBuilder() + .validMockECDSA() + .attestedCredData( + credentialIDLength: [0b00000000, 0b00000010], // we expect length = 2 + credentialID: [255], // but only get length = 1 + credentialPublicKey: [] + ) + .noExtensionData() + ) + .build() + .cborEncoded + ), + expect: WebAuthnError.credentialIDTooShort + ) + } + + func testFinishRegistrationFailsIfRelyingPartyIDHashDoesNotMatch() async throws { + try await assertThrowsError( + await finishRegistration( + attestationObject: TestAttestationObjectBuilder() + .validMockECDSA() + .authData(TestAuthDataBuilder().validMockECDSA().relyingPartyIDHash(fromRelyingPartyID: "invalid-id.com")) + .build() + .cborEncoded + ), + expect: WebAuthnError.relyingPartyIDHashDoesNotMatch + ) + } + + func testFinishRegistrationFailsIfUserPresentFlagIsNotSet() async throws { + try await assertThrowsError( + await finishRegistration( + attestationObject: TestAttestationObjectBuilder() + .validMockECDSA() + .authData(TestAuthDataBuilder().validMockECDSA().flags(0b11000000)) + .build() + .cborEncoded + ), + expect: WebAuthnError.userPresentFlagNotSet + ) + } + + func testFinishRegistrationFailsIfUserVerificationFlagIsNotSetButRequired() async throws { + try await assertThrowsError( + await finishRegistration( + attestationObject: TestAttestationObjectBuilder() + .validMockECDSA() + .authData(TestAuthDataBuilder().validMockECDSA().flags(0b11000001)) + .build() + .cborEncoded, + requireUserVerification: true + ), + expect: WebAuthnError.userVerificationRequiredButFlagNotSet + ) + } + + func testFinishRegistrationFailsIfAttFmtIsNoneButAttStmtIsIncluded() async throws { + try await assertThrowsError( + await finishRegistration( + attestationObject: TestAttestationObjectBuilder() + .validMockECDSA() + .fmt("none") + .attStmt(.double(123)) + .build() + .cborEncoded, + requireUserVerification: true + ), + expect: WebAuthnError.attestationStatementMustBeEmpty + ) + } + + func testFinishRegistrationFailsIfRawIDIsTooLong() async throws { + try await assertThrowsError( + await finishRegistration(rawID: [UInt8](repeating: 0, count: 1024)), + expect: WebAuthnError.credentialRawIDTooLong + ) + } + + func testFinishAuthenticationFailsIfCredentialIDTooLong() async throws { + /// This should succeed as it's on the border of being acceptable + _ = try await finishRegistration( + attestationObject: TestAttestationObjectBuilder() + .validMockECDSA() + .authData( + TestAuthDataBuilder() + .validMockECDSA() + .attestedCredData( + credentialIDLength: [0b000_00011, 0b1111_1111], + credentialID: Array(repeating: 0, count: 1023), + credentialPublicKey: TestCredentialPublicKeyBuilder().validMockECDSA().buildAsByteArray() + ) + ) + .build() + .cborEncoded + ) + + /// While this one should throw + try await assertThrowsError( + await finishRegistration( + attestationObject: TestAttestationObjectBuilder() + .validMockECDSA() + .authData( + TestAuthDataBuilder() + .validMockECDSA() + .attestedCredData( + credentialIDLength: [0b000_00100, 0b0000_0000], + credentialID: Array(repeating: 0, count: 1024), + credentialPublicKey: TestCredentialPublicKeyBuilder().validMockECDSA().buildAsByteArray() + ) + ) + .build() + .cborEncoded + ), + expect: WebAuthnError.credentialIDTooLong + ) + } + + func testFinishRegistrationSucceeds() async throws { + let credentialID: [UInt8] = [0, 1, 0, 1, 0, 1] + let credentialPublicKey: [UInt8] = TestCredentialPublicKeyBuilder().validMockECDSA().buildAsByteArray() + let authData = TestAuthDataBuilder() + .validMockECDSA() + .attestedCredData(credentialPublicKey: credentialPublicKey) + .noExtensionData() + let attestationObject = TestAttestationObjectBuilder() + .validMockECDSA() + .authData(authData) + .build() + .cborEncoded + + let credential = try await finishRegistration( + rawID: credentialID, + attestationObject: attestationObject + ) + XCTAssertNotNil(credential) + + XCTAssertEqual(credential.id, credentialID.base64EncodedString().asString()) + XCTAssertEqual(credential.publicKey, credentialPublicKey) + } + + func testFinishRegistrationFuzzying() async throws { + for _ in 1...50 { + let length = Int.random(in: 1...10_000_000) + let randomAttestationObject = Array(repeating: UInt8.random(), count: length) + let shouldBeNil = try? await finishRegistration(attestationObject: randomAttestationObject) + XCTAssertNil(shouldBeNil) + } + } + + private func finishRegistration( + challenge: [UInt8] = TestConstants.mockChallenge, + type: CredentialType = .publicKey, + rawID: [UInt8] = "e0fac9350509f71748d83782ccaf6b4c1462c615c70e255da1344e40887c8fcd".hexadecimal!, + clientDataJSON: [UInt8] = TestClientDataJSON().jsonBytes, + attestationObject: [UInt8] = TestAttestationObjectBuilder().validMockECDSA().build().cborEncoded, + requireUserVerification: Bool = false, + 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: clientDataJSON, + attestationObject: attestationObject + ) + ), + requireUserVerification: requireUserVerification, + confirmCredentialIDNotRegisteredYet: confirmCredentialIDNotRegisteredYet + ) + } +} diff --git a/Tests/WebAuthnTests/WebAuthnManagerRegistrationTests.swift b/Tests/WebAuthnTests/WebAuthnManagerRegistrationRSATests.swift similarity index 90% rename from Tests/WebAuthnTests/WebAuthnManagerRegistrationTests.swift rename to Tests/WebAuthnTests/WebAuthnManagerRegistrationRSATests.swift index 0654d5f..1fb9f36 100644 --- a/Tests/WebAuthnTests/WebAuthnManagerRegistrationTests.swift +++ b/Tests/WebAuthnTests/WebAuthnManagerRegistrationRSATests.swift @@ -16,7 +16,7 @@ import XCTest import SwiftCBOR // swiftlint:disable:next type_body_length -final class WebAuthnManagerRegistrationTests: XCTestCase { +final class WebAuthnManagerRegistrationRSATests: XCTestCase { var webAuthnManager: WebAuthnManager! let challenge: [UInt8] = [1, 0, 1] @@ -30,7 +30,7 @@ final class WebAuthnManagerRegistrationTests: XCTestCase { relyingPartyName: relyingPartyDisplayName, relyingPartyOrigin: relyingPartyOrigin ) - webAuthnManager = .init(configuration: configuration, challengeGenerator: .mock(generate: challenge)) + webAuthnManager = .init(configuration: configuration, challengeGenerator: MockChallengeGenerator.mock(generate: challenge)) } // MARK: - beginRegistration() @@ -109,7 +109,7 @@ final class WebAuthnManagerRegistrationTests: XCTestCase { try await assertThrowsError( await finishRegistration( attestationObject: TestAttestationObjectBuilder() - .validMock() + .validMockRSA() .invalidAuthData() .build() .cborEncoded @@ -122,7 +122,7 @@ final class WebAuthnManagerRegistrationTests: XCTestCase { try await assertThrowsError( await finishRegistration( attestationObject: TestAttestationObjectBuilder() - .validMock() + .validMockRSA() .invalidFmt() .build() .cborEncoded @@ -135,7 +135,7 @@ final class WebAuthnManagerRegistrationTests: XCTestCase { try await assertThrowsError( await finishRegistration( attestationObject: TestAttestationObjectBuilder() - .validMock() + .validMockRSA() .missingAttStmt() .build() .cborEncoded @@ -148,7 +148,7 @@ final class WebAuthnManagerRegistrationTests: XCTestCase { try await assertThrowsError( await finishRegistration( attestationObject: TestAttestationObjectBuilder() - .validMock() + .validMockRSA() .zeroAuthData(byteCount: 36) .build() .cborEncoded @@ -161,10 +161,10 @@ final class WebAuthnManagerRegistrationTests: XCTestCase { try await assertThrowsError( await finishRegistration( attestationObject: TestAttestationObjectBuilder() - .validMock() + .validMockRSA() .authData( TestAuthDataBuilder() - .validMock() + .validMockRSA() .flags(0b01000001) .noAttestedCredentialData() .noExtensionData() @@ -180,10 +180,10 @@ final class WebAuthnManagerRegistrationTests: XCTestCase { try await assertThrowsError( await finishRegistration( attestationObject: TestAttestationObjectBuilder() - .validMock() + .validMockRSA() .authData( TestAuthDataBuilder() - .validMock() + .validMockRSA() .flags(0b00000001) .attestedCredData(credentialPublicKey: []) ) @@ -198,8 +198,8 @@ final class WebAuthnManagerRegistrationTests: XCTestCase { try await assertThrowsError( await finishRegistration( attestationObject: TestAttestationObjectBuilder() - .validMock() - .authData(TestAuthDataBuilder().validMock().noExtensionData().flags(0b11000001)) + .validMockRSA() + .authData(TestAuthDataBuilder().validMockRSA().noExtensionData().flags(0b11000001)) .build() .cborEncoded ), @@ -211,10 +211,10 @@ final class WebAuthnManagerRegistrationTests: XCTestCase { try await assertThrowsError( await finishRegistration( attestationObject: TestAttestationObjectBuilder() - .validMock() + .validMockRSA() .authData( TestAuthDataBuilder() - .validMock() + .validMockRSA() .attestedCredData( credentialIDLength: [0b00000000, 0b00000010], // we expect length = 2 credentialID: [255], // but only get length = 1 @@ -233,8 +233,8 @@ final class WebAuthnManagerRegistrationTests: XCTestCase { try await assertThrowsError( await finishRegistration( attestationObject: TestAttestationObjectBuilder() - .validMock() - .authData(TestAuthDataBuilder().validMock().relyingPartyIDHash(fromRelyingPartyID: "invalid-id.com")) + .validMockRSA() + .authData(TestAuthDataBuilder().validMockRSA().relyingPartyIDHash(fromRelyingPartyID: "invalid-id.com")) .build() .cborEncoded ), @@ -246,8 +246,8 @@ final class WebAuthnManagerRegistrationTests: XCTestCase { try await assertThrowsError( await finishRegistration( attestationObject: TestAttestationObjectBuilder() - .validMock() - .authData(TestAuthDataBuilder().validMock().flags(0b11000000)) + .validMockRSA() + .authData(TestAuthDataBuilder().validMockRSA().flags(0b11000000)) .build() .cborEncoded ), @@ -259,8 +259,8 @@ final class WebAuthnManagerRegistrationTests: XCTestCase { try await assertThrowsError( await finishRegistration( attestationObject: TestAttestationObjectBuilder() - .validMock() - .authData(TestAuthDataBuilder().validMock().flags(0b11000001)) + .validMockRSA() + .authData(TestAuthDataBuilder().validMockRSA().flags(0b11000001)) .build() .cborEncoded, requireUserVerification: true @@ -273,7 +273,7 @@ final class WebAuthnManagerRegistrationTests: XCTestCase { try await assertThrowsError( await finishRegistration( attestationObject: TestAttestationObjectBuilder() - .validMock() + .validMockRSA() .fmt("none") .attStmt(.double(123)) .build() @@ -295,14 +295,14 @@ final class WebAuthnManagerRegistrationTests: XCTestCase { /// This should succeed as it's on the border of being acceptable _ = try await finishRegistration( attestationObject: TestAttestationObjectBuilder() - .validMock() + .validMockRSA() .authData( TestAuthDataBuilder() - .validMock() + .validMockRSA() .attestedCredData( credentialIDLength: [0b000_00011, 0b1111_1111], credentialID: Array(repeating: 0, count: 1023), - credentialPublicKey: TestCredentialPublicKeyBuilder().validMock().buildAsByteArray() + credentialPublicKey: TestCredentialPublicKeyBuilder().validMockRSA().buildAsByteArray() ) ) .build() @@ -313,14 +313,14 @@ final class WebAuthnManagerRegistrationTests: XCTestCase { try await assertThrowsError( await finishRegistration( attestationObject: TestAttestationObjectBuilder() - .validMock() + .validMockRSA() .authData( TestAuthDataBuilder() - .validMock() + .validMockRSA() .attestedCredData( credentialIDLength: [0b000_00100, 0b0000_0000], credentialID: Array(repeating: 0, count: 1024), - credentialPublicKey: TestCredentialPublicKeyBuilder().validMock().buildAsByteArray() + credentialPublicKey: TestCredentialPublicKeyBuilder().validMockRSA().buildAsByteArray() ) ) .build() @@ -332,13 +332,13 @@ final class WebAuthnManagerRegistrationTests: XCTestCase { func testFinishRegistrationSucceeds() async throws { let credentialID: [UInt8] = [0, 1, 0, 1, 0, 1] - let credentialPublicKey: [UInt8] = TestCredentialPublicKeyBuilder().validMock().buildAsByteArray() + let credentialPublicKey: [UInt8] = TestCredentialPublicKeyBuilder().validMockRSA().buildAsByteArray() let authData = TestAuthDataBuilder() - .validMock() + .validMockRSA() .attestedCredData(credentialPublicKey: credentialPublicKey) .noExtensionData() let attestationObject = TestAttestationObjectBuilder() - .validMock() + .validMockRSA() .authData(authData) .build() .cborEncoded @@ -367,7 +367,7 @@ final class WebAuthnManagerRegistrationTests: XCTestCase { type: CredentialType = .publicKey, rawID: [UInt8] = "e0fac9350509f71748d83782ccaf6b4c1462c615c70e255da1344e40887c8fcd".hexadecimal!, clientDataJSON: [UInt8] = TestClientDataJSON().jsonBytes, - attestationObject: [UInt8] = TestAttestationObjectBuilder().validMock().build().cborEncoded, + attestationObject: [UInt8] = TestAttestationObjectBuilder().validMockRSA().build().cborEncoded, requireUserVerification: Bool = false, confirmCredentialIDNotRegisteredYet: (String) async throws -> Bool = { _ in true } ) async throws -> Credential {