diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 49911b4..2d9cae1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,10 +7,8 @@ on: jobs: unit-tests: name: Unit tests - uses: apple/swift-nio/.github/workflows/unit_tests.yml@main + uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main with: - linux_5_9_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error" - linux_5_10_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error" - linux_6_0_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" - linux_nightly_6_1_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" - linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" + exclude_swift_versions: "[{\"swift_version\": \"5.8\"}, {\"swift_version\": \"5.9\"}, {\"swift_version\": \"5.10\"}]" + swift_flags: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" + swift_nightly_flags: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 6531fb3..a09181e 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -12,20 +12,13 @@ jobs: license_header_check_project_name: "Swift WebAuthn" shell_check_enabled: false format_check_enabled: false - license_header_check_enabled: false docs_check_enabled: false - yamllint_check_enabled: false unit-tests: name: Unit tests - uses: apple/swift-nio/.github/workflows/unit_tests.yml@main + uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main with: - linux_5_9_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error" - linux_5_10_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error" - linux_6_0_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" - linux_nightly_6_1_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" - linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" - - cxx-interop: - name: Cxx interop - uses: apple/swift-nio/.github/workflows/cxx_interop.yml@main + linux_exclude_swift_versions: "[{\"swift_version\": \"5.8\"}, {\"swift_version\": \"5.9\"}, {\"swift_version\": \"5.10\"}]" + windows_exclude_swift_versions: "[{\"swift_version\": \"5.8\"}, {\"swift_version\": \"5.9\"}, {\"swift_version\": \"5.10\"}]" + swift_flags: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" + swift_nightly_flags: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" diff --git a/.license_header_template b/.license_header_template new file mode 100644 index 0000000..189471c --- /dev/null +++ b/.license_header_template @@ -0,0 +1,12 @@ +@@===----------------------------------------------------------------------===@@ +@@ +@@ This source file is part of the Swift WebAuthn open source project +@@ +@@ Copyright (c) YEARS the Swift WebAuthn project authors +@@ Licensed under Apache License v2.0 +@@ +@@ See LICENSE.txt for license information +@@ +@@ SPDX-License-Identifier: Apache-2.0 +@@ +@@===----------------------------------------------------------------------===@@ diff --git a/.spi.yml b/.spi.yml index 8c5ee82..2d69f2b 100644 --- a/.spi.yml +++ b/.spi.yml @@ -1,4 +1,4 @@ version: 1 builder: configs: - - documentation_targets: [WebAuthn] \ No newline at end of file + - documentation_targets: [WebAuthn] diff --git a/.swiftlint.yml b/.swiftlint.yml index b2ea2ec..0aac881 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -10,4 +10,4 @@ identifier_name: - rp line_length: - ignores_comments: true \ No newline at end of file + ignores_comments: true diff --git a/Package.swift b/Package.swift index 8c9ec3f..0f5d80b 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.9 +// swift-tools-version: 6.0 //===----------------------------------------------------------------------===// // // This source file is part of the Swift WebAuthn open source project @@ -36,15 +36,13 @@ let package = Package( .product(name: "Crypto", package: "swift-crypto"), .product(name: "_CryptoExtras", package: "swift-crypto"), .product(name: "Logging", package: "swift-log"), - ], - swiftSettings: [.enableExperimentalFeature("StrictConcurrency=complete")] + ] ), .testTarget( name: "WebAuthnTests", dependencies: [ .target(name: "WebAuthn") - ], - swiftSettings: [.enableExperimentalFeature("StrictConcurrency=complete")] + ] ) ] ) diff --git a/README.md b/README.md index f129785..e1f604f 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,13 @@ For an authentication ceremony use the following two methods: - `WebAuthnManager.beginAuthentication()` - `WebAuthnManager.finishAuthentication()` +## Contributing + +If you add any new files, please run the following command at the root of the repo to identify any missing license headers: +```bash +% PROJECTNAME="Swift WebAuthn" /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/swiftlang/github-workflows/refs/heads/main/.github/workflows/scripts/check-license-header.sh)" +``` + ## Credits Swift WebAuthn is heavily inspired by existing WebAuthn libraries like diff --git a/Sources/WebAuthn/Ceremonies/Authentication/AuthenticationCredential.swift b/Sources/WebAuthn/Ceremonies/Authentication/AuthenticationCredential.swift index 88dc884..6237cc6 100644 --- a/Sources/WebAuthn/Ceremonies/Authentication/AuthenticationCredential.swift +++ b/Sources/WebAuthn/Ceremonies/Authentication/AuthenticationCredential.swift @@ -35,7 +35,7 @@ public struct AuthenticationCredential: Sendable { } extension AuthenticationCredential: Decodable { - public init(from decoder: Decoder) throws { + public init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decode(URLEncodedBase64.self, forKey: .id) diff --git a/Sources/WebAuthn/Ceremonies/Authentication/AuthenticatorAssertionResponse.swift b/Sources/WebAuthn/Ceremonies/Authentication/AuthenticatorAssertionResponse.swift index 979ba08..cf9580b 100644 --- a/Sources/WebAuthn/Ceremonies/Authentication/AuthenticatorAssertionResponse.swift +++ b/Sources/WebAuthn/Ceremonies/Authentication/AuthenticatorAssertionResponse.swift @@ -49,7 +49,7 @@ public struct AuthenticatorAssertionResponse: Sendable { } extension AuthenticatorAssertionResponse: Decodable { - public init(from decoder: Decoder) throws { + public init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) clientDataJSON = try container.decodeBytesFromURLEncodedBase64(forKey: .clientDataJSON) diff --git a/Sources/WebAuthn/Ceremonies/Authentication/PublicKeyCredentialRequestOptions.swift b/Sources/WebAuthn/Ceremonies/Authentication/PublicKeyCredentialRequestOptions.swift index e142aa3..81340ed 100644 --- a/Sources/WebAuthn/Ceremonies/Authentication/PublicKeyCredentialRequestOptions.swift +++ b/Sources/WebAuthn/Ceremonies/Authentication/PublicKeyCredentialRequestOptions.swift @@ -45,7 +45,7 @@ public struct PublicKeyCredentialRequestOptions: Encodable, Sendable { // let extensions: [String: Any] - public func encode(to encoder: Encoder) throws { + public func encode(to encoder: any Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(challenge.base64URLEncodedString(), forKey: .challenge) @@ -107,7 +107,7 @@ public struct PublicKeyCredentialDescriptor: Equatable, Encodable, Sendable { self.transports = transports } - public func encode(to encoder: Encoder) throws { + public func encode(to encoder: any Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(type, forKey: .type) diff --git a/Sources/WebAuthn/Ceremonies/Registration/AuthenticatorAttestationResponse.swift b/Sources/WebAuthn/Ceremonies/Registration/AuthenticatorAttestationResponse.swift index 0d45439..52999cd 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/AuthenticatorAttestationResponse.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/AuthenticatorAttestationResponse.swift @@ -30,7 +30,7 @@ public struct AuthenticatorAttestationResponse: Sendable { } extension AuthenticatorAttestationResponse: Decodable { - public init(from decoder: Decoder) throws { + public init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) clientDataJSON = try container.decodeBytesFromURLEncodedBase64(forKey: .clientDataJSON) diff --git a/Sources/WebAuthn/Ceremonies/Registration/PublicKeyCredentialCreationOptions.swift b/Sources/WebAuthn/Ceremonies/Registration/PublicKeyCredentialCreationOptions.swift index a069c4a..9fdfabf 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/PublicKeyCredentialCreationOptions.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/PublicKeyCredentialCreationOptions.swift @@ -47,7 +47,7 @@ public struct PublicKeyCredentialCreationOptions: Encodable, Sendable { /// supported. public let attestation: AttestationConveyancePreference - public func encode(to encoder: Encoder) throws { + public func encode(to encoder: any Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(challenge.base64URLEncodedString(), forKey: .challenge) @@ -142,7 +142,7 @@ public struct PublicKeyCredentialUserEntity: Encodable, Sendable { self.displayName = displayName } - public func encode(to encoder: Encoder) throws { + public func encode(to encoder: any Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(id.base64URLEncodedString(), forKey: .id) diff --git a/Sources/WebAuthn/Ceremonies/Registration/RegistrationCredential.swift b/Sources/WebAuthn/Ceremonies/Registration/RegistrationCredential.swift index fe8a88f..2bc3f9f 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/RegistrationCredential.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/RegistrationCredential.swift @@ -32,7 +32,7 @@ public struct RegistrationCredential: Sendable { } extension RegistrationCredential: Decodable { - public init(from decoder: Decoder) throws { + public init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decode(URLEncodedBase64.self, forKey: .id) diff --git a/Sources/WebAuthn/Ceremonies/Shared/AuthenticatorAttestationGloballyUniqueID.swift b/Sources/WebAuthn/Ceremonies/Shared/AuthenticatorAttestationGloballyUniqueID.swift index a9bef10..e2b252a 100644 --- a/Sources/WebAuthn/Ceremonies/Shared/AuthenticatorAttestationGloballyUniqueID.swift +++ b/Sources/WebAuthn/Ceremonies/Shared/AuthenticatorAttestationGloballyUniqueID.swift @@ -64,12 +64,12 @@ public struct AuthenticatorAttestationGloballyUniqueID: Hashable, Sendable { public typealias AAGUID = AuthenticatorAttestationGloballyUniqueID extension AuthenticatorAttestationGloballyUniqueID: Codable { - public init(from decoder: Decoder) throws { + public init(from decoder: any Decoder) throws { let container = try decoder.singleValueContainer() id = try container.decode(UUID.self) } - public func encode(to encoder: Encoder) throws { + public func encode(to encoder: any Encoder) throws { var container = encoder.singleValueContainer() try container.encode(id) } diff --git a/Sources/WebAuthn/Ceremonies/Shared/AuthenticatorData.swift b/Sources/WebAuthn/Ceremonies/Shared/AuthenticatorData.swift index 103de56..e039ca6 100644 --- a/Sources/WebAuthn/Ceremonies/Shared/AuthenticatorData.swift +++ b/Sources/WebAuthn/Ceremonies/Shared/AuthenticatorData.swift @@ -27,10 +27,10 @@ struct AuthenticatorData: Equatable, Sendable { } extension AuthenticatorData { - init(bytes: [UInt8]) throws { + init(bytes: [UInt8]) throws(WebAuthnError) { let minAuthDataLength = 37 guard bytes.count >= minAuthDataLength else { - throw WebAuthnError.authDataTooShort + throw .authDataTooShort } let relyingPartyIDHash = Array(bytes[..<32]) @@ -44,7 +44,7 @@ extension AuthenticatorData { if flags.attestedCredentialData { let minAttestedAuthLength = 37 + AAGUID.size + 2 guard bytes.count > minAttestedAuthLength else { - throw WebAuthnError.attestedCredentialDataMissing + throw .attestedCredentialDataMissing } let (data, length) = try Self.parseAttestedData(bytes) attestedCredentialData = data @@ -52,21 +52,21 @@ extension AuthenticatorData { // For assertion signatures, the AT flag MUST NOT be set and the attestedCredentialData MUST NOT be included. } else { if !flags.extensionDataIncluded && bytes.count != minAuthDataLength { - throw WebAuthnError.attestedCredentialFlagNotSet + throw .attestedCredentialFlagNotSet } } var extensionData: [UInt8]? if flags.extensionDataIncluded { guard remainingCount != 0 else { - throw WebAuthnError.extensionDataMissing + throw .extensionDataMissing } extensionData = Array(bytes[(bytes.count - remainingCount)...]) remainingCount -= extensionData!.count } guard remainingCount == 0 else { - throw WebAuthnError.leftOverBytesInAuthenticatorData + throw .leftOverBytesInAuthenticatorData } self.relyingPartyIDHash = relyingPartyIDHash @@ -81,10 +81,10 @@ extension AuthenticatorData { /// /// This is assumed to take place after the first 37 bytes of `data`, which is always of fixed size. /// - SeeAlso: [WebAuthn Level 3 Editor's Draft §6.5.1. Attested Credential Data]( https://w3c.github.io/webauthn/#sctn-attested-credential-data) - private static func parseAttestedData(_ data: [UInt8]) throws -> (AttestedCredentialData, Int) { + private static func parseAttestedData(_ data: [UInt8]) throws(WebAuthnError) -> (AttestedCredentialData, Int) { /// **aaguid** (16): The AAGUID of the authenticator. guard let aaguid = AAGUID(bytes: data[37..<(37 + AAGUID.size)]) // Bytes [37-52] - else { throw WebAuthnError.attestedCredentialDataMissing } + else { throw .attestedCredentialDataMissing } /// **credentialIdLength** (2): Byte length L of credentialId, 16-bit unsigned big-endian integer. Value MUST be ≤ 1023. let idLengthBytes = data[53..<55] // Length is 2 bytes @@ -92,20 +92,22 @@ extension AuthenticatorData { let idLength = UInt16(bigEndianBytes: idLengthData) guard idLength <= 1023 - else { throw WebAuthnError.credentialIDTooLong } + else { throw .credentialIDTooLong } let credentialIDEndIndex = Int(idLength) + 55 guard data.count >= credentialIDEndIndex - else { throw WebAuthnError.credentialIDTooShort } + else { throw .credentialIDTooShort } /// **credentialId** (L): Credential ID let credentialID = data[55.. UInt8 { - if slice.count < 1 { throw CBORError.unfinishedSequence } + func popByte() throws(CBORError) -> UInt8 { + if slice.count < 1 { throw .unfinishedSequence } return slice.removeFirst() } - func popBytes(_ n: Int) throws -> ArraySlice { - if slice.count < n { throw CBORError.unfinishedSequence } + func popBytes(_ n: Int) throws(CBORError) -> ArraySlice { + if slice.count < n { throw .unfinishedSequence } let result = slice.prefix(n) slice = slice.dropFirst(n) return result diff --git a/Sources/WebAuthn/Ceremonies/Shared/CollectedClientData.swift b/Sources/WebAuthn/Ceremonies/Shared/CollectedClientData.swift index 2726cb2..88ca7f3 100644 --- a/Sources/WebAuthn/Ceremonies/Shared/CollectedClientData.swift +++ b/Sources/WebAuthn/Ceremonies/Shared/CollectedClientData.swift @@ -34,11 +34,11 @@ public struct CollectedClientData: Codable, Hashable, Sendable { public let challenge: URLEncodedBase64 public let origin: String - func verify(storedChallenge: [UInt8], ceremonyType: CeremonyType, relyingPartyOrigin: String) throws { - guard type == ceremonyType else { throw CollectedClientDataVerifyError.ceremonyTypeDoesNotMatch } + func verify(storedChallenge: [UInt8], ceremonyType: CeremonyType, relyingPartyOrigin: String) throws(CollectedClientDataVerifyError) { + guard type == ceremonyType else { throw .ceremonyTypeDoesNotMatch } guard challenge == storedChallenge.base64URLEncodedString() else { - throw CollectedClientDataVerifyError.challengeDoesNotMatch + throw .challengeDoesNotMatch } - guard origin == relyingPartyOrigin else { throw CollectedClientDataVerifyError.originDoesNotMatch } + guard origin == relyingPartyOrigin else { throw .originDoesNotMatch } } } diff --git a/Sources/WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift b/Sources/WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift index 307b0ce..dccc84d 100644 --- a/Sources/WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift +++ b/Sources/WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift @@ -108,7 +108,7 @@ struct EC2PublicKey: PublicKey, Sendable { self.yCoordinate = yCoordinate } - init(publicKeyObject: CBOR, algorithm: COSEAlgorithmIdentifier) throws { + init(publicKeyObject: CBOR, algorithm: COSEAlgorithmIdentifier) throws(WebAuthnError) { self.algorithm = algorithm // Curve is key -1 - or -0 for SwiftCBOR @@ -117,18 +117,18 @@ struct EC2PublicKey: PublicKey, Sendable { guard let curveRaw = publicKeyObject[COSEKey.crv.cbor], case let .unsignedInt(curve) = curveRaw, let coseCurve = COSECurve(rawValue: curve) else { - throw WebAuthnError.invalidCurve + throw .invalidCurve } self.curve = coseCurve guard let xCoordRaw = publicKeyObject[COSEKey.x.cbor], case let .byteString(xCoordinateBytes) = xCoordRaw else { - throw WebAuthnError.invalidXCoordinate + throw .invalidXCoordinate } xCoordinate = xCoordinateBytes guard let yCoordRaw = publicKeyObject[COSEKey.y.cbor], case let .byteString(yCoordinateBytes) = yCoordRaw else { - throw WebAuthnError.invalidYCoordinate + throw .invalidYCoordinate } yCoordinate = yCoordinateBytes } @@ -167,18 +167,18 @@ struct RSAPublicKeyData: PublicKey, Sendable { var rawRepresentation: [UInt8] { n + e } - init(publicKeyObject: CBOR, algorithm: COSEAlgorithmIdentifier) throws { + init(publicKeyObject: CBOR, algorithm: COSEAlgorithmIdentifier) throws(WebAuthnError) { self.algorithm = algorithm guard let nRaw = publicKeyObject[COSEKey.n.cbor], case let .byteString(nBytes) = nRaw else { - throw WebAuthnError.invalidModulus + throw .invalidModulus } n = nBytes guard let eRaw = publicKeyObject[COSEKey.e.cbor], case let .byteString(eBytes) = eRaw else { - throw WebAuthnError.invalidExponent + throw .invalidExponent } e = eBytes } @@ -213,17 +213,17 @@ struct OKPPublicKey: PublicKey, Sendable { let curve: UInt64 let xCoordinate: [UInt8] - init(publicKeyObject: CBOR, algorithm: COSEAlgorithmIdentifier) throws { + init(publicKeyObject: CBOR, algorithm: COSEAlgorithmIdentifier) throws(WebAuthnError) { self.algorithm = algorithm // Curve is key -1, or NegativeInt 0 for SwiftCBOR guard let curveRaw = publicKeyObject[.negativeInt(0)], case let .unsignedInt(curve) = curveRaw else { - throw WebAuthnError.invalidCurve + throw .invalidCurve } self.curve = curve // X Coordinate is key -2, or NegativeInt 1 for SwiftCBOR guard let xCoordRaw = publicKeyObject[.negativeInt(1)], case let .byteString(xCoordinateBytes) = xCoordRaw else { - throw WebAuthnError.invalidXCoordinate + throw .invalidXCoordinate } xCoordinate = xCoordinateBytes } diff --git a/Sources/WebAuthn/Helpers/Base64Utilities.swift b/Sources/WebAuthn/Helpers/Base64Utilities.swift index 126af5c..7d3d882 100644 --- a/Sources/WebAuthn/Helpers/Base64Utilities.swift +++ b/Sources/WebAuthn/Helpers/Base64Utilities.swift @@ -26,12 +26,12 @@ public struct EncodedBase64: ExpressibleByStringLiteral, Codable, Hashable, Equa self.init(value) } - public init(from decoder: Decoder) throws { + public init(from decoder: any Decoder) throws { let container = try decoder.singleValueContainer() self.base64 = try container.decode(String.self) } - public func encode(to encoder: Encoder) throws { + public func encode(to encoder: any Encoder) throws { var container = encoder.singleValueContainer() try container.encode(self.base64) } diff --git a/Sources/WebAuthn/WebAuthnError.swift b/Sources/WebAuthn/WebAuthnError.swift index f9b942a..f4dce7b 100644 --- a/Sources/WebAuthn/WebAuthnError.swift +++ b/Sources/WebAuthn/WebAuthnError.swift @@ -51,6 +51,7 @@ public struct WebAuthnError: Error, Hashable, Sendable { case leftOverBytesInAuthenticatorData case credentialIDTooLong case credentialIDTooShort + case invalidPublicKeyLength // MARK: CredentialPublicKey case badPublicKeyBytes @@ -110,6 +111,7 @@ public struct WebAuthnError: Error, Hashable, Sendable { public static let leftOverBytesInAuthenticatorData = Self(reason: .leftOverBytesInAuthenticatorData) public static let credentialIDTooLong = Self(reason: .credentialIDTooLong) public static let credentialIDTooShort = Self(reason: .credentialIDTooShort) + public static let invalidPublicKeyLength = Self(reason: .invalidPublicKeyLength) // MARK: CredentialPublicKey public static let badPublicKeyBytes = Self(reason: .badPublicKeyBytes) diff --git a/Sources/WebAuthn/WebAuthnManager.swift b/Sources/WebAuthn/WebAuthnManager.swift index 1da063b..299089b 100644 --- a/Sources/WebAuthn/WebAuthnManager.swift +++ b/Sources/WebAuthn/WebAuthnManager.swift @@ -139,7 +139,7 @@ public struct WebAuthnManager: Sendable { timeout: Duration? = .seconds(60), allowCredentials: [PublicKeyCredentialDescriptor]? = nil, userVerification: UserVerificationRequirement = .preferred - ) throws -> PublicKeyCredentialRequestOptions { + ) -> PublicKeyCredentialRequestOptions { let challenge = challengeGenerator.generate() return PublicKeyCredentialRequestOptions( diff --git a/Tests/WebAuthnTests/AuthenticatorAttestationGloballyUniqueIDTests.swift b/Tests/WebAuthnTests/AuthenticatorAttestationGloballyUniqueIDTests.swift index 7c2ccf2..e5d744b 100644 --- a/Tests/WebAuthnTests/AuthenticatorAttestationGloballyUniqueIDTests.swift +++ b/Tests/WebAuthnTests/AuthenticatorAttestationGloballyUniqueIDTests.swift @@ -11,22 +11,25 @@ // //===----------------------------------------------------------------------===// -import XCTest +import Foundation +import Testing @testable import WebAuthn -final class AuthenticatorAttestationGloballyUniqueIDTests: XCTestCase { - func testByteCoding() throws { +struct AuthenticatorAttestationGloballyUniqueIDTests { + @Test + func byteCoding() { let aaguid = AuthenticatorAttestationGloballyUniqueID(bytes: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]) - XCTAssertNotNil(aaguid) - XCTAssertEqual(aaguid?.bytes, [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f]) - XCTAssertEqual(aaguid?.id, UUID(uuid: (0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f))) - XCTAssertEqual(aaguid, AuthenticatorAttestationGloballyUniqueID(uuid: UUID(uuid: (0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f)))) - XCTAssertEqual(aaguid, AuthenticatorAttestationGloballyUniqueID(uuidString: "00010203-0405-0607-0809-0A0B0C0D0E0F" )) + #expect(aaguid != nil) + #expect(aaguid?.bytes == [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f]) + #expect(aaguid?.id == UUID(uuid: (0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f))) + #expect(aaguid == AuthenticatorAttestationGloballyUniqueID(uuid: UUID(uuid: (0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f)))) + #expect(aaguid == AuthenticatorAttestationGloballyUniqueID(uuidString: "00010203-0405-0607-0809-0A0B0C0D0E0F" )) } - func testInvalidByteDecoding() throws { - XCTAssertNil(AuthenticatorAttestationGloballyUniqueID(bytes: [])) - XCTAssertNil(AuthenticatorAttestationGloballyUniqueID(bytes: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14])) - XCTAssertNil(AuthenticatorAttestationGloballyUniqueID(bytes: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16])) + @Test + func invalidByteDecoding() { + #expect(AuthenticatorAttestationGloballyUniqueID(bytes: []) == nil) + #expect(AuthenticatorAttestationGloballyUniqueID(bytes: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]) == nil) + #expect(AuthenticatorAttestationGloballyUniqueID(bytes: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]) == nil) } } diff --git a/Tests/WebAuthnTests/DurationTests.swift b/Tests/WebAuthnTests/DurationTests.swift index 5c576f6..b2cb242 100644 --- a/Tests/WebAuthnTests/DurationTests.swift +++ b/Tests/WebAuthnTests/DurationTests.swift @@ -11,14 +11,15 @@ // //===----------------------------------------------------------------------===// -import XCTest +import Testing @testable import WebAuthn -final class DurationTests: XCTestCase { - func testMilliseconds() throws { - XCTAssertEqual(Duration.milliseconds(1234).milliseconds, 1234) - XCTAssertEqual(Duration.milliseconds(-1234).milliseconds, -1234) - XCTAssertEqual(Duration.microseconds(12345).milliseconds, 12) - XCTAssertEqual(Duration.microseconds(-12345).milliseconds, -12) +struct DurationTests { + @Test + func milliseconds() { + #expect(Duration.milliseconds(1234).milliseconds == 1234) + #expect(Duration.milliseconds(-1234).milliseconds == -1234) + #expect(Duration.microseconds(12345).milliseconds == 12) + #expect(Duration.microseconds(-12345).milliseconds == -12) } } diff --git a/Tests/WebAuthnTests/HelpersTests.swift b/Tests/WebAuthnTests/HelpersTests.swift index 35ddaa4..03b43a3 100644 --- a/Tests/WebAuthnTests/HelpersTests.swift +++ b/Tests/WebAuthnTests/HelpersTests.swift @@ -11,12 +11,13 @@ // //===----------------------------------------------------------------------===// -import XCTest - +import Foundation +import Testing @testable import WebAuthn -final class HelpersTests: XCTestCase { - func testBase64URLEncodeReturnsCorrectString() { +struct HelpersTests { + @Test + func base64URLEncodeReturnsCorrectString() { let input: [UInt8] = [1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0] let expectedBase64 = "AQABAAEBAAEAAQEAAAABAA==" let expectedBase64URL = "AQABAAEBAAEAAQEAAAABAA" @@ -24,14 +25,15 @@ final class HelpersTests: XCTestCase { let base64Encoded = input.base64EncodedString() let base64URLEncoded = input.base64URLEncodedString() - XCTAssertEqual(expectedBase64, base64Encoded.asString()) - XCTAssertEqual(expectedBase64URL, base64URLEncoded.asString()) + #expect(expectedBase64 == base64Encoded.asString()) + #expect(expectedBase64URL == base64URLEncoded.asString()) } - func testEncodeBase64Codable() throws { + @Test + func encodeBase64Codable() throws { let base64 = EncodedBase64("AQABAAEBAAEAAQEAAAABAA==") let json = try JSONEncoder().encode(base64) let decodedBase64 = try JSONDecoder().decode(EncodedBase64.self, from: json) - XCTAssertEqual(base64, decodedBase64) + #expect(base64 == decodedBase64) } } diff --git a/Tests/WebAuthnTests/Utils/assert+async.swift b/Tests/WebAuthnTests/Utils/assert+async.swift deleted file mode 100644 index e9cd079..0000000 --- a/Tests/WebAuthnTests/Utils/assert+async.swift +++ /dev/null @@ -1,50 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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 -// -//===----------------------------------------------------------------------===// - -import XCTest -import WebAuthn - -func assertThrowsError( - _ expression: @autoclosure () async throws -> T, - _ message: @autoclosure () -> String = "", - file: StaticString = #filePath, - line: UInt = #line, - _ errorHandler: (_ error: E) -> Void = { _ in } -) async { - do { - _ = try await expression() - XCTFail(message(), file: file, line: line) - } catch { - guard let error = error as? E else { - XCTFail(""" - Error was thrown, but didn't match expected type '\(E.self)'. - Got error of type '\(type(of: error))'. - Error: \(error) - """) - return - } - errorHandler(error) - } -} - -func assertThrowsError( - _ expression: @autoclosure () async throws -> T, - _ message: @autoclosure () -> String = "", - file: StaticString = #filePath, - line: UInt = #line, - expect: E -) async { - try await assertThrowsError(await expression(), message(), file: file, line: line) { error in - XCTAssertEqual(error, expect, message(), file: file, line: line) - } -} diff --git a/Tests/WebAuthnTests/Utils/assert+expect.swift b/Tests/WebAuthnTests/Utils/assert+expect.swift deleted file mode 100644 index 67447b8..0000000 --- a/Tests/WebAuthnTests/Utils/assert+expect.swift +++ /dev/null @@ -1,50 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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 -// -//===----------------------------------------------------------------------===// - -import XCTest -import WebAuthn - -func assertThrowsError( - _ expression: @autoclosure () throws -> T, - _ message: @autoclosure () -> String = "", - file: StaticString = #filePath, - line: UInt = #line, - _ errorHandler: (_ error: E) -> Void = { _ in } -) { - do { - _ = try expression() - XCTFail(message(), file: file, line: line) - } catch { - guard let error = error as? E else { - XCTFail(""" - Error was thrown, but didn't match expected type '\(E.self)'. - Got error of type '\(type(of: error))'. - Error: \(error) - """) - return - } - errorHandler(error) - } -} - -func assertThrowsError( - _ expression: @autoclosure () throws -> T, - _ message: @autoclosure () -> String = "", - file: StaticString = #filePath, - line: UInt = #line, - expect: E -) { - try assertThrowsError(expression(), message(), file: file, line: line) { error in - XCTAssertEqual(error, expect, message(), file: file, line: line) - } -} diff --git a/Tests/WebAuthnTests/WebAuthnManagerAuthenticationTests.swift b/Tests/WebAuthnTests/WebAuthnManagerAuthenticationTests.swift index 52f3752..1dfd74f 100644 --- a/Tests/WebAuthnTests/WebAuthnManagerAuthenticationTests.swift +++ b/Tests/WebAuthnTests/WebAuthnManagerAuthenticationTests.swift @@ -12,11 +12,12 @@ //===----------------------------------------------------------------------===// @testable import WebAuthn -import XCTest +import Testing +import Foundation import SwiftCBOR import Crypto -final class WebAuthnManagerAuthenticationTests: XCTestCase { +struct WebAuthnManagerAuthenticationTests { var webAuthnManager: WebAuthnManager! let challenge: [UInt8] = [1, 0, 1] @@ -24,7 +25,7 @@ final class WebAuthnManagerAuthenticationTests: XCTestCase { let relyingPartyName = "Testy test" let relyingPartyOrigin = "https://example.com" - override func setUp() { + init() { let configuration = WebAuthnManager.Configuration( relyingPartyID: relyingPartyID, relyingPartyName: relyingPartyName, @@ -33,98 +34,101 @@ final class WebAuthnManagerAuthenticationTests: XCTestCase { webAuthnManager = .init(configuration: configuration, challengeGenerator: .mock(generate: challenge)) } - func testBeginAuthentication() async throws { + @Test + func beginAuthentication() async throws { let allowCredentials: [PublicKeyCredentialDescriptor] = [.init(type: .publicKey, id: [1, 0, 2, 30])] - let options = try webAuthnManager.beginAuthentication( + let options = 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) + #expect(options.challenge == challenge) + #expect(options.timeout == .seconds(1234)) + #expect(options.relyingPartyID == relyingPartyID) + #expect(options.allowCredentials == allowCredentials) + #expect(options.userVerification == .preferred) } - func testFinishAuthenticationFailsIfCredentialTypeIsInvalid() throws { - try assertThrowsError( - finishAuthentication(type: "invalid"), - expect: WebAuthnError.invalidAssertionCredentialType - ) + @Test + func finishAuthenticationFailsIfCredentialTypeIsInvalid() throws { + #expect(throws: WebAuthnError.invalidAssertionCredentialType) { + try finishAuthentication(type: "invalid") + } } - func testFinishAuthenticationFailsIfClientDataJSONDecodingFails() throws { - try assertThrowsError(finishAuthentication(clientDataJSON: [0])) { (_: DecodingError) in - return + @Test + func finishAuthenticationFailsIfClientDataJSONDecodingFails() throws { + #expect(throws: DecodingError.self) { + try finishAuthentication(clientDataJSON: [0]) } } - - func testFinishAuthenticationFailsIfCeremonyTypeDoesNotMatch() throws { + + @Test + func finishAuthenticationFailsIfCeremonyTypeDoesNotMatch() throws { var clientDataJSON = TestClientDataJSON() clientDataJSON.type = "webauthn.create" - try assertThrowsError( - finishAuthentication(clientDataJSON: clientDataJSON.jsonBytes), - expect: CollectedClientData.CollectedClientDataVerifyError.ceremonyTypeDoesNotMatch - ) + #expect(throws: CollectedClientData.CollectedClientDataVerifyError.ceremonyTypeDoesNotMatch) { + try finishAuthentication(clientDataJSON: clientDataJSON.jsonBytes) + } } - func testFinishAuthenticationFailsIfRelyingPartyIDHashDoesNotMatch() throws { - try assertThrowsError( - finishAuthentication( + @Test + func finishAuthenticationFailsIfRelyingPartyIDHashDoesNotMatch() throws { + #expect(throws: WebAuthnError.relyingPartyIDHashDoesNotMatch) { + try finishAuthentication( authenticatorData: TestAuthDataBuilder() .validAuthenticationMock() .relyingPartyIDHash(fromRelyingPartyID: "wrong-id.org") .build() .byteArrayRepresentation - ), - expect: WebAuthnError.relyingPartyIDHashDoesNotMatch - ) + ) + } } - func testFinishAuthenticationFailsIfUserPresentFlagIsNotSet() throws { - try assertThrowsError( - finishAuthentication( + @Test + func finishAuthenticationFailsIfUserPresentFlagIsNotSet() throws { + #expect(throws: WebAuthnError.userPresentFlagNotSet) { + try finishAuthentication( authenticatorData: TestAuthDataBuilder() .validAuthenticationMock() .flags(0b10000000) .build() .byteArrayRepresentation - ), - expect: WebAuthnError.userPresentFlagNotSet - ) + ) + } } - func testFinishAuthenticationFailsIfUserIsNotVerified() throws { - try assertThrowsError( - finishAuthentication( + @Test + func finishAuthenticationFailsIfUserIsNotVerified() throws { + #expect(throws: WebAuthnError.userVerifiedFlagNotSet) { + try finishAuthentication( authenticatorData: TestAuthDataBuilder() .validAuthenticationMock() .flags(0b10000001) .build() .byteArrayRepresentation, requireUserVerification: true - ), - expect: WebAuthnError.userVerifiedFlagNotSet - ) + ) + } } - func testFinishAuthenticationFailsIfCredentialCounterIsNotUpToDate() throws { - try assertThrowsError( - finishAuthentication( + @Test + func finishAuthenticationFailsIfCredentialCounterIsNotUpToDate() throws { + #expect(throws: WebAuthnError.potentialReplayAttack) { + try finishAuthentication( authenticatorData: TestAuthDataBuilder() .validAuthenticationMock() .counter([0, 0, 0, 1]) // signCount = 1 .build() .byteArrayRepresentation, credentialCurrentSignCount: 2 - ), - expect: WebAuthnError.potentialReplayAttack - ) + ) + } } - func testFinishAuthenticationSucceeds() throws { + @Test + func finishAuthenticationSucceeds() throws { let credentialID = TestConstants.mockCredentialID let oldSignCount: UInt32 = 0 @@ -152,8 +156,8 @@ final class WebAuthnManagerAuthenticationTests: XCTestCase { credentialCurrentSignCount: oldSignCount ) - XCTAssertEqual(verifiedAuthentication.credentialID, credentialID.base64URLEncodedString()) - XCTAssertEqual(verifiedAuthentication.newSignCount, oldSignCount + 1) + #expect(verifiedAuthentication.credentialID == credentialID.base64URLEncodedString()) + #expect(verifiedAuthentication.newSignCount == oldSignCount + 1) } /// Using the default parameters `finishAuthentication` should succeed. diff --git a/Tests/WebAuthnTests/WebAuthnManagerIntegrationTests.swift b/Tests/WebAuthnTests/WebAuthnManagerIntegrationTests.swift index 0102045..0c76582 100644 --- a/Tests/WebAuthnTests/WebAuthnManagerIntegrationTests.swift +++ b/Tests/WebAuthnTests/WebAuthnManagerIntegrationTests.swift @@ -12,12 +12,14 @@ //===----------------------------------------------------------------------===// @testable import WebAuthn -import XCTest +import Foundation +import Testing import Crypto -final class WebAuthnManagerIntegrationTests: XCTestCase { +struct WebAuthnManagerIntegrationTests { // swiftlint:disable:next function_body_length - func testRegistrationAndAuthenticationSucceeds() async throws { + @Test + func registrationAndAuthenticationSucceeds() async throws { let configuration = WebAuthnManager.Configuration( relyingPartyID: "example.com", relyingPartyName: "Example RP", @@ -41,15 +43,15 @@ final class WebAuthnManagerIntegrationTests: XCTestCase { 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) + #expect(registrationOptions.challenge == mockChallenge) + #expect(registrationOptions.user.id == mockUser.id) + #expect(registrationOptions.user.name == mockUser.name) + #expect(registrationOptions.user.displayName == mockUser.displayName) + #expect(registrationOptions.attestation == attestationPreference) + #expect(registrationOptions.relyingParty.id == configuration.relyingPartyID) + #expect(registrationOptions.relyingParty.name == configuration.relyingPartyName) + #expect(registrationOptions.timeout == timeout) + #expect(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 @@ -82,14 +84,14 @@ final class WebAuthnManagerIntegrationTests: XCTestCase { confirmCredentialIDNotRegisteredYet: { _ in true } ) - XCTAssertEqual(credential.id, mockCredentialID.base64EncodedString().asString()) - XCTAssertEqual(credential.attestationClientDataJSON.type, .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.publicKey, mockCredentialPublicKey) + #expect(credential.id == mockCredentialID.base64EncodedString().asString()) + #expect(credential.attestationClientDataJSON.type == .create) + #expect(credential.attestationClientDataJSON.origin == mockClientDataJSON.origin) + #expect(credential.attestationClientDataJSON.challenge == mockChallenge.base64URLEncodedString()) + #expect(credential.isBackedUp == false) + #expect(credential.signCount == 0) + #expect(credential.type == .publicKey) + #expect(credential.publicKey == mockCredentialPublicKey) // Step 3.: Begin Authentication let authenticationTimeout: Duration = .seconds(4567) @@ -99,17 +101,17 @@ final class WebAuthnManagerIntegrationTests: XCTestCase { id: [UInt8](URLEncodedBase64(credential.id).urlDecoded.decoded!) )] - let authenticationOptions = try webAuthnManager.beginAuthentication( + let authenticationOptions = 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) + #expect(authenticationOptions.relyingPartyID == configuration.relyingPartyID) + #expect(authenticationOptions.timeout == authenticationTimeout) + #expect(authenticationOptions.challenge == mockChallenge) + #expect(authenticationOptions.userVerification == userVerification) + #expect(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 @@ -156,10 +158,10 @@ final class WebAuthnManagerIntegrationTests: XCTestCase { requireUserVerification: false ) - XCTAssertEqual(successfullAuthentication.newSignCount, 1) - XCTAssertEqual(successfullAuthentication.credentialBackedUp, false) - XCTAssertEqual(successfullAuthentication.credentialDeviceType, .singleDevice) - XCTAssertEqual(successfullAuthentication.credentialID, mockCredentialID.base64URLEncodedString()) + #expect(successfullAuthentication.newSignCount == 1) + #expect(successfullAuthentication.credentialBackedUp == false) + #expect(successfullAuthentication.credentialDeviceType == .singleDevice) + #expect(successfullAuthentication.credentialID == mockCredentialID.base64URLEncodedString()) // We did it! } diff --git a/Tests/WebAuthnTests/WebAuthnManagerRegistrationTests.swift b/Tests/WebAuthnTests/WebAuthnManagerRegistrationTests.swift index 0654d5f..51cdb5c 100644 --- a/Tests/WebAuthnTests/WebAuthnManagerRegistrationTests.swift +++ b/Tests/WebAuthnTests/WebAuthnManagerRegistrationTests.swift @@ -12,11 +12,11 @@ //===----------------------------------------------------------------------===// @testable import WebAuthn -import XCTest +import Testing import SwiftCBOR // swiftlint:disable:next type_body_length -final class WebAuthnManagerRegistrationTests: XCTestCase { +struct WebAuthnManagerRegistrationTests { var webAuthnManager: WebAuthnManager! let challenge: [UInt8] = [1, 0, 1] @@ -24,7 +24,7 @@ final class WebAuthnManagerRegistrationTests: XCTestCase { let relyingPartyID = "example.com" let relyingPartyOrigin = "https://example.com" - override func setUp() { + init() { let configuration = WebAuthnManager.Configuration( relyingPartyID: relyingPartyID, relyingPartyName: relyingPartyDisplayName, @@ -34,8 +34,8 @@ final class WebAuthnManagerRegistrationTests: XCTestCase { } // MARK: - beginRegistration() - - func testBeginRegistrationReturns() throws { + @Test + func beginRegistrationReturns() { let user = PublicKeyCredentialUserEntity.mock let publicKeyCredentialParameter = PublicKeyCredentialParameters(type: .publicKey, alg: .algES256) let options = webAuthnManager.beginRegistration( @@ -43,123 +43,125 @@ final class WebAuthnManagerRegistrationTests: XCTestCase { 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]) + #expect(options.challenge == challenge) + #expect(options.relyingParty.id == relyingPartyID) + #expect(options.relyingParty.name == relyingPartyDisplayName) + #expect(options.user.id == user.id) + #expect(options.user.displayName == user.displayName) + #expect(options.user.name == user.name) + #expect(options.publicKeyCredentialParameters == [publicKeyCredentialParameter]) } // MARK: - finishRegistration() - - func testFinishRegistrationFailsIfCeremonyTypeDoesNotMatch() async throws { + + @Test + func finishRegistrationFailsIfCeremonyTypeDoesNotMatch() async throws { var clientDataJSON = TestClientDataJSON() clientDataJSON.type = "webauthn.get" - try await assertThrowsError( - await finishRegistration(clientDataJSON: clientDataJSON.jsonBytes), - expect: CollectedClientData.CollectedClientDataVerifyError.ceremonyTypeDoesNotMatch - ) + await #expect(throws: CollectedClientData.CollectedClientDataVerifyError.ceremonyTypeDoesNotMatch) { + try await finishRegistration(clientDataJSON: clientDataJSON.jsonBytes) + } } - - func testFinishRegistrationFailsIfChallengeDoesNotMatch() async throws { + + @Test + func finishRegistrationFailsIfChallengeDoesNotMatch() async throws { var clientDataJSON = TestClientDataJSON() clientDataJSON.challenge = [0, 2, 4].base64URLEncodedString() - try await assertThrowsError( - await finishRegistration( + await #expect(throws: CollectedClientData.CollectedClientDataVerifyError.challengeDoesNotMatch) { + try await finishRegistration( challenge: [UInt8]("definitely another challenge".utf8), clientDataJSON: clientDataJSON.jsonBytes - ), - expect: CollectedClientData.CollectedClientDataVerifyError.challengeDoesNotMatch - ) + ) + } } - - func testFinishRegistrationFailsIfOriginDoesNotMatch() async throws { + + @Test + func finishRegistrationFailsIfOriginDoesNotMatch() 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 - ) + await #expect(throws: CollectedClientData.CollectedClientDataVerifyError.originDoesNotMatch) { + try await finishRegistration(clientDataJSON: clientDataJSON.jsonBytes) + } } - - func testFinishRegistrationFailsWithInvalidCredentialCreationType() async throws { - try await assertThrowsError( - await finishRegistration(type: "foo"), - expect: WebAuthnError.invalidCredentialCreationType - ) + + @Test + func finishRegistrationFailsWithInvalidCredentialCreationType() async throws { + await #expect(throws: WebAuthnError.invalidCredentialCreationType) { + try await finishRegistration(type: "foo") + } } - func testFinishRegistrationFailsIfClientDataJSONDecodingFails() async throws { - try await assertThrowsError(await finishRegistration(clientDataJSON: [0])) { (_: DecodingError) in - return + @Test + func finishRegistrationFailsIfClientDataJSONDecodingFails() async throws { + await #expect(throws: DecodingError.self) { + try await finishRegistration(clientDataJSON: [0]) } } - - func testFinishRegistrationFailsIfAttestationObjectIsNotBase64() async throws { - try await assertThrowsError( - await finishRegistration(attestationObject: []), - expect: WebAuthnError.invalidAttestationObject - ) + + @Test + func finishRegistrationFailsIfAttestationObjectIsNotBase64() async throws { + await #expect(throws: WebAuthnError.invalidAttestationObject) { + try await finishRegistration(attestationObject: []) + } } - func testFinishRegistrationFailsIfAuthDataIsInvalid() async throws { - try await assertThrowsError( - await finishRegistration( + @Test + func finishRegistrationFailsIfAuthDataIsInvalid() async throws { + await #expect(throws: WebAuthnError.invalidAuthData) { + try await finishRegistration( attestationObject: TestAttestationObjectBuilder() .validMock() .invalidAuthData() .build() .cborEncoded - ), - expect: WebAuthnError.invalidAuthData - ) + ) + } } - func testFinishRegistrationFailsIfFmtIsInvalid() async throws { - try await assertThrowsError( - await finishRegistration( + @Test + func finishRegistrationFailsIfFmtIsInvalid() async throws { + await #expect(throws: WebAuthnError.invalidFmt) { + try await finishRegistration( attestationObject: TestAttestationObjectBuilder() .validMock() .invalidFmt() .build() .cborEncoded - ), - expect: WebAuthnError.invalidFmt - ) + ) + } } - func testFinishRegistrationFailsIfAttStmtIsMissing() async throws { - try await assertThrowsError( - await finishRegistration( + @Test + func finishRegistrationFailsIfAttStmtIsMissing() async throws { + await #expect(throws: WebAuthnError.missingAttStmt) { + try await finishRegistration( attestationObject: TestAttestationObjectBuilder() .validMock() .missingAttStmt() .build() .cborEncoded - ), - expect: WebAuthnError.missingAttStmt - ) + ) + } } - func testFinishRegistrationFailsIfAuthDataIsTooShort() async throws { - try await assertThrowsError( - await finishRegistration( + @Test + func finishRegistrationFailsIfAuthDataIsTooShort() async throws { + await #expect(throws: WebAuthnError.authDataTooShort) { + try await finishRegistration( attestationObject: TestAttestationObjectBuilder() .validMock() .zeroAuthData(byteCount: 36) .build() .cborEncoded - ), - expect: WebAuthnError.authDataTooShort - ) + ) + } } - func testFinishRegistrationFailsIfAttestedCredentialDataFlagIsSetButThereIsNoCredentialData() async throws { - try await assertThrowsError( - await finishRegistration( + @Test + func finishRegistrationFailsIfAttestedCredentialDataFlagIsSetButThereIsNoCredentialData() async throws { + await #expect(throws: WebAuthnError.attestedCredentialDataMissing) { + try await finishRegistration( attestationObject: TestAttestationObjectBuilder() .validMock() .authData( @@ -171,14 +173,14 @@ final class WebAuthnManagerRegistrationTests: XCTestCase { ) .build() .cborEncoded - ), - expect: WebAuthnError.attestedCredentialDataMissing - ) + ) + } } - func testFinishRegistrationFailsIfAttestedCredentialDataFlagIsNotSetButThereIsCredentialData() async throws { - try await assertThrowsError( - await finishRegistration( + @Test + func finishRegistrationFailsIfAttestedCredentialDataFlagIsNotSetButThereIsCredentialData() async throws { + await #expect(throws: WebAuthnError.attestedCredentialFlagNotSet) { + try await finishRegistration( attestationObject: TestAttestationObjectBuilder() .validMock() .authData( @@ -189,27 +191,27 @@ final class WebAuthnManagerRegistrationTests: XCTestCase { ) .build() .cborEncoded - ), - expect: WebAuthnError.attestedCredentialFlagNotSet - ) + ) + } } - func testFinishRegistrationFailsIfExtensionDataFlagIsSetButThereIsNoExtensionData() async throws { - try await assertThrowsError( - await finishRegistration( + @Test + func finishRegistrationFailsIfExtensionDataFlagIsSetButThereIsNoExtensionData() async throws { + await #expect(throws: WebAuthnError.extensionDataMissing) { + try await finishRegistration( attestationObject: TestAttestationObjectBuilder() .validMock() .authData(TestAuthDataBuilder().validMock().noExtensionData().flags(0b11000001)) .build() .cborEncoded - ), - expect: WebAuthnError.extensionDataMissing - ) + ) + } } - func testFinishRegistrationFailsIfCredentialIdIsTooShort() async throws { - try await assertThrowsError( - await finishRegistration( + @Test + func finishRegistrationFailsIfCredentialIdIsTooShort() async throws { + await #expect(throws: WebAuthnError.credentialIDTooShort) { + try await finishRegistration( attestationObject: TestAttestationObjectBuilder() .validMock() .authData( @@ -224,54 +226,54 @@ final class WebAuthnManagerRegistrationTests: XCTestCase { ) .build() .cborEncoded - ), - expect: WebAuthnError.credentialIDTooShort - ) + ) + } } - func testFinishRegistrationFailsIfRelyingPartyIDHashDoesNotMatch() async throws { - try await assertThrowsError( - await finishRegistration( + @Test + func finishRegistrationFailsIfRelyingPartyIDHashDoesNotMatch() async throws { + await #expect(throws: WebAuthnError.relyingPartyIDHashDoesNotMatch) { + try await finishRegistration( attestationObject: TestAttestationObjectBuilder() .validMock() .authData(TestAuthDataBuilder().validMock().relyingPartyIDHash(fromRelyingPartyID: "invalid-id.com")) .build() .cborEncoded - ), - expect: WebAuthnError.relyingPartyIDHashDoesNotMatch - ) + ) + } } - func testFinishRegistrationFailsIfUserPresentFlagIsNotSet() async throws { - try await assertThrowsError( - await finishRegistration( + @Test + func finishRegistrationFailsIfUserPresentFlagIsNotSet() async throws { + await #expect(throws: WebAuthnError.userPresentFlagNotSet) { + try await finishRegistration( attestationObject: TestAttestationObjectBuilder() .validMock() .authData(TestAuthDataBuilder().validMock().flags(0b11000000)) .build() .cborEncoded - ), - expect: WebAuthnError.userPresentFlagNotSet - ) + ) + } } - func testFinishRegistrationFailsIfUserVerificationFlagIsNotSetButRequired() async throws { - try await assertThrowsError( - await finishRegistration( + @Test + func finishRegistrationFailsIfUserVerificationFlagIsNotSetButRequired() async throws { + await #expect(throws: WebAuthnError.userVerificationRequiredButFlagNotSet) { + try await finishRegistration( attestationObject: TestAttestationObjectBuilder() .validMock() .authData(TestAuthDataBuilder().validMock().flags(0b11000001)) .build() .cborEncoded, requireUserVerification: true - ), - expect: WebAuthnError.userVerificationRequiredButFlagNotSet - ) + ) + } } - func testFinishRegistrationFailsIfAttFmtIsNoneButAttStmtIsIncluded() async throws { - try await assertThrowsError( - await finishRegistration( + @Test + func finishRegistrationFailsIfAttFmtIsNoneButAttStmtIsIncluded() async throws { + await #expect(throws: WebAuthnError.attestationStatementMustBeEmpty) { + try await finishRegistration( attestationObject: TestAttestationObjectBuilder() .validMock() .fmt("none") @@ -279,19 +281,19 @@ final class WebAuthnManagerRegistrationTests: XCTestCase { .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 - ) + @Test + func finishRegistrationFailsIfRawIDIsTooLong() async throws { + await #expect(throws: WebAuthnError.credentialRawIDTooLong) { + try await finishRegistration(rawID: [UInt8](repeating: 0, count: 1024)) + } } - func testFinishAuthenticationFailsIfCredentialIDTooLong() async throws { + @Test + func finishAuthenticationFailsIfCredentialIDTooLong() async throws { /// This should succeed as it's on the border of being acceptable _ = try await finishRegistration( attestationObject: TestAttestationObjectBuilder() @@ -310,8 +312,8 @@ final class WebAuthnManagerRegistrationTests: XCTestCase { ) /// While this one should throw - try await assertThrowsError( - await finishRegistration( + await #expect(throws: WebAuthnError.credentialIDTooLong) { + try await finishRegistration( attestationObject: TestAttestationObjectBuilder() .validMock() .authData( @@ -325,12 +327,12 @@ final class WebAuthnManagerRegistrationTests: XCTestCase { ) .build() .cborEncoded - ), - expect: WebAuthnError.credentialIDTooLong - ) + ) + } } - func testFinishRegistrationSucceeds() async throws { + @Test + func finishRegistrationSucceeds() async throws { let credentialID: [UInt8] = [0, 1, 0, 1, 0, 1] let credentialPublicKey: [UInt8] = TestCredentialPublicKeyBuilder().validMock().buildAsByteArray() let authData = TestAuthDataBuilder() @@ -347,18 +349,19 @@ final class WebAuthnManagerRegistrationTests: XCTestCase { rawID: credentialID, attestationObject: attestationObject ) - XCTAssertNotNil(credential) + #expect(credential != nil) - XCTAssertEqual(credential.id, credentialID.base64EncodedString().asString()) - XCTAssertEqual(credential.publicKey, credentialPublicKey) + #expect(credential.id == credentialID.base64EncodedString().asString()) + #expect(credential.publicKey == credentialPublicKey) } - - func testFinishRegistrationFuzzying() async throws { + + @Test + func finishRegistrationFuzzying() 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) + #expect(shouldBeNil == nil) } } diff --git a/scripts/soundness.sh b/scripts/soundness.sh deleted file mode 100755 index aca40cd..0000000 --- a/scripts/soundness.sh +++ /dev/null @@ -1,119 +0,0 @@ -#!/bin/bash -##===----------------------------------------------------------------------===## -## -## 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 -## -##===----------------------------------------------------------------------===## - -set -eu -here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" - -function replace_acceptable_years() { - # this needs to replace all acceptable forms with 'YEARS' - sed -e 's/20[12][78901234]-20[12][8901234]/YEARS/' -e 's/20[12][8901234]/YEARS/' -} - -printf "=> Checking for unacceptable language... " -# This greps for unacceptable terminology. The square bracket[s] are so that -# "git grep" doesn't find the lines that greps :). -unacceptable_terms=( - -e blacklis[t] - -e whitelis[t] - -e slav[e] - -e sanit[y] -) - -# We have to exclude the code of conduct as it gives examples of unacceptable -# language. -if git grep --color=never -i "${unacceptable_terms[@]}" -- . ":(exclude)CODE_OF_CONDUCT.md" > /dev/null; then - printf "\033[0;31mUnacceptable language found.\033[0m\n" - git grep -i "${unacceptable_terms[@]}" -- . ":(exclude)CODE_OF_CONDUCT.md" - exit 1 -fi -printf "\033[0;32mokay.\033[0m\n" - -printf "=> Checking license headers... " -tmp=$(mktemp /tmp/.webauthn-swift-soundness_XXXXXX) - -for language in swift bash; do - declare -a matching_files - declare -a exceptions - expections=( ) - matching_files=( -name '*' ) - case "$language" in - swift) - exceptions=( -name Package.swift ) - matching_files=( -name '*.swift' ) - cat > "$tmp" <<"EOF" -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift WebAuthn open source project -// -// Copyright (c) YEARS the Swift WebAuthn project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// -EOF - ;; - bash) - matching_files=( -name '*.sh' ) - cat > "$tmp" <<"EOF" -#!/bin/bash -##===----------------------------------------------------------------------===## -## -## This source file is part of the Swift WebAuthn open source project -## -## Copyright (c) YEARS the Swift WebAuthn project authors -## Licensed under Apache License v2.0 -## -## See LICENSE.txt for license information -## -## SPDX-License-Identifier: Apache-2.0 -## -##===----------------------------------------------------------------------===## -EOF - ;; - *) - echo >&2 "ERROR: unknown language '$language'" - ;; - esac - - expected_lines=$(cat "$tmp" | wc -l) - expected_sha=$(cat "$tmp" | shasum) - - ( - cd "$here/.." - { - find . \ - \( \! -path './.build/*' -a \ - \( "${matching_files[@]}" \) -a \ - \( \! \( "${exceptions[@]}" \) \) \) - - if [[ "$language" = bash ]]; then - # add everything with a shell shebang too - git grep --full-name -l '#!/bin/bash' - git grep --full-name -l '#!/bin/sh' - fi - } | while read line; do - if [[ "$(cat "$line" | replace_acceptable_years | head -n $expected_lines | shasum)" != "$expected_sha" ]]; then - printf "\033[0;31mmissing headers in file '$line'!\033[0m\n" - diff -u <(cat "$line" | replace_acceptable_years | head -n $expected_lines) "$tmp" - exit 1 - fi - done - printf "\033[0;32mokay.\033[0m\n" - ) -done - -rm "$tmp"