├── .github └── workflows │ ├── main.yml │ └── pull_request.yml ├── .gitignore ├── .license_header_template ├── .licenseignore ├── .mailmap ├── .spi.yml ├── .swiftlint.yml ├── LICENSE ├── NOTICE.txt ├── Package.swift ├── README.md ├── Sources └── WebAuthn │ ├── Ceremonies │ ├── Authentication │ │ ├── AuthenticationCredential.swift │ │ ├── AuthenticatorAssertionResponse.swift │ │ ├── PublicKeyCredentialRequestOptions.swift │ │ └── VerifiedAuthentication.swift │ ├── Registration │ │ ├── AttestationConveyancePreference.swift │ │ ├── AttestationFormat.swift │ │ ├── AttestationObject.swift │ │ ├── AttestedCredentialData.swift │ │ ├── AuthenticatorAttestationResponse.swift │ │ ├── Credential.swift │ │ ├── Formats │ │ │ ├── PackedAttestation.swift │ │ │ ├── TPMAttestation+Structs.swift │ │ │ └── TPMAttestation.swift │ │ ├── PublicKeyCredentialCreationOptions.swift │ │ └── RegistrationCredential.swift │ └── Shared │ │ ├── AuthenticatorAttachment.swift │ │ ├── AuthenticatorAttestationGloballyUniqueID.swift │ │ ├── AuthenticatorData.swift │ │ ├── AuthenticatorFlags.swift │ │ ├── COSE │ │ ├── COSEAlgorithmIdentifier.swift │ │ ├── COSECurve.swift │ │ ├── COSEKey.swift │ │ └── COSEKeyType.swift │ │ ├── CollectedClientData.swift │ │ ├── CredentialPublicKey.swift │ │ └── CredentialType.swift │ ├── Docs.docc │ ├── Example Implementation.md │ ├── authentication.svg │ ├── authentication~dark.svg │ ├── index.md │ ├── overview.svg │ ├── overview~dark.svg │ ├── registration.svg │ └── registration~dark.svg │ ├── Helpers │ ├── Base64Utilities.swift │ ├── ByteCasting.swift │ ├── ChallengeGenerator.swift │ ├── Data+safeSubscript.swift │ ├── Duration+Milliseconds.swift │ ├── KeyedDecodingContainer+decodeURLEncoded.swift │ ├── UInt8+random.swift │ └── UnreferencedStringEnumeration.swift │ ├── WebAuthnError.swift │ ├── WebAuthnManager+Configuration.swift │ └── WebAuthnManager.swift └── Tests └── WebAuthnTests ├── AuthenticatorAttestationGloballyUniqueIDTests.swift ├── DurationTests.swift ├── Formats └── TPMAttestationTests │ └── CertInfoTests.swift ├── HelpersTests.swift ├── Mocks ├── MockChallengeGenerator.swift └── MockUser.swift ├── Utils ├── Hexadecimal.swift └── TestModels │ ├── TestAttestationObject.swift │ ├── TestAuthData.swift │ ├── TestClientDataJSON.swift │ ├── TestConstants.swift │ ├── TestCredentialPublicKey.swift │ ├── TestECCKeyPair.swift │ ├── TestKeyConfiguration.swift │ └── TestRSAKeyPair.swift ├── WebAuthnManagerAuthenticationTests.swift ├── WebAuthnManagerIntegrationTests.swift └── WebAuthnManagerRegistrationTests.swift /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Main 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | unit-tests: 9 | name: Unit tests 10 | uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main 11 | with: 12 | exclude_swift_versions: "[{\"swift_version\": \"5.8\"}, {\"swift_version\": \"5.9\"}, {\"swift_version\": \"5.10\"}]" 13 | swift_flags: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 14 | swift_nightly_flags: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 15 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request 2 | 3 | on: 4 | pull_request: 5 | types: [opened, reopened, synchronize] 6 | 7 | jobs: 8 | soundness: 9 | name: Soundness 10 | uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main 11 | with: 12 | license_header_check_project_name: "Swift WebAuthn" 13 | shell_check_enabled: false 14 | format_check_enabled: false 15 | docs_check_enabled: false 16 | 17 | unit-tests: 18 | name: Unit tests 19 | uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main 20 | with: 21 | linux_exclude_swift_versions: "[{\"swift_version\": \"5.8\"}, {\"swift_version\": \"5.9\"}, {\"swift_version\": \"5.10\"}]" 22 | windows_exclude_swift_versions: "[{\"swift_version\": \"5.8\"}, {\"swift_version\": \"5.9\"}, {\"swift_version\": \"5.10\"}]" 23 | swift_flags: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 24 | swift_nightly_flags: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Package.resolved 2 | .build/ 3 | *.xcodeproj 4 | DerivedData 5 | .DS_Store 6 | .swiftpm/ -------------------------------------------------------------------------------- /.license_header_template: -------------------------------------------------------------------------------- 1 | @@===----------------------------------------------------------------------===@@ 2 | @@ 3 | @@ This source file is part of the Swift WebAuthn open source project 4 | @@ 5 | @@ Copyright (c) YEARS the Swift WebAuthn project authors 6 | @@ Licensed under Apache License v2.0 7 | @@ 8 | @@ See LICENSE.txt for license information 9 | @@ 10 | @@ SPDX-License-Identifier: Apache-2.0 11 | @@ 12 | @@===----------------------------------------------------------------------===@@ 13 | -------------------------------------------------------------------------------- /.licenseignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | .licenseignore 3 | .swiftformatignore 4 | .spi.yml 5 | .swiftlint.yml 6 | .swift-format 7 | .github/* 8 | *.md 9 | **/*.md 10 | CONTRIBUTORS.txt 11 | LICENSE 12 | NOTICE.txt 13 | Package.swift 14 | Package@swift-*.swift 15 | Package.resolved 16 | **/*.docc/* 17 | **/.gitignore 18 | **/Package.swift 19 | **/Package.resolved 20 | **/docker-compose*.yaml 21 | **/docker/* 22 | **/.dockerignore 23 | **/Dockerfile 24 | **/Makefile 25 | **/*.html 26 | **/*-template.yml 27 | **/*.xcworkspace/* 28 | **/*.xcodeproj/* 29 | **/*.xcassets/* 30 | **/*.appiconset/* 31 | **/ResourcePackaging/hello.txt 32 | .mailmap 33 | .swiftformat 34 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | Marius Seufzer <44228394+marius-se@users.noreply.github.com> 2 | Tim Condon <0xtimc@gmail.com> <0xTim@users.noreply.github.com> 3 | Tim Condon <0xtimc@gmail.com> Tim <0xtimc@gmail.com> 4 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [WebAuthn] 5 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - comment_spacing 3 | excluded: 4 | - .build 5 | 6 | 7 | identifier_name: 8 | excluded: 9 | - id 10 | - rp 11 | 12 | line_length: 13 | ignores_comments: true 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | Swift WebAuthn project 2 | 3 | ---------------------------------------------------------------------- 4 | 5 | This source file is part of the Swift WebAuthn open source project 6 | 7 | Copyright (c) 2022 the Swift WebAuthn project authors 8 | Licensed under Apache License v2.0 9 | 10 | See LICENSE.txt for license information 11 | 12 | SPDX-License-Identifier: Apache-2.0 13 | 14 | ---------------------------------------------------------------------- 15 | 16 | This product contains a derivation of various scripts from SwiftNIO. 17 | 18 | * LICENSE (Apache License 2.0): 19 | * https://www.apache.org/licenses/LICENSE-2.0 20 | * HOMEPAGE: 21 | * https://github.com/apple/swift-nio 22 | 23 | --- 24 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | //===----------------------------------------------------------------------===// 3 | // 4 | // This source file is part of the Swift WebAuthn open source project 5 | // 6 | // Copyright (c) 2022 the Swift WebAuthn project authors 7 | // Licensed under Apache License v2.0 8 | // 9 | // See LICENSE.txt for license information 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import PackageDescription 16 | 17 | let package = Package( 18 | name: "swift-webauthn", 19 | platforms: [ 20 | .macOS(.v13) 21 | ], 22 | products: [ 23 | .library(name: "WebAuthn", targets: ["WebAuthn"]) 24 | ], 25 | dependencies: [ 26 | .package(url: "https://github.com/unrelentingtech/SwiftCBOR.git", from: "0.4.7"), 27 | .package(url: "https://github.com/apple/swift-crypto.git", "3.8.1" ..< "4.0.0"), 28 | .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"), 29 | .package(url: "https://github.com/swiftlang/swift-docc-plugin.git", from: "1.1.0") 30 | ], 31 | targets: [ 32 | .target( 33 | name: "WebAuthn", 34 | dependencies: [ 35 | "SwiftCBOR", 36 | .product(name: "Crypto", package: "swift-crypto"), 37 | .product(name: "_CryptoExtras", package: "swift-crypto"), 38 | .product(name: "Logging", package: "swift-log"), 39 | ] 40 | ), 41 | .testTarget( 42 | name: "WebAuthnTests", 43 | dependencies: [ 44 | .target(name: "WebAuthn"), 45 | .product(name: "Crypto", package: "swift-crypto"), 46 | .product(name: "_CryptoExtras", package: "swift-crypto"), 47 | ] 48 | ) 49 | ] 50 | ) 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # swift-webauthn 2 | 3 | This package provides a Swift implementation of the [WebAuthn API](https://w3c.github.io/webauthn) focused on making it 4 | easy to leverage the power of WebAuthn to support Passkeys and security keys. 5 | 6 | ## Getting Started 7 | 8 | **Adding the dependency** 9 | 10 | Add the following entry in your `Package.swift` to start using `WebAuthn`: 11 | 12 | ```swift 13 | .package(url: "https://github.com/swift-server/swift-webauthn.git", from: "1.0.0-alpha.2") 14 | ``` 15 | 16 | and `WebAuthn` dependency to your target: 17 | 18 | ```swift 19 | .target(name: "MyApp", dependencies: [.product(name: "WebAuthn", package: "swift-webauthn")]) 20 | ``` 21 | 22 | ### Setup 23 | 24 | Configure your Relying Party with a `WebAuthnManager` instance: 25 | 26 | ```swift 27 | let webAuthnManager = WebAuthnManager( 28 | configuration: WebAuthnManager.Configuration( 29 | relyingPartyID: "example.com", 30 | relyingPartyName: "My Fancy Web App", 31 | relyingPartyOrigin: "https://example.com" 32 | ) 33 | ) 34 | ``` 35 | 36 | ### Registration 37 | 38 | For a registration ceremony use the following two methods: 39 | 40 | - `WebAuthnManager.beginRegistration()` 41 | - `WebAuthnManager.finishRegistration()` 42 | 43 | ### Authentication 44 | 45 | For an authentication ceremony use the following two methods: 46 | 47 | - `WebAuthnManager.beginAuthentication()` 48 | - `WebAuthnManager.finishAuthentication()` 49 | 50 | ## Contributing 51 | 52 | If you add any new files, please run the following command at the root of the repo to identify any missing license headers: 53 | ```bash 54 | % 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)" 55 | ``` 56 | 57 | ## Credits 58 | 59 | Swift WebAuthn is heavily inspired by existing WebAuthn libraries like 60 | [py_webauthn](https://github.com/duo-labs/py_webauthn) and [go-webauthn](https://github.com/go-webauthn/webauthn). 61 | 62 | ## Links 63 | 64 | - [WebAuthn.io](https://webauthn.io/) 65 | - [WebAuthn guide](https://webauthn.guide/) 66 | - [WebAuthn Spec](https://w3c.github.io/webauthn/) 67 | - [CBOR.me](https://cbor.me/) 68 | -------------------------------------------------------------------------------- /Sources/WebAuthn/Ceremonies/Authentication/AuthenticationCredential.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2022 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import Foundation 15 | 16 | /// The unprocessed response received from `navigator.credentials.get()`. 17 | /// 18 | /// When decoding using `Decodable`, the `rawID` is decoded from base64url to bytes. 19 | public struct AuthenticationCredential: Sendable { 20 | /// The credential ID of the newly created credential. 21 | public let id: URLEncodedBase64 22 | 23 | /// The raw credential ID of the newly created credential. 24 | public let rawID: [UInt8] 25 | 26 | /// The attestation response from the authenticator. 27 | public let response: AuthenticatorAssertionResponse 28 | 29 | /// Reports the authenticator attachment modality in effect at the time the navigator.credentials.create() or 30 | /// navigator.credentials.get() methods successfully complete 31 | public let authenticatorAttachment: AuthenticatorAttachment? 32 | 33 | /// Value will always be ``CredentialType/publicKey`` (for now) 34 | public let type: CredentialType 35 | } 36 | 37 | extension AuthenticationCredential: Decodable { 38 | public init(from decoder: any Decoder) throws { 39 | let container = try decoder.container(keyedBy: CodingKeys.self) 40 | 41 | id = try container.decode(URLEncodedBase64.self, forKey: .id) 42 | rawID = try container.decodeBytesFromURLEncodedBase64(forKey: .rawID) 43 | response = try container.decode(AuthenticatorAssertionResponse.self, forKey: .response) 44 | authenticatorAttachment = try container.decodeIfPresent(AuthenticatorAttachment.self, forKey: .authenticatorAttachment) 45 | type = try container.decode(CredentialType.self, forKey: .type) 46 | } 47 | 48 | private enum CodingKeys: String, CodingKey { 49 | case id 50 | case rawID = "rawId" 51 | case response 52 | case authenticatorAttachment 53 | case type 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/WebAuthn/Ceremonies/Authentication/AuthenticatorAssertionResponse.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2022 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import Foundation 15 | import Crypto 16 | 17 | /// This is what the authenticator device returned after we requested it to authenticate a user. 18 | /// 19 | /// When decoding using `Decodable`, byte arrays are decoded from base64url to bytes. 20 | public struct AuthenticatorAssertionResponse: Sendable { 21 | /// Representation of what we passed to `navigator.credentials.get()` 22 | /// 23 | /// When decoding using `Decodable`, this is decoded from base64url to bytes. 24 | public let clientDataJSON: [UInt8] 25 | 26 | /// Contains the authenticator data returned by the authenticator. 27 | /// 28 | /// When decoding using `Decodable`, this is decoded from base64url to bytes. 29 | public let authenticatorData: [UInt8] 30 | 31 | /// Contains the raw signature returned from the authenticator 32 | /// 33 | /// When decoding using `Decodable`, this is decoded from base64url to bytes. 34 | public let signature: [UInt8] 35 | 36 | /// Contains the user handle returned from the authenticator, or null if the authenticator did not return 37 | /// a user handle. Used by to give scope to credentials. 38 | /// 39 | /// When decoding using `Decodable`, this is decoded from base64url to bytes. 40 | public let userHandle: [UInt8]? 41 | 42 | /// Contains an attestation object, if the authenticator supports attestation in assertions. 43 | /// The attestation object, if present, includes an attestation statement. Unlike the attestationObject 44 | /// in an AuthenticatorAttestationResponse, it does not contain an authData key because the authenticator 45 | /// data is provided directly in an AuthenticatorAssertionResponse structure. 46 | /// 47 | /// When decoding using `Decodable`, this is decoded from base64url to bytes. 48 | public let attestationObject: [UInt8]? 49 | } 50 | 51 | extension AuthenticatorAssertionResponse: Decodable { 52 | public init(from decoder: any Decoder) throws { 53 | let container = try decoder.container(keyedBy: CodingKeys.self) 54 | 55 | clientDataJSON = try container.decodeBytesFromURLEncodedBase64(forKey: .clientDataJSON) 56 | authenticatorData = try container.decodeBytesFromURLEncodedBase64(forKey: .authenticatorData) 57 | signature = try container.decodeBytesFromURLEncodedBase64(forKey: .signature) 58 | userHandle = try container.decodeBytesFromURLEncodedBase64IfPresent(forKey: .userHandle) 59 | attestationObject = try container.decodeBytesFromURLEncodedBase64IfPresent(forKey: .attestationObject) 60 | } 61 | 62 | private enum CodingKeys: String, CodingKey { 63 | case clientDataJSON 64 | case authenticatorData 65 | case signature 66 | case userHandle 67 | case attestationObject 68 | } 69 | } 70 | 71 | struct ParsedAuthenticatorAssertionResponse: Sendable { 72 | let rawClientData: [UInt8] 73 | let clientData: CollectedClientData 74 | let rawAuthenticatorData: [UInt8] 75 | let authenticatorData: AuthenticatorData 76 | let signature: URLEncodedBase64 77 | let userHandle: [UInt8]? 78 | 79 | init(from authenticatorAssertionResponse: AuthenticatorAssertionResponse) throws { 80 | rawClientData = authenticatorAssertionResponse.clientDataJSON 81 | clientData = try JSONDecoder().decode(CollectedClientData.self, from: Data(rawClientData)) 82 | 83 | rawAuthenticatorData = authenticatorAssertionResponse.authenticatorData 84 | authenticatorData = try AuthenticatorData(bytes: rawAuthenticatorData) 85 | signature = authenticatorAssertionResponse.signature.base64URLEncodedString() 86 | userHandle = authenticatorAssertionResponse.userHandle 87 | } 88 | 89 | // swiftlint:disable:next function_parameter_count 90 | func verify( 91 | expectedChallenge: [UInt8], 92 | relyingPartyOrigin: String, 93 | relyingPartyID: String, 94 | requireUserVerification: Bool, 95 | credentialPublicKey: [UInt8], 96 | credentialCurrentSignCount: UInt32 97 | ) throws { 98 | try clientData.verify( 99 | storedChallenge: expectedChallenge, 100 | ceremonyType: .assert, 101 | relyingPartyOrigin: relyingPartyOrigin 102 | ) 103 | 104 | let expectedRelyingPartyIDData = Data(relyingPartyID.utf8) 105 | let expectedRelyingPartyIDHash = SHA256.hash(data: expectedRelyingPartyIDData) 106 | guard expectedRelyingPartyIDHash == authenticatorData.relyingPartyIDHash else { 107 | throw WebAuthnError.relyingPartyIDHashDoesNotMatch 108 | } 109 | 110 | guard authenticatorData.flags.userPresent else { throw WebAuthnError.userPresentFlagNotSet } 111 | if requireUserVerification { 112 | guard authenticatorData.flags.userVerified else { throw WebAuthnError.userVerifiedFlagNotSet } 113 | } 114 | 115 | if authenticatorData.counter > 0 || credentialCurrentSignCount > 0 { 116 | guard authenticatorData.counter > credentialCurrentSignCount else { 117 | // This is a signal that the authenticator may be cloned, i.e. at least two copies of the credential 118 | // private key may exist and are being used in parallel. 119 | throw WebAuthnError.potentialReplayAttack 120 | } 121 | } 122 | 123 | let clientDataHash = SHA256.hash(data: rawClientData) 124 | let signatureBase = rawAuthenticatorData + clientDataHash 125 | 126 | let credentialPublicKey = try CredentialPublicKey(publicKeyBytes: credentialPublicKey) 127 | guard let signatureData = signature.urlDecoded.decoded else { throw WebAuthnError.invalidSignature } 128 | try credentialPublicKey.verify(signature: signatureData, data: signatureBase) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /Sources/WebAuthn/Ceremonies/Authentication/PublicKeyCredentialRequestOptions.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2022 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import Foundation 15 | 16 | /// The `PublicKeyCredentialRequestOptions` gets passed to the WebAuthn API (`navigator.credentials.get()`) 17 | /// 18 | /// When encoding using `Encodable`, the byte arrays are encoded as base64url. 19 | /// 20 | /// - SeeAlso: https://www.w3.org/TR/webauthn-2/#dictionary-assertion-options 21 | public struct PublicKeyCredentialRequestOptions: Encodable, Sendable { 22 | /// A challenge that the authenticator signs, along with other data, when producing an authentication assertion 23 | /// 24 | /// When encoding using `Encodable` this is encoded as base64url. 25 | public let challenge: [UInt8] 26 | 27 | /// A time, in seconds, that the caller is willing to wait for the call to complete. This is treated as a 28 | /// hint, and may be overridden by the client. 29 | /// 30 | /// - Note: When encoded, this value is represented in milleseconds as a ``UInt32``. 31 | /// See https://www.w3.org/TR/webauthn-2/#dictionary-assertion-options 32 | public let timeout: Duration? 33 | 34 | /// The ID of the Relying Party making the request. 35 | /// 36 | /// This is configured on ``WebAuthnManager`` before its ``WebAuthnManager/beginAuthentication(timeout:allowCredentials:userVerification:)`` method is called. 37 | /// - Note: When encoded, this field appears as `rpId` to match the expectations of `navigator.credentials.get()`. 38 | public let relyingPartyID: String 39 | 40 | /// Optionally used by the client to find authenticators eligible for this authentication ceremony. 41 | public let allowCredentials: [PublicKeyCredentialDescriptor]? 42 | 43 | /// Specifies whether the user should be verified during the authentication ceremony. 44 | public let userVerification: UserVerificationRequirement? 45 | 46 | // let extensions: [String: Any] 47 | 48 | public func encode(to encoder: any Encoder) throws { 49 | var container = encoder.container(keyedBy: CodingKeys.self) 50 | 51 | try container.encode(challenge.base64URLEncodedString(), forKey: .challenge) 52 | try container.encodeIfPresent(timeout?.milliseconds, forKey: .timeout) 53 | try container.encode(relyingPartyID, forKey: .rpID) 54 | try container.encodeIfPresent(allowCredentials, forKey: .allowCredentials) 55 | try container.encodeIfPresent(userVerification, forKey: .userVerification) 56 | } 57 | 58 | private enum CodingKeys: String, CodingKey { 59 | case challenge 60 | case timeout 61 | case rpID = "rpId" 62 | case allowCredentials 63 | case userVerification 64 | } 65 | } 66 | 67 | /// Information about a generated credential. 68 | /// 69 | /// When encoding using `Encodable`, `id` is encoded as base64url. 70 | public struct PublicKeyCredentialDescriptor: Equatable, Encodable, Sendable { 71 | /// Defines hints as to how clients might communicate with a particular authenticator in order to obtain an 72 | /// assertion for a specific credential 73 | public enum AuthenticatorTransport: String, Equatable, Encodable, Sendable { 74 | /// Indicates the respective authenticator can be contacted over removable USB. 75 | case usb 76 | /// Indicates the respective authenticator can be contacted over Near Field Communication (NFC). 77 | case nfc 78 | /// Indicates the respective authenticator can be contacted over Bluetooth Smart (Bluetooth Low Energy / BLE). 79 | case ble 80 | /// Indicates the respective authenticator can be contacted using a combination of (often separate) 81 | /// data-transport and proximity mechanisms. This supports, for example, authentication on a desktop 82 | /// computer using a smartphone. 83 | case hybrid 84 | /// Indicates the respective authenticator is contacted using a client device-specific transport, i.e., it is 85 | /// a platform authenticator. These authenticators are not removable from the client device. 86 | case `internal` 87 | } 88 | 89 | /// Will always be ``CredentialType/publicKey`` 90 | public let type: CredentialType 91 | 92 | /// The sequence of bytes representing the credential's ID 93 | /// 94 | /// When encoding using `Encodable`, this is encoded as base64url. 95 | public let id: [UInt8] 96 | 97 | /// The types of connections to the client/browser the authenticator supports 98 | public let transports: [AuthenticatorTransport] 99 | 100 | public init( 101 | type: CredentialType = .publicKey, 102 | id: [UInt8], 103 | transports: [AuthenticatorTransport] = [] 104 | ) { 105 | self.type = type 106 | self.id = id 107 | self.transports = transports 108 | } 109 | 110 | public func encode(to encoder: any Encoder) throws { 111 | var container = encoder.container(keyedBy: CodingKeys.self) 112 | 113 | try container.encode(type, forKey: .type) 114 | try container.encode(id.base64URLEncodedString(), forKey: .id) 115 | try container.encodeIfPresent(transports, forKey: .transports) 116 | } 117 | 118 | private enum CodingKeys: String, CodingKey { 119 | case type 120 | case id 121 | case transports 122 | } 123 | } 124 | 125 | /// The Relying Party may require user verification for some of its operations but not for others, and may use this 126 | /// type to express its needs. 127 | public enum UserVerificationRequirement: String, Encodable, Sendable { 128 | /// The Relying Party requires user verification for the operation and will fail the overall ceremony if the 129 | /// user wasn't verified. 130 | case required 131 | /// The Relying Party prefers user verification for the operation if possible, but will not fail the operation. 132 | case preferred 133 | /// The Relying Party does not want user verification employed during the operation (e.g., in the interest of 134 | /// minimizing disruption to the user interaction flow). 135 | case discouraged 136 | } 137 | -------------------------------------------------------------------------------- /Sources/WebAuthn/Ceremonies/Authentication/VerifiedAuthentication.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2023 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import Foundation 15 | 16 | /// On successful authentication, this structure contains a summary of the authentication flow 17 | public struct VerifiedAuthentication: Sendable { 18 | public enum CredentialDeviceType: String, Sendable { 19 | case singleDevice = "single_device" 20 | case multiDevice = "multi_device" 21 | } 22 | 23 | /// The credential id associated with the public key 24 | public let credentialID: URLEncodedBase64 25 | /// The updated sign count after the authentication ceremony 26 | public let newSignCount: UInt32 27 | /// Whether the authenticator is a single- or multi-device authenticator. This value is determined after 28 | /// registration and will not change afterwards. 29 | public let credentialDeviceType: CredentialDeviceType 30 | /// Whether the authenticator is known to be backed up currently 31 | public let credentialBackedUp: Bool 32 | } 33 | -------------------------------------------------------------------------------- /Sources/WebAuthn/Ceremonies/Registration/AttestationConveyancePreference.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2022 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | /// Options to specify the Relying Party's preference regarding attestation conveyance during credential generation. 15 | /// 16 | /// Currently only supports `none`. 17 | public enum AttestationConveyancePreference: String, Encodable, Sendable { 18 | /// Indicates the Relying Party is not interested in authenticator attestation. 19 | case none 20 | // case indirect 21 | // case direct 22 | // case enterprise 23 | } 24 | -------------------------------------------------------------------------------- /Sources/WebAuthn/Ceremonies/Registration/AttestationFormat.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2022 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | public enum AttestationFormat: String, RawRepresentable, Equatable, Sendable { 15 | case packed 16 | case tpm 17 | case androidKey = "android-key" 18 | case androidSafetynet = "android-safetynet" 19 | case fidoU2F = "fido-u2f" 20 | case apple 21 | case none 22 | } 23 | -------------------------------------------------------------------------------- /Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2022 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import Foundation 15 | import Crypto 16 | @preconcurrency import SwiftCBOR 17 | 18 | /// Contains the cryptographic attestation that a new key pair was created by that authenticator. 19 | public struct AttestationObject: Sendable { 20 | let authenticatorData: AuthenticatorData 21 | let rawAuthenticatorData: [UInt8] 22 | let format: AttestationFormat 23 | let attestationStatement: CBOR 24 | 25 | func verify( 26 | relyingPartyID: String, 27 | verificationRequired: Bool, 28 | clientDataHash: SHA256.Digest, 29 | supportedPublicKeyAlgorithms: [PublicKeyCredentialParameters], 30 | pemRootCertificatesByFormat: [AttestationFormat: [Data]] = [:] 31 | ) async throws -> AttestedCredentialData { 32 | let relyingPartyIDHash = SHA256.hash(data: Data(relyingPartyID.utf8)) 33 | 34 | guard relyingPartyIDHash == authenticatorData.relyingPartyIDHash else { 35 | throw WebAuthnError.relyingPartyIDHashDoesNotMatch 36 | } 37 | 38 | guard authenticatorData.flags.userPresent else { 39 | throw WebAuthnError.userPresentFlagNotSet 40 | } 41 | 42 | if verificationRequired { 43 | guard authenticatorData.flags.userVerified else { 44 | throw WebAuthnError.userVerificationRequiredButFlagNotSet 45 | } 46 | } 47 | 48 | guard let attestedCredentialData = authenticatorData.attestedData else { 49 | throw WebAuthnError.attestedCredentialDataMissing 50 | } 51 | 52 | // Step 17. 53 | let credentialPublicKey = try CredentialPublicKey(publicKeyBytes: attestedCredentialData.publicKey) 54 | guard supportedPublicKeyAlgorithms.map(\.alg).contains(credentialPublicKey.key.algorithm) else { 55 | throw WebAuthnError.unsupportedCredentialPublicKeyAlgorithm 56 | } 57 | 58 | // let pemRootCertificates = pemRootCertificatesByFormat[format] ?? [] 59 | switch format { 60 | case .none: 61 | // if format is `none` statement must be empty 62 | guard attestationStatement == .map([:]) else { 63 | throw WebAuthnError.attestationStatementMustBeEmpty 64 | } 65 | // case .packed: 66 | // try await PackedAttestation.verify( 67 | // attStmt: attestationStatement, 68 | // authenticatorData: rawAuthenticatorData, 69 | // clientDataHash: Data(clientDataHash), 70 | // credentialPublicKey: credentialPublicKey, 71 | // pemRootCertificates: pemRootCertificates 72 | // ) 73 | // case .tpm: 74 | // try TPMAttestation.verify( 75 | // attStmt: attestationStatement, 76 | // authenticatorData: rawAuthenticatorData, 77 | // attestedCredentialData: attestedCredentialData, 78 | // clientDataHash: Data(clientDataHash), 79 | // credentialPublicKey: credentialPublicKey, 80 | // pemRootCertificates: pemRootCertificates 81 | // ) 82 | default: 83 | throw WebAuthnError.attestationVerificationNotSupported 84 | } 85 | 86 | return attestedCredentialData 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Sources/WebAuthn/Ceremonies/Registration/AttestedCredentialData.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2022 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | // Contains the new public key created by the authenticator. 15 | struct AttestedCredentialData: Equatable { 16 | let authenticatorAttestationGUID: AAGUID 17 | let credentialID: [UInt8] 18 | let publicKey: [UInt8] 19 | } 20 | -------------------------------------------------------------------------------- /Sources/WebAuthn/Ceremonies/Registration/AuthenticatorAttestationResponse.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2022 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import Foundation 15 | import SwiftCBOR 16 | 17 | /// The response from the authenticator device for the creation of a new public key credential. 18 | /// 19 | /// When decoding using `Decodable`, `clientDataJSON` and `attestationObject` are decoded from base64url to bytes. 20 | public struct AuthenticatorAttestationResponse: Sendable { 21 | /// The client data that was passed to the authenticator during the creation ceremony. 22 | /// 23 | /// When decoding using `Decodable`, this is decoded from base64url to bytes. 24 | public let clientDataJSON: [UInt8] 25 | 26 | /// Contains both attestation data and attestation statement. 27 | /// 28 | /// When decoding using `Decodable`, this is decoded from base64url to bytes. 29 | public let attestationObject: [UInt8] 30 | } 31 | 32 | extension AuthenticatorAttestationResponse: Decodable { 33 | public init(from decoder: any Decoder) throws { 34 | let container = try decoder.container(keyedBy: CodingKeys.self) 35 | 36 | clientDataJSON = try container.decodeBytesFromURLEncodedBase64(forKey: .clientDataJSON) 37 | attestationObject = try container.decodeBytesFromURLEncodedBase64(forKey: .attestationObject) 38 | } 39 | 40 | private enum CodingKeys: String, CodingKey { 41 | case clientDataJSON 42 | case attestationObject 43 | } 44 | } 45 | 46 | /// A parsed version of `AuthenticatorAttestationResponse` 47 | struct ParsedAuthenticatorAttestationResponse { 48 | let clientData: CollectedClientData 49 | let attestationObject: AttestationObject 50 | 51 | init(from rawResponse: AuthenticatorAttestationResponse) throws { 52 | // assembling clientData 53 | let clientData = try JSONDecoder().decode(CollectedClientData.self, from: Data(rawResponse.clientDataJSON)) 54 | self.clientData = clientData 55 | 56 | // Step 11. (assembling attestationObject) 57 | let attestationObjectData = Data(rawResponse.attestationObject) 58 | guard let decodedAttestationObject = try? CBOR.decode([UInt8](attestationObjectData), options: CBOROptions(maximumDepth: 16)) else { 59 | throw WebAuthnError.invalidAttestationObject 60 | } 61 | 62 | guard let authData = decodedAttestationObject["authData"], 63 | case let .byteString(authDataBytes) = authData else { 64 | throw WebAuthnError.invalidAuthData 65 | } 66 | guard let formatCBOR = decodedAttestationObject["fmt"], 67 | case let .utf8String(format) = formatCBOR, 68 | let attestationFormat = AttestationFormat(rawValue: format) else { 69 | throw WebAuthnError.invalidFmt 70 | } 71 | 72 | guard let attestationStatement = decodedAttestationObject["attStmt"] else { 73 | throw WebAuthnError.missingAttStmt 74 | } 75 | 76 | attestationObject = AttestationObject( 77 | authenticatorData: try AuthenticatorData(bytes: authDataBytes), 78 | rawAuthenticatorData: authDataBytes, 79 | format: attestationFormat, 80 | attestationStatement: attestationStatement 81 | ) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Sources/WebAuthn/Ceremonies/Registration/Credential.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2022 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import Foundation 15 | 16 | /// After a successful registration ceremony we pass this data back to the relying party. It contains all needed 17 | /// information about a WebAuthn credential for storage in e.g. a database. 18 | public struct Credential: Sendable { 19 | /// Value will always be ``CredentialType/publicKey`` (for now) 20 | public let type: CredentialType 21 | 22 | /// base64 encoded String of the credential ID bytes 23 | public let id: String 24 | 25 | /// The public key for this certificate 26 | public let publicKey: [UInt8] 27 | 28 | /// How often the authenticator says the credential was used 29 | /// If this is not implemented by the authenticator this value will always be zero. 30 | public let signCount: UInt32 31 | 32 | /// Wether the public key is allowed to be backed up. 33 | /// If a public key is considered backup eligible it is referred to as a multi-device credential (the 34 | /// opposite being single-device credential) 35 | public let backupEligible: Bool 36 | 37 | /// If the public key is currently backed up (using another authenticator than the one that generated 38 | /// the credential) 39 | public let isBackedUp: Bool 40 | 41 | // MARK: Optional content 42 | 43 | public let attestationObject: AttestationObject 44 | 45 | public let attestationClientDataJSON: CollectedClientData 46 | } 47 | -------------------------------------------------------------------------------- /Sources/WebAuthn/Ceremonies/Registration/Formats/PackedAttestation.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2023 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | // // 🚨 WIP 15 | 16 | // import Foundation 17 | // import SwiftCBOR 18 | // import X509 19 | // import Crypto 20 | 21 | // /// 🚨 WIP 22 | // struct PackedAttestation { 23 | // enum PackedAttestationError: Error { 24 | // case invalidAlg 25 | // case invalidSig 26 | // case invalidX5C 27 | // case invalidLeafCertificate 28 | // case missingAttestationCertificate 29 | // case algDoesNotMatch 30 | // case missingAttestedCredential 31 | // case notImplemented 32 | // } 33 | 34 | // static func verify( 35 | // attStmt: CBOR, 36 | // authenticatorData: Data, 37 | // clientDataHash: Data, 38 | // credentialPublicKey: CredentialPublicKey, 39 | // pemRootCertificates: [Data] 40 | // ) async throws { 41 | // guard let algCBOR = attStmt["alg"], 42 | // case let .negativeInt(algorithmNegative) = algCBOR, 43 | // let alg = COSEAlgorithmIdentifier(rawValue: -1 - Int(algorithmNegative)) else { 44 | // throw PackedAttestationError.invalidAlg 45 | // } 46 | // guard let sigCBOR = attStmt["sig"], case let .byteString(sig) = sigCBOR else { 47 | // throw PackedAttestationError.invalidSig 48 | // } 49 | 50 | // let verificationData = authenticatorData + clientDataHash 51 | 52 | // if let x5cCBOR = attStmt["x5c"] { 53 | // guard case let .array(x5cCBOR) = x5cCBOR else { 54 | // throw PackedAttestationError.invalidX5C 55 | // } 56 | 57 | // let x5c: [Certificate] = try x5cCBOR.map { 58 | // guard case let .byteString(certificate) = $0 else { 59 | // throw PackedAttestationError.invalidX5C 60 | // } 61 | // return try Certificate(derEncoded: certificate) 62 | // } 63 | // guard let leafCertificate = x5c.first else { throw PackedAttestationError.invalidX5C } 64 | // let intermediates = CertificateStore(x5c[1...]) 65 | // let rootCertificates = CertificateStore( 66 | // try pemRootCertificates.map { try Certificate(derEncoded: [UInt8]($0)) } 67 | // ) 68 | 69 | // var verifier = Verifier(rootCertificates: rootCertificates, policy: .init(policies: [])) 70 | // let verifierResult: VerificationResult = await verifier.validate( 71 | // leafCertificate: leafCertificate, 72 | // intermediates: intermediates 73 | // ) 74 | // guard case .validCertificate = verifierResult else { 75 | // throw PackedAttestationError.invalidLeafCertificate 76 | // } 77 | 78 | // // 2. Verify signature 79 | // // let leafCertificatePublicKey: Certificate.PublicKey = leafCertificate.publicKey 80 | 81 | // // 2.1 Determine key type (with new Swift ASN.1/ Certificates library) 82 | // // 2.2 Create corresponding public key object (EC2PublicKey/RSAPublicKey/OKPPublicKey) 83 | // // 2.3 Call verify method on public key with signature + data 84 | // throw PackedAttestationError.notImplemented 85 | // } else { // self attestation is in use 86 | // guard credentialPublicKey.key.algorithm == alg else { 87 | // throw PackedAttestationError.algDoesNotMatch 88 | // } 89 | 90 | // try credentialPublicKey.verify(signature: Data(sig), data: verificationData) 91 | // } 92 | // } 93 | // } 94 | 95 | // extension Certificate.PublicKey { 96 | // // func verifySignature(_ signature: Data, algorithm: COSEAlgorithmIdentifier, data: Data) throws -> Bool { 97 | // // switch algorithm { 98 | // // 99 | // // case .algES256: 100 | // // guard case let .p256(key) = backing else { return false } 101 | // // let signature = try P256.Signing.ECDSASignature(derRepresentation: signature) 102 | // // return key.isValidSignature(signature, for: data) 103 | // // case .algES384: 104 | // // guard case let .p384(key) = backing else { return false } 105 | // // let signature = try P384.Signing.ECDSASignature(derRepresentation: signature) 106 | // // return key.isValidSignature(signature, for: data) 107 | // // case .algES512: 108 | // // guard case let .p521(key) = backing else { return false } 109 | // // let signature = try P521.Signing.ECDSASignature(derRepresentation: signature) 110 | // // return key.isValidSignature(signature, for: data) 111 | // // case .algPS256: 112 | // // case .algPS384: 113 | // // case .algPS512: 114 | // // case .algRS1: 115 | // // case .algRS256: 116 | // // case .algRS384: 117 | // // case .algRS512: 118 | // //} 119 | // // switch backing { 120 | // // case let .p256(key): 121 | // // try EC2PublicKey(rawRepresentation: key.rawRepresentation, algorithm: algorithm) 122 | // // .verify(signature: signature, data: data) 123 | // // case let .p384(key): 124 | // // try EC2PublicKey(rawRepresentation: key.rawRepresentation, algorithm: algorithm) 125 | // // .verify(signature: signature, data: data) 126 | // // case let .p521(key): 127 | // // try EC2PublicKey(rawRepresentation: key.rawRepresentation, algorithm: algorithm) 128 | // // .verify(signature: signature, data: data) 129 | // // case let .rsa(key): 130 | // // try RSAPublicKeyData(rawRepresentation: key.derRepresentation, algorithm: algorithm) 131 | // // .verify(signature: signature, data: data) 132 | // // } 133 | // // } 134 | // } 135 | -------------------------------------------------------------------------------- /Sources/WebAuthn/Ceremonies/Registration/Formats/TPMAttestation+Structs.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2023 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | // import Foundation 15 | // extension TPMAttestation { 16 | // enum CertInfoError: Error { 17 | // case magicInvalid 18 | // case typeInvalid 19 | // case dataTooShort 20 | // case tpmImplementationIsWIP 21 | // } 22 | 23 | // struct AttestationInformation { 24 | // let name: Data 25 | // let qualifiedName: Data 26 | // } 27 | 28 | // struct CertInfo { 29 | // let magic: Data 30 | // let type: Data 31 | // let qualifiedSigner: Data 32 | // let extraData: Data 33 | // let clockInfo: Data 34 | // let firmwareVersion: Data 35 | // let attested: AttestationInformation 36 | 37 | // init?(fromBytes data: Data) { 38 | // var pointer = 0 39 | 40 | // guard let magic = data[safe: pointer..<(pointer + 4)] else { return nil } 41 | // self.magic = magic 42 | // pointer += 4 43 | 44 | // guard let type = data[safe: pointer..<(pointer + 2)] else { return nil } 45 | // self.type = type 46 | // pointer += 2 47 | 48 | // guard let qualifiedSignerLengthData = data[safe: pointer..<(pointer + 2)] else { return nil } 49 | // pointer += 2 50 | // let qualifiedSignerLength: Int = qualifiedSignerLengthData.toInteger(endian: .big) 51 | // guard let qualifiedSigner = data[safe: pointer..<(pointer + qualifiedSignerLength)] else { return nil } 52 | // self.qualifiedSigner = qualifiedSigner 53 | // pointer += qualifiedSignerLength 54 | 55 | // guard let extraDataLengthData = data[safe: pointer..<(pointer + 2)] else { return nil } 56 | // pointer += 2 57 | // let extraDataLength: Int = extraDataLengthData.toInteger(endian: .big) 58 | // guard let extraData = data[safe: pointer..<(pointer + extraDataLength)] else { return nil } 59 | // self.extraData = extraData 60 | // pointer += extraDataLength 61 | 62 | // guard let clockInfo = data[safe: pointer..<(pointer + 17)] else { return nil } 63 | // self.clockInfo = clockInfo 64 | // pointer += 17 65 | 66 | // guard let firmwareVersion = data[safe: pointer..<(pointer + 8)] else { return nil } 67 | // self.firmwareVersion = firmwareVersion 68 | // pointer += 8 69 | 70 | // guard let attestedNameLengthData = data[safe: pointer..<(pointer + 2)] else { return nil } 71 | // pointer += 2 72 | // let attestedNameLength: Int = attestedNameLengthData.toInteger(endian: .big) 73 | // guard let attestedName = data[safe: pointer..<(pointer + attestedNameLength)] else { return nil } 74 | // pointer += attestedNameLength 75 | 76 | // guard let qualifiedNameLengthData = data[safe: pointer..<(pointer + 2)] else { return nil } 77 | // pointer += 2 78 | // let qualifiedNameLength: Int = qualifiedNameLengthData.toInteger(endian: .big) 79 | // guard let qualifiedName = data[safe: pointer..<(pointer + qualifiedNameLength)] else { return nil } 80 | // pointer += qualifiedNameLength 81 | 82 | // attested = AttestationInformation(name: attestedName, qualifiedName: qualifiedName) 83 | // } 84 | 85 | // func verify() throws { 86 | // let tpmGeneratedValue = 0xFF544347 87 | // guard magic.toInteger(endian: .big) == tpmGeneratedValue else { 88 | // throw CertInfoError.magicInvalid 89 | // } 90 | 91 | // let tpmStAttestCertify = 0x8017 92 | // guard type.toInteger(endian: .big) == tpmStAttestCertify else { 93 | // throw CertInfoError.typeInvalid 94 | // } 95 | 96 | // throw CertInfoError.tpmImplementationIsWIP 97 | // } 98 | // } 99 | 100 | // enum PubAreaParameters { 101 | // case rsa(PubAreaParametersRSA) 102 | // case ecc (PubAreaParametersECC) 103 | // } 104 | 105 | // struct PubArea { 106 | // let type: Data 107 | // let nameAlg: Data 108 | // let objectAttributes: Data 109 | // let authPolicy: Data 110 | // let parameters: PubAreaParameters 111 | // let unique: PubAreaUnique 112 | 113 | // let mappedType: TPMAlg 114 | 115 | // init?(from data: Data) { 116 | // var pointer = 0 117 | 118 | // guard let type = data.safeSlice(length: 2, using: &pointer), 119 | // let mappedType = TPMAlg(from: type), 120 | // let nameAlg = data.safeSlice(length: 2, using: &pointer), 121 | // let objectAttributes = data.safeSlice(length: 4, using: &pointer), 122 | // let authPolicyLength: Int = data.safeSlice(length: 2, using: &pointer)?.toInteger(endian: .big), 123 | // let authPolicy = data.safeSlice(length: authPolicyLength, using: &pointer) else { 124 | // return nil 125 | // } 126 | 127 | // self.type = type 128 | // self.nameAlg = nameAlg 129 | // self.objectAttributes = objectAttributes 130 | // self.authPolicy = authPolicy 131 | 132 | // self.mappedType = mappedType 133 | 134 | // switch mappedType { 135 | // case .rsa: 136 | // guard let rsa = data.safeSlice(length: 10, using: &pointer), 137 | // let parameters = PubAreaParametersRSA(from: rsa) else { return nil } 138 | // self.parameters = .rsa(parameters) 139 | // case .ecc: 140 | // guard let ecc = data.safeSlice(length: 8, using: &pointer), 141 | // let parameters = PubAreaParametersECC(from: ecc) else { return nil } 142 | // self.parameters = .ecc(parameters) 143 | // default: 144 | // return nil 145 | // } 146 | 147 | // guard data.count >= pointer, 148 | // let unique = PubAreaUnique(from: data[pointer...], algorithm: mappedType) else { 149 | // return nil 150 | // } 151 | 152 | // self.unique = unique 153 | // } 154 | // } 155 | // } 156 | 157 | // extension TPMAttestation { 158 | // enum TPMAlg: String { 159 | // case error = "TPM_ALG_ERROR" 160 | // case rsa = "TPM_ALG_RSA" 161 | // case sha1 = "TPM_ALG_SHA1" 162 | // case hmac = "TPM_ALG_HMAC" 163 | // case aes = "TPM_ALG_AES" 164 | // case mgf1 = "TPM_ALG_MGF1" 165 | // case keyedhash = "TPM_ALG_KEYEDHASH" 166 | // case xor = "TPM_ALG_XOR" 167 | // case sha256 = "TPM_ALG_SHA256" 168 | // case sha384 = "TPM_ALG_SHA384" 169 | // case sha512 = "TPM_ALG_SHA512" 170 | // case null = "TPM_ALG_NULL" 171 | // case sm3256 = "TPM_ALG_SM3_256" 172 | // case sm4 = "TPM_ALG_SM4" 173 | // case rsassa = "TPM_ALG_RSASSA" 174 | // case rsaes = "TPM_ALG_RSAES" 175 | // case rsapss = "TPM_ALG_RSAPSS" 176 | // case oaep = "TPM_ALG_OAEP" 177 | // case ecdsa = "TPM_ALG_ECDSA" 178 | // case ecdh = "TPM_ALG_ECDH" 179 | // case ecdaa = "TPM_ALG_ECDAA" 180 | // case sm2 = "TPM_ALG_SM2" 181 | // case ecschnorr = "TPM_ALG_ECSCHNORR" 182 | // case ecmqv = "TPM_ALG_ECMQV" 183 | // case kdf1Sp80056a = "TPM_ALG_KDF1_SP800_56A" 184 | // case kdf2 = "TPM_ALG_KDF2" 185 | // case kdf1Sp800108 = "TPM_ALG_KDF1_SP800_108" 186 | // case ecc = "TPM_ALG_ECC" 187 | // case symcipher = "TPM_ALG_SYMCIPHER" 188 | // case camellia = "TPM_ALG_CAMELLIA" 189 | // case ctr = "TPM_ALG_CTR" 190 | // case ofb = "TPM_ALG_OFB" 191 | // case cbc = "TPM_ALG_CBC" 192 | // case cfb = "TPM_ALG_CFB" 193 | // case ecb = "TPM_ALG_ECB" 194 | 195 | // // swiftlint:disable:next cyclomatic_complexity function_body_length 196 | // init?(from data: Data) { 197 | // let bytes = [UInt8](data) 198 | // switch bytes { 199 | // case [0x00, 0x00]: 200 | // self = .error 201 | // case [0x00, 0x01]: 202 | // self = .rsa 203 | // case [0x00, 0x04]: 204 | // self = .sha1 205 | // case [0x00, 0x05]: 206 | // self = .hmac 207 | // case [0x00, 0x06]: 208 | // self = .aes 209 | // case [0x00, 0x07]: 210 | // self = .mgf1 211 | // case [0x00, 0x08]: 212 | // self = .keyedhash 213 | // case [0x00, 0x0a]: 214 | // self = .xor 215 | // case [0x00, 0x0b]: 216 | // self = .sha256 217 | // case [0x00, 0x0c]: 218 | // self = .sha384 219 | // case [0x00, 0x0d]: 220 | // self = .sha512 221 | // case [0x00, 0x10]: 222 | // self = .null 223 | // case [0x00, 0x12]: 224 | // self = .sm3256 225 | // case [0x00, 0x13]: 226 | // self = .sm4 227 | // case [0x00, 0x14]: 228 | // self = .rsassa 229 | // case [0x00, 0x15]: 230 | // self = .rsaes 231 | // case [0x00, 0x16]: 232 | // self = .rsapss 233 | // case [0x00, 0x17]: 234 | // self = .oaep 235 | // case [0x00, 0x18]: 236 | // self = .ecdsa 237 | // case [0x00, 0x19]: 238 | // self = .ecdh 239 | // case [0x00, 0x1a]: 240 | // self = .ecdaa 241 | // case [0x00, 0x1b]: 242 | // self = .sm2 243 | // case [0x00, 0x1c]: 244 | // self = .ecschnorr 245 | // case [0x00, 0x1d]: 246 | // self = .ecmqv 247 | // case [0x00, 0x20]: 248 | // self = .kdf1Sp80056a 249 | // case [0x00, 0x21]: 250 | // self = .kdf2 251 | // case [0x00, 0x22]: 252 | // self = .kdf1Sp800108 253 | // case [0x00, 0x23]: 254 | // self = .ecc 255 | // case [0x00, 0x25]: 256 | // self = .symcipher 257 | // case [0x00, 0x26]: 258 | // self = .camellia 259 | // case [0x00, 0x40]: 260 | // self = .ctr 261 | // case [0x00, 0x41]: 262 | // self = .ofb 263 | // case [0x00, 0x42]: 264 | // self = .cbc 265 | // case [0x00, 0x43]: 266 | // self = .cfb 267 | // case [0x00, 0x44]: 268 | // self = .ecb 269 | // default: 270 | // return nil 271 | // } 272 | // } 273 | // } 274 | // } 275 | 276 | // extension TPMAttestation { 277 | // enum ECCCurve: String { 278 | // case none = "NONE" 279 | // case nistP192 = "NIST_P192" 280 | // case nistP224 = "NIST_P224" 281 | // case nistP256 = "NIST_P256" 282 | // case nistP384 = "NIST_P384" 283 | // case nistP521 = "NIST_P521" 284 | // case bnP256 = "BN_P256" 285 | // case bnP638 = "BN_P638" 286 | // case sm2P256 = "SM2_P256" 287 | 288 | // init?(from data: Data) { 289 | // let bytes = [UInt8](data) 290 | // switch bytes { 291 | // case [0x00, 0x00]: 292 | // self = .none 293 | // case [0x00, 0x01]: 294 | // self = .nistP192 295 | // case [0x00, 0x02]: 296 | // self = .nistP224 297 | // case [0x00, 0x03]: 298 | // self = .nistP256 299 | // case [0x00, 0x04]: 300 | // self = .nistP384 301 | // case [0x00, 0x05]: 302 | // self = .nistP521 303 | // case [0x00, 0x10]: 304 | // self = .bnP256 305 | // case [0x00, 0x11]: 306 | // self = .bnP638 307 | // case [0x00, 0x20]: 308 | // self = .sm2P256 309 | // default: 310 | // return nil 311 | // } 312 | // } 313 | // } 314 | // } 315 | 316 | // extension TPMAttestation { 317 | // struct PubAreaParametersRSA { 318 | // let symmetric: TPMAlg 319 | // let scheme: TPMAlg 320 | // let key: Data 321 | // let exponent: Data 322 | 323 | // init?(from data: Data) { 324 | // guard let symmetricData = data[safe: 0..<2], 325 | // let symmetric = TPMAlg(from: symmetricData), 326 | // let schemeData = data[safe: 2..<4], 327 | // let scheme = TPMAlg(from: schemeData), 328 | // let key = data[safe: 4..<6], 329 | // let exponent = data[safe: 6..<10] else { 330 | // return nil 331 | // } 332 | 333 | // self.symmetric = symmetric 334 | // self.scheme = scheme 335 | // self.key = key 336 | // self.exponent = exponent 337 | // } 338 | // } 339 | // } 340 | 341 | // extension TPMAttestation { 342 | // struct PubAreaParametersECC { 343 | // let symmetric: TPMAlg 344 | // let scheme: TPMAlg 345 | // let curveID: ECCCurve 346 | // let kdf: TPMAlg 347 | 348 | // init?(from data: Data) { 349 | // guard let symmetricData = data[safe: 0..<2], 350 | // let symmetric = TPMAlg(from: symmetricData), 351 | // let schemeData = data[safe: 2..<4], 352 | // let scheme = TPMAlg(from: schemeData), 353 | // let curveIDData = data[safe: 4..<6], 354 | // let curveID = ECCCurve(from: curveIDData), 355 | // let kdfData = data[safe: 6..<8], 356 | // let kdf = TPMAlg(from: kdfData) else { 357 | // return nil 358 | // } 359 | 360 | // self.symmetric = symmetric 361 | // self.scheme = scheme 362 | // self.curveID = curveID 363 | // self.kdf = kdf 364 | // } 365 | // } 366 | // } 367 | 368 | // extension TPMAttestation { 369 | // struct PubAreaUnique { 370 | // let data: Data 371 | 372 | // init?(from data: Data, algorithm: TPMAlg) { 373 | // switch algorithm { 374 | // case .rsa: 375 | // guard let uniqueLength: Int = data[safe: 0..<2]?.toInteger(endian: .big), 376 | // let rsaUnique = data[safe: 2..<(2 + uniqueLength)] else { 377 | // return nil 378 | // } 379 | // self.data = rsaUnique 380 | // case .ecc: 381 | // var pointer = 0 382 | // guard let uniqueXLength: Int = data.safeSlice(length: 2, using: &pointer)?.toInteger(endian: .big), 383 | // let uniqueX = data.safeSlice(length: uniqueXLength, using: &pointer), 384 | // let uniqueYLength: Int = data.safeSlice(length: 2, using: &pointer)?.toInteger(endian: .big), 385 | // let uniqueY = data.safeSlice(length: uniqueYLength, using: &pointer) else { 386 | // return nil 387 | // } 388 | // self.data = uniqueX + uniqueY 389 | // default: 390 | // return nil 391 | // } 392 | // } 393 | // } 394 | // } 395 | 396 | // extension COSECurve { 397 | // init?(from eccCurve: TPMAttestation.ECCCurve) { 398 | // switch eccCurve { 399 | // case .nistP256, .bnP256, .sm2P256: 400 | // self = .p256 401 | // case .nistP384: 402 | // self = .p384 403 | // case .nistP521: 404 | // self = .p521 405 | // default: 406 | // return nil 407 | // } 408 | // } 409 | // } 410 | -------------------------------------------------------------------------------- /Sources/WebAuthn/Ceremonies/Registration/Formats/TPMAttestation.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2023 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | // 🚨 WIP 15 | 16 | // import Foundation 17 | // import SwiftCBOR 18 | 19 | // /// 🚨 WIP 20 | // struct TPMAttestation { 21 | // enum TPMAttestationError: Error { 22 | // case pubAreaInvalid 23 | // case certInfoInvalid 24 | // case invalidAlg 25 | // case invalidVersion 26 | // case invalidX5c 27 | // case invalidPublicKey 28 | // case pubAreaExponentDoesNotMatchPubKeyExponent 29 | // case invalidPubAreaCurve 30 | // case extraDataDoesNotMatchAttToBeSignedHash 31 | // } 32 | 33 | // static func verify( 34 | // attStmt: CBOR, 35 | // authenticatorData: Data, 36 | // attestedCredentialData: AttestedCredentialData, 37 | // clientDataHash: Data, 38 | // credentialPublicKey: CredentialPublicKey, 39 | // pemRootCertificates: [Data] 40 | // ) throws { 41 | // // Verify version 42 | // guard let verCBOR = attStmt["ver"], 43 | // case let .utf8String(ver) = verCBOR, 44 | // ver == "2.0" else { 45 | // throw TPMAttestationError.invalidVersion 46 | // } 47 | 48 | // // Verify certificate chain 49 | // guard let x5cCBOR = attStmt["x5c"], 50 | // case let .array(x5cArray) = x5cCBOR, 51 | // case let .byteString(aikCert) = x5cArray.first else { 52 | // throw TPMAttestationError.invalidX5c 53 | // } 54 | // let certificateChain = try x5cArray[1...].map { 55 | // guard case let .byteString(caCert) = $0 else { throw TPMAttestationError.invalidX5c } 56 | // return caCert 57 | // } 58 | 59 | // // TODO: Validate certificate chain 60 | // // try CertificateChain.validate( 61 | // // x5c: aikCert + certificateChain, 62 | // // pemRootCertificates: pemRootCertificates 63 | // // ) 64 | 65 | // // Verify pubArea 66 | // guard let pubAreaCBOR = attStmt["pubArea"], 67 | // case let .byteString(pubArea) = pubAreaCBOR, 68 | // let pubArea = PubArea(from: Data(pubArea)) else { 69 | // throw TPMAttestationError.pubAreaInvalid 70 | // } 71 | // switch pubArea.parameters { 72 | // case let .rsa(rsaParameters): 73 | // guard case let .rsa(rsaPublicKeyData) = credentialPublicKey, 74 | // pubArea.unique.data == rsaPublicKeyData.n else { 75 | // throw TPMAttestationError.invalidPublicKey 76 | // } 77 | // var pubAreaExponent: Int = rsaParameters.exponent.toInteger(endian: .big) 78 | // if pubAreaExponent == 0 { 79 | // // "When zero, indicates that the exponent is the default of 2^16 + 1" 80 | // pubAreaExponent = 65537 81 | // } 82 | // 83 | // let pubKeyExponent: Int = rsaPublicKeyData.e.toInteger(endian: .big) 84 | // guard pubAreaExponent == pubKeyExponent else { 85 | // throw TPMAttestationError.pubAreaExponentDoesNotMatchPubKeyExponent 86 | // } 87 | // case let .ecc(eccParameters): 88 | // guard case let .ec2(ec2PublicKeyData) = credentialPublicKey, 89 | // pubArea.unique.data == ec2PublicKeyData.rawRepresentation else { 90 | // throw TPMAttestationError.invalidPublicKey 91 | // } 92 | // 93 | // guard let pubAreaCrv = COSECurve(from: eccParameters.curveID), 94 | // pubAreaCrv == ec2PublicKeyData.curve else { 95 | // throw TPMAttestationError.invalidPubAreaCurve 96 | // } 97 | // } 98 | 99 | // // Verify certInfo 100 | // guard let certInfoCBOR = attStmt["certInfo"], 101 | // case let .byteString(certInfo) = certInfoCBOR, 102 | // let parsedCertInfo = CertInfo(fromBytes: Data(certInfo)) else { 103 | // throw TPMAttestationError.certInfoInvalid 104 | // } 105 | // try parsedCertInfo.verify() 106 | 107 | // let attToBeSigned = authenticatorData + clientDataHash 108 | 109 | // guard let algCBOR = attStmt["alg"], 110 | // case let .negativeInt(algorithmNegative) = algCBOR, 111 | // let alg = COSEAlgorithmIdentifier(rawValue: -1 - Int(algorithmNegative)) else { 112 | // throw TPMAttestationError.invalidAlg 113 | // } 114 | 115 | // guard alg.hashAndCompare(data: attToBeSigned, to: parsedCertInfo.extraData) else { 116 | // throw TPMAttestationError.extraDataDoesNotMatchAttToBeSignedHash 117 | // } 118 | // } 119 | // } 120 | -------------------------------------------------------------------------------- /Sources/WebAuthn/Ceremonies/Registration/PublicKeyCredentialCreationOptions.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2022 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import Foundation 15 | 16 | /// The `PublicKeyCredentialCreationOptions` gets passed to the WebAuthn API (`navigator.credentials.create()`) 17 | /// 18 | /// Generally this should not be created manually. Instead use `RelyingParty.beginRegistration()`. When encoding using 19 | /// `Encodable` byte arrays are base64url encoded. 20 | /// 21 | /// - SeeAlso: https://www.w3.org/TR/webauthn-2/#dictionary-makecredentialoptions 22 | public struct PublicKeyCredentialCreationOptions: Encodable, Sendable { 23 | /// A byte array randomly generated by the Relying Party. Should be at least 16 bytes long to ensure sufficient 24 | /// entropy. 25 | /// 26 | /// The Relying Party should store the challenge temporarily until the registration flow is complete. When 27 | /// encoding using `Encodable`, the challenge is base64url encoded. 28 | public let challenge: [UInt8] 29 | 30 | /// Contains names and an identifier for the user account performing the registration 31 | public let user: PublicKeyCredentialUserEntity 32 | 33 | /// Contains a name and an identifier for the Relying Party responsible for the request 34 | public let relyingParty: PublicKeyCredentialRelyingPartyEntity 35 | 36 | /// A list of key types and signature algorithms the Relying Party supports. Ordered from most preferred to least 37 | /// preferred. 38 | public let publicKeyCredentialParameters: [PublicKeyCredentialParameters] 39 | 40 | /// A time, in seconds, that the caller is willing to wait for the call to complete. This is treated as a 41 | /// hint, and may be overridden by the client. 42 | /// 43 | /// - Note: When encoded, this value is represented in milleseconds as a ``UInt32``. 44 | public let timeout: Duration? 45 | 46 | /// Sets the Relying Party's preference for attestation conveyance. At the time of writing only `none` is 47 | /// supported. 48 | public let attestation: AttestationConveyancePreference 49 | 50 | public func encode(to encoder: any Encoder) throws { 51 | var container = encoder.container(keyedBy: CodingKeys.self) 52 | 53 | try container.encode(challenge.base64URLEncodedString(), forKey: .challenge) 54 | try container.encode(user, forKey: .user) 55 | try container.encode(relyingParty, forKey: .relyingParty) 56 | try container.encode(publicKeyCredentialParameters, forKey: .publicKeyCredentialParameters) 57 | try container.encodeIfPresent(timeout?.milliseconds, forKey: .timeout) 58 | try container.encode(attestation, forKey: .attestation) 59 | } 60 | 61 | private enum CodingKeys: String, CodingKey { 62 | case challenge 63 | case user 64 | case relyingParty = "rp" 65 | case publicKeyCredentialParameters = "pubKeyCredParams" 66 | case timeout 67 | case attestation 68 | } 69 | } 70 | 71 | // MARK: - Credential parameters 72 | /// From §5.3 (https://w3c.github.io/TR/webauthn/#dictionary-credential-params) 73 | public struct PublicKeyCredentialParameters: Equatable, Encodable, Sendable { 74 | /// The type of credential to be created. At the time of writing always ``CredentialType/publicKey``. 75 | public let type: CredentialType 76 | /// The cryptographic signature algorithm with which the newly generated credential will be used, and thus also 77 | /// the type of asymmetric key pair to be generated, e.g., RSA or Elliptic Curve. 78 | public let alg: COSEAlgorithmIdentifier 79 | 80 | /// Creates a new `PublicKeyCredentialParameters` instance. 81 | /// 82 | /// - Parameters: 83 | /// - type: The type of credential to be created. At the time of writing always ``CredentialType/publicKey``. 84 | /// - alg: The cryptographic signature algorithm to be used with the newly generated credential. 85 | /// For example RSA or Elliptic Curve. 86 | public init(type: CredentialType = .publicKey, alg: COSEAlgorithmIdentifier) { 87 | self.type = type 88 | self.alg = alg 89 | } 90 | } 91 | 92 | extension Array where Element == PublicKeyCredentialParameters { 93 | /// A list of `PublicKeyCredentialParameters` Swift WebAuthn currently supports. 94 | public static var supported: [Element] { 95 | COSEAlgorithmIdentifier.allCases.map { 96 | Element.init(type: .publicKey, alg: $0) 97 | } 98 | } 99 | } 100 | 101 | // MARK: - Credential entities 102 | 103 | /// From §5.4.2 (https://www.w3.org/TR/webauthn/#sctn-rp-credential-params). 104 | /// The PublicKeyCredentialRelyingPartyEntity dictionary is used to supply additional Relying Party attributes when 105 | /// creating a new credential. 106 | public struct PublicKeyCredentialRelyingPartyEntity: Encodable, Sendable { 107 | /// A unique identifier for the Relying Party entity. 108 | public let id: String 109 | 110 | /// A human-readable identifier for the Relying Party, intended only for display. For example, "ACME Corporation", 111 | /// "Wonderful Widgets, Inc." or "ОАО Примертех". 112 | public let name: String 113 | 114 | } 115 | 116 | /// From §5.4.3 (https://www.w3.org/TR/webauthn/#dictionary-user-credential-params) 117 | /// The PublicKeyCredentialUserEntity dictionary is used to supply additional user account attributes when 118 | /// creating a new credential. 119 | /// 120 | /// When encoding using `Encodable`, `id` is base64url encoded. 121 | public struct PublicKeyCredentialUserEntity: Encodable, Sendable { 122 | /// Generated by the Relying Party, unique to the user account, and must not contain personally identifying 123 | /// information about the user. 124 | /// 125 | /// When encoding this is base64url encoded. 126 | public let id: [UInt8] 127 | 128 | /// A human-readable identifier for the user account, intended only for display. It helps the user to 129 | /// distinguish between user accounts with similar `displayName`s. For example, two different user accounts 130 | /// might both have the same `displayName`, "Alex P. Müller", but might have different `name` values "alexm", 131 | /// "alex.mueller@example.com" or "+14255551234". 132 | public let name: String 133 | 134 | /// A human-readable name for the user account, intended only for display. For example, "Alex P. Müller" or 135 | /// "田中 倫" 136 | public let displayName: String 137 | 138 | /// Creates a new ``PublicKeyCredentialUserEntity`` from id, name and displayName 139 | public init(id: [UInt8], name: String, displayName: String) { 140 | self.id = id 141 | self.name = name 142 | self.displayName = displayName 143 | } 144 | 145 | public func encode(to encoder: any Encoder) throws { 146 | var container = encoder.container(keyedBy: CodingKeys.self) 147 | 148 | try container.encode(id.base64URLEncodedString(), forKey: .id) 149 | try container.encode(name, forKey: .name) 150 | try container.encode(displayName, forKey: .displayName) 151 | } 152 | 153 | private enum CodingKeys: String, CodingKey { 154 | case id 155 | case name 156 | case displayName 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /Sources/WebAuthn/Ceremonies/Registration/RegistrationCredential.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2022 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import Foundation 15 | import Crypto 16 | 17 | /// The unprocessed response received from `navigator.credentials.create()`. 18 | /// 19 | /// When decoding using `Decodable`, the `rawID` is decoded from base64url to bytes. 20 | public struct RegistrationCredential: Sendable { 21 | /// The credential ID of the newly created credential. 22 | public let id: URLEncodedBase64 23 | 24 | /// Value will always be ``CredentialType/publicKey`` (for now) 25 | public let type: CredentialType 26 | 27 | /// The raw credential ID of the newly created credential. 28 | public let rawID: [UInt8] 29 | 30 | /// The attestation response from the authenticator. 31 | public let attestationResponse: AuthenticatorAttestationResponse 32 | } 33 | 34 | extension RegistrationCredential: Decodable { 35 | public init(from decoder: any Decoder) throws { 36 | let container = try decoder.container(keyedBy: CodingKeys.self) 37 | 38 | id = try container.decode(URLEncodedBase64.self, forKey: .id) 39 | type = try container.decode(CredentialType.self, forKey: .type) 40 | guard let rawID = try container.decode(URLEncodedBase64.self, forKey: .rawID).decodedBytes else { 41 | throw DecodingError.dataCorruptedError( 42 | forKey: .rawID, 43 | in: container, 44 | debugDescription: "Failed to decode base64url encoded rawID into bytes" 45 | ) 46 | } 47 | self.rawID = rawID 48 | attestationResponse = try container.decode(AuthenticatorAttestationResponse.self, forKey: .attestationResponse) 49 | } 50 | 51 | private enum CodingKeys: String, CodingKey { 52 | case id 53 | case type 54 | case rawID = "rawId" 55 | case attestationResponse = "response" 56 | } 57 | } 58 | 59 | /// The processed response received from `navigator.credentials.create()`. 60 | struct ParsedCredentialCreationResponse { 61 | let id: URLEncodedBase64 62 | let rawID: Data 63 | /// Value will always be ``CredentialType/publicKey`` (for now) 64 | let type: CredentialType 65 | let raw: AuthenticatorAttestationResponse 66 | let response: ParsedAuthenticatorAttestationResponse 67 | 68 | /// Create a `ParsedCredentialCreationResponse` from a raw `CredentialCreationResponse`. 69 | init(from rawResponse: RegistrationCredential) throws { 70 | id = rawResponse.id 71 | rawID = Data(rawResponse.rawID) 72 | 73 | guard rawResponse.type == .publicKey 74 | else { throw WebAuthnError.invalidCredentialCreationType } 75 | type = rawResponse.type 76 | 77 | raw = rawResponse.attestationResponse 78 | response = try ParsedAuthenticatorAttestationResponse(from: raw) 79 | } 80 | 81 | // swiftlint:disable:next function_parameter_count 82 | func verify( 83 | storedChallenge: [UInt8], 84 | verifyUser: Bool, 85 | relyingPartyID: String, 86 | relyingPartyOrigin: String, 87 | supportedPublicKeyAlgorithms: [PublicKeyCredentialParameters], 88 | pemRootCertificatesByFormat: [AttestationFormat: [Data]] 89 | ) async throws -> AttestedCredentialData { 90 | // Step 7. - 9. 91 | try response.clientData.verify( 92 | storedChallenge: storedChallenge, 93 | ceremonyType: .create, 94 | relyingPartyOrigin: relyingPartyOrigin 95 | ) 96 | 97 | // Step 10. 98 | let hash = SHA256.hash(data: Data(raw.clientDataJSON)) 99 | 100 | // CBOR decoding happened already. Skipping Step 11. 101 | 102 | // Step 12. - 17. 103 | let attestedCredentialData = try await response.attestationObject.verify( 104 | relyingPartyID: relyingPartyID, 105 | verificationRequired: verifyUser, 106 | clientDataHash: hash, 107 | supportedPublicKeyAlgorithms: supportedPublicKeyAlgorithms, 108 | pemRootCertificatesByFormat: pemRootCertificatesByFormat 109 | ) 110 | 111 | // Step 23. 112 | guard rawID.count <= 1023 else { 113 | throw WebAuthnError.credentialRawIDTooLong 114 | } 115 | 116 | return attestedCredentialData 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /Sources/WebAuthn/Ceremonies/Shared/AuthenticatorAttachment.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2024 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | /// An authenticators' attachment modalities. 15 | /// 16 | /// Relying Parties use this to express a preferred authenticator attachment modality when registering a credential, and clients use this to report the authenticator attachment modality used to complete a registration or authentication ceremony. 17 | /// - SeeAlso: [WebAuthn Level 3 Editor's Draft §5.4.5. Authenticator Attachment Enumeration (enum AuthenticatorAttachment)](https://w3c.github.io/webauthn/#enum-attachment) 18 | /// - SeeAlso: [WebAuthn Level 3 Editor's Draft §6.2.1. Authenticator Attachment Modality](https://w3c.github.io/webauthn/#sctn-authenticator-attachment-modality) 19 | /// 20 | public struct AuthenticatorAttachment: UnreferencedStringEnumeration, Sendable { 21 | public var rawValue: String 22 | public init(_ rawValue: String) { 23 | self.rawValue = rawValue 24 | } 25 | 26 | /// A platform authenticator is attached using a client device-specific transport, called platform attachment, and is usually not removable from the client device. A public key credential bound to a platform authenticator is called a platform credential. 27 | /// - SeeAlso: [WebAuthn Level 3 Editor's Draft §6.2.1. Authenticator Attachment Modality](https://w3c.github.io/webauthn/#platform-attachment) 28 | public static let platform: Self = "platform" 29 | 30 | /// A roaming authenticator is attached using cross-platform transports, called cross-platform attachment. Authenticators of this class are removable from, and can "roam" between, client devices. A public key credential bound to a roaming authenticator is called a roaming credential. 31 | /// - SeeAlso: [WebAuthn Level 3 Editor's Draft §6.2.1. Authenticator Attachment Modality](https://w3c.github.io/webauthn/#cross-platform-attachment) 32 | public static let crossPlatform: Self = "cross-platform" 33 | } 34 | -------------------------------------------------------------------------------- /Sources/WebAuthn/Ceremonies/Shared/AuthenticatorAttestationGloballyUniqueID.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2024 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import Foundation 15 | 16 | /// A globally unique ID identifying an authenticator. 17 | /// 18 | /// Each authenticator has an Authenticator Attestation Globally Unique Identifier or **AAGUID**, which is a 128-bit identifier indicating the type (e.g. make and model) of the authenticator. The AAGUID MUST be chosen by its maker to be identical across all substantially identical authenticators made by that maker, and different (with high probability) from the AAGUIDs of all other types of authenticators. The AAGUID for a given type of authenticator SHOULD be randomly generated to ensure this. 19 | /// 20 | /// The Relying Party MAY use the AAGUID to infer certain properties of the authenticator, such as certification level and strength of key protection, using information from other sources. The Relying Party MAY use the AAGUID to attempt to identify the maker of the authenticator without requesting and verifying attestation, but the AAGUID is not provably authentic without attestation. 21 | /// - SeeAlso: [WebAuthn Leven 3 Editor's Draft §6. WebAuthn Authenticator Model](https://w3c.github.io/webauthn/#aaguid) 22 | public struct AuthenticatorAttestationGloballyUniqueID: Hashable, Sendable { 23 | /// The underlying UUID for the authenticator. 24 | public let id: UUID 25 | 26 | /// Initialize an AAGUID with a UUID. 27 | @inlinable 28 | public init(uuid: UUID) { 29 | self.id = uuid 30 | } 31 | 32 | /// Initialize an AAGUID with a byte sequence. 33 | /// 34 | /// This sequence must be of length ``AuthenticatorAttestationGloballyUniqueID/size``. 35 | @inlinable 36 | public init?(bytes: some BidirectionalCollection) { 37 | let uuidSize = MemoryLayout.size 38 | assert(uuidSize == Self.size, "Size of uuid_t (\(uuidSize)) does not match Self.size (\(Self.size))!") 39 | guard bytes.count == uuidSize else { return nil } 40 | self.init(uuid: UUID(uuid: bytes.casting())) 41 | } 42 | 43 | /// Initialize an AAGUID with a string-based UUID. 44 | @inlinable 45 | public init?(uuidString: String) { 46 | guard let uuid = UUID(uuidString: uuidString) 47 | else { return nil } 48 | 49 | self.init(uuid: uuid) 50 | } 51 | 52 | /// Access the AAGUID as an encoded byte sequence. 53 | @inlinable 54 | public var bytes: [UInt8] { withUnsafeBytes(of: id) { Array($0) } } 55 | 56 | /// The identifier of an anonymized authenticator, set to a byte sequence of 16 zeros. 57 | public static let anonymous = AuthenticatorAttestationGloballyUniqueID(bytes: Array(repeating: 0, count: Self.size))! 58 | 59 | /// The byte length of an encoded identifer. 60 | public static let size: Int = 16 61 | } 62 | 63 | /// A shorthand for an ``AuthenticatorAttestationGloballyUniqueID`` 64 | public typealias AAGUID = AuthenticatorAttestationGloballyUniqueID 65 | 66 | extension AuthenticatorAttestationGloballyUniqueID: Codable { 67 | public init(from decoder: any Decoder) throws { 68 | let container = try decoder.singleValueContainer() 69 | id = try container.decode(UUID.self) 70 | } 71 | 72 | public func encode(to encoder: any Encoder) throws { 73 | var container = encoder.singleValueContainer() 74 | try container.encode(id) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/WebAuthn/Ceremonies/Shared/AuthenticatorData.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2022 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import Foundation 15 | import Crypto 16 | import SwiftCBOR 17 | 18 | /// Data created and/ or used by the authenticator during authentication/ registration. 19 | /// The data contains, for example, whether a user was present or verified. 20 | struct AuthenticatorData: Equatable, Sendable { 21 | let relyingPartyIDHash: [UInt8] 22 | let flags: AuthenticatorFlags 23 | let counter: UInt32 24 | /// For attestation signatures this value will be set. For assertion signatures not. 25 | let attestedData: AttestedCredentialData? 26 | let extData: [UInt8]? 27 | } 28 | 29 | extension AuthenticatorData { 30 | init(bytes: [UInt8]) throws(WebAuthnError) { 31 | let minAuthDataLength = 37 32 | guard bytes.count >= minAuthDataLength else { 33 | throw .authDataTooShort 34 | } 35 | 36 | let relyingPartyIDHash = Array(bytes[..<32]) 37 | let flags = AuthenticatorFlags(bytes[32]) 38 | let counter = UInt32(bigEndianBytes: bytes[33..<37]) 39 | 40 | var remainingCount = bytes.count - minAuthDataLength 41 | 42 | var attestedCredentialData: AttestedCredentialData? 43 | // For attestation signatures, the authenticator MUST set the AT flag and include the attestedCredentialData. 44 | if flags.attestedCredentialData { 45 | let minAttestedAuthLength = 37 + AAGUID.size + 2 46 | guard bytes.count > minAttestedAuthLength else { 47 | throw .attestedCredentialDataMissing 48 | } 49 | let (data, length) = try Self.parseAttestedData(bytes) 50 | attestedCredentialData = data 51 | remainingCount -= length 52 | // For assertion signatures, the AT flag MUST NOT be set and the attestedCredentialData MUST NOT be included. 53 | } else { 54 | if !flags.extensionDataIncluded && bytes.count != minAuthDataLength { 55 | throw .attestedCredentialFlagNotSet 56 | } 57 | } 58 | 59 | var extensionData: [UInt8]? 60 | if flags.extensionDataIncluded { 61 | guard remainingCount != 0 else { 62 | throw .extensionDataMissing 63 | } 64 | extensionData = Array(bytes[(bytes.count - remainingCount)...]) 65 | remainingCount -= extensionData!.count 66 | } 67 | 68 | guard remainingCount == 0 else { 69 | throw .leftOverBytesInAuthenticatorData 70 | } 71 | 72 | self.relyingPartyIDHash = relyingPartyIDHash 73 | self.flags = flags 74 | self.counter = counter 75 | self.attestedData = attestedCredentialData 76 | self.extData = extensionData 77 | 78 | } 79 | 80 | /// Parse and return the attested credential data and its length. 81 | /// 82 | /// This is assumed to take place after the first 37 bytes of `data`, which is always of fixed size. 83 | /// - SeeAlso: [WebAuthn Level 3 Editor's Draft §6.5.1. Attested Credential Data]( https://w3c.github.io/webauthn/#sctn-attested-credential-data) 84 | private static func parseAttestedData(_ data: [UInt8]) throws(WebAuthnError) -> (AttestedCredentialData, Int) { 85 | /// **aaguid** (16): The AAGUID of the authenticator. 86 | guard let aaguid = AAGUID(bytes: data[37..<(37 + AAGUID.size)]) // Bytes [37-52] 87 | else { throw .attestedCredentialDataMissing } 88 | 89 | /// **credentialIdLength** (2): Byte length L of credentialId, 16-bit unsigned big-endian integer. Value MUST be ≤ 1023. 90 | let idLengthBytes = data[53..<55] // Length is 2 bytes 91 | let idLengthData = Data(idLengthBytes) 92 | let idLength = UInt16(bigEndianBytes: idLengthData) 93 | 94 | guard idLength <= 1023 95 | else { throw .credentialIDTooLong } 96 | 97 | let credentialIDEndIndex = Int(idLength) + 55 98 | guard data.count >= credentialIDEndIndex 99 | else { throw .credentialIDTooShort } 100 | 101 | /// **credentialId** (L): Credential ID 102 | let credentialID = data[55.. 129 | 130 | init(_ slice: ArraySlice) { 131 | self.slice = slice 132 | } 133 | 134 | /// The remaining bytes in the original data buffer. 135 | var remainingBytes: Int { slice.count } 136 | 137 | func popByte() throws(CBORError) -> UInt8 { 138 | if slice.count < 1 { throw .unfinishedSequence } 139 | return slice.removeFirst() 140 | } 141 | 142 | func popBytes(_ n: Int) throws(CBORError) -> ArraySlice { 143 | if slice.count < n { throw .unfinishedSequence } 144 | let result = slice.prefix(n) 145 | slice = slice.dropFirst(n) 146 | return result 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /Sources/WebAuthn/Ceremonies/Shared/AuthenticatorFlags.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2022 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | struct AuthenticatorFlags: Equatable, Sendable { 15 | 16 | /** 17 | Taken from https://w3c.github.io/webauthn/#sctn-authenticator-data 18 | Bit 0: User Present Result 19 | Bit 1: Reserved for future use 20 | Bit 2: User Verified Result 21 | Bits 3-5: Reserved for future use 22 | Bit 6: Attested credential data included 23 | Bit 7: Extension data include 24 | */ 25 | 26 | enum Bit: UInt8 { 27 | case userPresent = 0 28 | case userVerified = 2 29 | case backupEligible = 3 30 | case backupState = 4 31 | case attestedCredentialDataIncluded = 6 32 | case extensionDataIncluded = 7 33 | } 34 | 35 | let userPresent: Bool 36 | let userVerified: Bool 37 | let isBackupEligible: Bool 38 | let isCurrentlyBackedUp: Bool 39 | let attestedCredentialData: Bool 40 | let extensionDataIncluded: Bool 41 | 42 | var deviceType: VerifiedAuthentication.CredentialDeviceType { 43 | isBackupEligible ? .multiDevice : .singleDevice 44 | } 45 | 46 | static func isFlagSet(on byte: UInt8, at position: Bit) -> Bool { 47 | (byte & (1 << position.rawValue)) != 0 48 | } 49 | } 50 | 51 | extension AuthenticatorFlags { 52 | init(_ byte: UInt8) { 53 | userPresent = Self.isFlagSet(on: byte, at: .userPresent) 54 | userVerified = Self.isFlagSet(on: byte, at: .userVerified) 55 | isBackupEligible = Self.isFlagSet(on: byte, at: .backupEligible) 56 | isCurrentlyBackedUp = Self.isFlagSet(on: byte, at: .backupState) 57 | attestedCredentialData = Self.isFlagSet(on: byte, at: .attestedCredentialDataIncluded) 58 | extensionDataIncluded = Self.isFlagSet(on: byte, at: .extensionDataIncluded) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/WebAuthn/Ceremonies/Shared/COSE/COSEAlgorithmIdentifier.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2022 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import Foundation 15 | import Crypto 16 | 17 | /// COSEAlgorithmIdentifier From §5.10.5. A number identifying a cryptographic algorithm. The algorithm 18 | /// identifiers SHOULD be values registered in the IANA COSE Algorithms registry 19 | /// [https://www.w3.org/TR/webauthn/#biblio-iana-cose-algs-reg], for instance, -7 for "ES256" and -257 for "RS256". 20 | public enum COSEAlgorithmIdentifier: Int, RawRepresentable, CaseIterable, Encodable, Sendable { 21 | /// AlgES256 ECDSA with SHA-256 22 | case algES256 = -7 23 | /// AlgES384 ECDSA with SHA-384 24 | case algES384 = -35 25 | /// AlgES512 ECDSA with SHA-512 26 | case algES512 = -36 27 | 28 | /// AlgRS1 RSASSA-PKCS1-v1_5 with SHA-1 29 | case algRS1 = -65535 30 | /// AlgRS256 RSASSA-PKCS1-v1_5 with SHA-256 31 | case algRS256 = -257 32 | /// AlgRS384 RSASSA-PKCS1-v1_5 with SHA-384 33 | case algRS384 = -258 34 | /// AlgRS512 RSASSA-PKCS1-v1_5 with SHA-512 35 | case algRS512 = -259 36 | /// AlgPS256 RSASSA-PSS with SHA-256 37 | case algPS256 = -37 38 | /// AlgPS384 RSASSA-PSS with SHA-384 39 | case algPS384 = -38 40 | /// AlgPS512 RSASSA-PSS with SHA-512 41 | case algPS512 = -39 42 | // /// AlgEdDSA EdDSA 43 | // case algEdDSA = -8 44 | 45 | func hashAndCompare(data: Data, to compareHash: Data) -> Bool { 46 | switch self { 47 | case .algES256, .algRS256, .algPS256: 48 | return SHA256.hash(data: data) == compareHash 49 | case .algES384, .algRS384, .algPS384: 50 | return SHA384.hash(data: data) == compareHash 51 | case .algES512, .algRS512, .algPS512: 52 | return SHA512.hash(data: data) == compareHash 53 | case .algRS1: 54 | return Insecure.SHA1.hash(data: data) == compareHash 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/WebAuthn/Ceremonies/Shared/COSE/COSECurve.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2023 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | enum COSECurve: UInt64, Sendable { 15 | /// EC2, NIST P-256 also known as secp256r1 16 | case p256 = 1 17 | /// EC2, NIST P-384 also known as secp384r1 18 | case p384 = 2 19 | /// EC2, NIST P-521 also known as secp521r1 20 | case p521 = 3 21 | /// OKP, Ed25519 for use w/ EdDSA only 22 | case ed25519 = 6 23 | } 24 | -------------------------------------------------------------------------------- /Sources/WebAuthn/Ceremonies/Shared/COSE/COSEKey.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2023 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import SwiftCBOR 15 | 16 | enum COSEKey: Sendable { 17 | // swiftlint:disable identifier_name 18 | case kty 19 | case alg 20 | 21 | // EC2, OKP 22 | case crv 23 | case x 24 | 25 | // EC2 26 | case y 27 | 28 | // RSA 29 | case n 30 | case e 31 | // swiftlint:enable identifier_name 32 | 33 | var cbor: CBOR { 34 | var value: Int 35 | switch self { 36 | case .kty: 37 | value = 1 38 | case .alg: 39 | value = 3 40 | case .crv: 41 | value = -1 42 | case .x: 43 | value = -2 44 | case .y: 45 | value = -3 46 | case .n: 47 | value = -1 48 | case .e: 49 | value = -2 50 | } 51 | if value < 0 { 52 | return .negativeInt(UInt64(abs(-1 - value))) 53 | } else { 54 | return .unsignedInt(UInt64(value)) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/WebAuthn/Ceremonies/Shared/COSE/COSEKeyType.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2022 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import Foundation 15 | 16 | /// The Key Type derived from the IANA COSE AuthData 17 | enum COSEKeyType: UInt64, RawRepresentable, Sendable { 18 | /// OctetKey is an Octet Key 19 | case octetKey = 1 20 | /// EllipticKey is an Elliptic Curve Public Key 21 | case ellipticKey = 2 22 | /// RSAKey is an RSA Public Key 23 | case rsaKey = 3 24 | } 25 | -------------------------------------------------------------------------------- /Sources/WebAuthn/Ceremonies/Shared/CollectedClientData.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2022 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import Foundation 15 | 16 | /// A parsed version of the `clientDataJSON` received from the authenticator. The `clientDataJSON` is a 17 | /// representation of the options we passed to the WebAuthn API (`.get()`/ `.create()`). 18 | public struct CollectedClientData: Codable, Hashable, Sendable { 19 | enum CollectedClientDataVerifyError: Error { 20 | case ceremonyTypeDoesNotMatch 21 | case challengeDoesNotMatch 22 | case originDoesNotMatch 23 | } 24 | 25 | public enum CeremonyType: String, Codable, Sendable { 26 | case create = "webauthn.create" 27 | case assert = "webauthn.get" 28 | } 29 | 30 | /// Contains the string "webauthn.create" when creating new credentials, 31 | /// and "webauthn.get" when getting an assertion from an existing credential 32 | public let type: CeremonyType 33 | /// The challenge that was provided by the Relying Party 34 | public let challenge: URLEncodedBase64 35 | public let origin: String 36 | 37 | func verify(storedChallenge: [UInt8], ceremonyType: CeremonyType, relyingPartyOrigin: String) throws(CollectedClientDataVerifyError) { 38 | guard type == ceremonyType else { throw .ceremonyTypeDoesNotMatch } 39 | guard challenge == storedChallenge.base64URLEncodedString() else { 40 | throw .challengeDoesNotMatch 41 | } 42 | guard origin == relyingPartyOrigin else { throw .originDoesNotMatch } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2022 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import Crypto 15 | import _CryptoExtras 16 | import Foundation 17 | import SwiftCBOR 18 | 19 | protocol PublicKey: Sendable { 20 | var algorithm: COSEAlgorithmIdentifier { get } 21 | /// Verify a signature was signed with the private key corresponding to the public key. 22 | func verify(signature: some DataProtocol, data: some DataProtocol) throws 23 | } 24 | 25 | enum CredentialPublicKey: Sendable { 26 | case okp(OKPPublicKey) 27 | case ec2(EC2PublicKey) 28 | case rsa(RSAPublicKeyData) 29 | 30 | var key: PublicKey { 31 | switch self { 32 | case let .okp(key): 33 | return key 34 | case let .ec2(key): 35 | return key 36 | case let .rsa(key): 37 | return key 38 | } 39 | } 40 | 41 | init(publicKeyBytes: [UInt8]) throws { 42 | guard let publicKeyObject = try CBOR.decode(publicKeyBytes, options: CBOROptions(maximumDepth: 16)) else { 43 | throw WebAuthnError.badPublicKeyBytes 44 | } 45 | 46 | // A leading 0x04 means we got a public key from an old U2F security key. 47 | // https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-registry-v2.0-id-20180227.html#public-key-representation-formats 48 | guard publicKeyBytes[0] != 0x04 else { 49 | self = .ec2(EC2PublicKey( 50 | algorithm: .algES256, 51 | curve: .p256, 52 | xCoordinate: Array(publicKeyBytes[1...33]), 53 | yCoordinate: Array(publicKeyBytes[33...65]) 54 | )) 55 | return 56 | } 57 | 58 | guard let keyTypeRaw = publicKeyObject[COSEKey.kty.cbor], 59 | case let .unsignedInt(keyTypeInt) = keyTypeRaw, 60 | let keyType = COSEKeyType(rawValue: keyTypeInt) else { 61 | throw WebAuthnError.invalidKeyType 62 | } 63 | 64 | guard let algorithmRaw = publicKeyObject[COSEKey.alg.cbor], 65 | case let .negativeInt(algorithmNegative) = algorithmRaw else { 66 | throw WebAuthnError.invalidAlgorithm 67 | } 68 | // https://github.com/unrelentingtech/SwiftCBOR#swiftcbor 69 | // Negative integers are decoded as NegativeInt(UInt), where the actual number is -1 - i 70 | guard let algorithm = COSEAlgorithmIdentifier(rawValue: -1 - Int(algorithmNegative)) else { 71 | throw WebAuthnError.unsupportedCOSEAlgorithm 72 | } 73 | 74 | // Currently we only support elliptic curve algorithms 75 | switch keyType { 76 | case .ellipticKey: 77 | self = try .ec2(EC2PublicKey(publicKeyObject: publicKeyObject, algorithm: algorithm)) 78 | case .rsaKey: 79 | self = try .rsa(RSAPublicKeyData(publicKeyObject: publicKeyObject, algorithm: algorithm)) 80 | case .octetKey: 81 | throw WebAuthnError.unsupported 82 | // self = try .okp(OKPPublicKey(publicKeyObject: publicKeyObject, algorithm: algorithm)) 83 | } 84 | } 85 | 86 | /// Verify a signature was signed with the private key corresponding to the provided public key. 87 | func verify(signature: some DataProtocol, data: some DataProtocol) throws { 88 | try key.verify(signature: signature, data: data) 89 | } 90 | } 91 | 92 | struct EC2PublicKey: PublicKey, Sendable { 93 | let algorithm: COSEAlgorithmIdentifier 94 | /// The curve on which we derive the signature from. 95 | let curve: COSECurve 96 | /// A byte string 32 bytes in length that holds the x coordinate of the key. 97 | let xCoordinate: [UInt8] 98 | /// A byte string 32 bytes in length that holds the y coordinate of the key. 99 | let yCoordinate: [UInt8] 100 | 101 | var rawRepresentation: [UInt8] { xCoordinate + yCoordinate } 102 | 103 | init(algorithm: COSEAlgorithmIdentifier, curve: COSECurve, xCoordinate: [UInt8], yCoordinate: [UInt8]) { 104 | self.algorithm = algorithm 105 | self.curve = curve 106 | self.xCoordinate = xCoordinate 107 | self.yCoordinate = yCoordinate 108 | } 109 | 110 | init(publicKeyObject: CBOR, algorithm: COSEAlgorithmIdentifier) throws(WebAuthnError) { 111 | self.algorithm = algorithm 112 | 113 | // Curve is key -1 - or -0 for SwiftCBOR 114 | // X Coordinate is key -2, or NegativeInt 1 for SwiftCBOR 115 | // Y Coordinate is key -3, or NegativeInt 2 for SwiftCBOR 116 | guard let curveRaw = publicKeyObject[COSEKey.crv.cbor], 117 | case let .unsignedInt(curve) = curveRaw, 118 | let coseCurve = COSECurve(rawValue: curve) else { 119 | throw .invalidCurve 120 | } 121 | self.curve = coseCurve 122 | 123 | guard let xCoordRaw = publicKeyObject[COSEKey.x.cbor], 124 | case let .byteString(xCoordinateBytes) = xCoordRaw else { 125 | throw .invalidXCoordinate 126 | } 127 | xCoordinate = xCoordinateBytes 128 | guard let yCoordRaw = publicKeyObject[COSEKey.y.cbor], 129 | case let .byteString(yCoordinateBytes) = yCoordRaw else { 130 | throw .invalidYCoordinate 131 | } 132 | yCoordinate = yCoordinateBytes 133 | } 134 | 135 | func verify(signature: some DataProtocol, data: some DataProtocol) throws { 136 | switch algorithm { 137 | case .algES256: 138 | let ecdsaSignature = try P256.Signing.ECDSASignature(derRepresentation: signature) 139 | guard try P256.Signing.PublicKey(rawRepresentation: rawRepresentation) 140 | .isValidSignature(ecdsaSignature, for: data) else { 141 | throw WebAuthnError.invalidSignature 142 | } 143 | case .algES384: 144 | let ecdsaSignature = try P384.Signing.ECDSASignature(derRepresentation: signature) 145 | guard try P384.Signing.PublicKey(rawRepresentation: rawRepresentation) 146 | .isValidSignature(ecdsaSignature, for: data) else { 147 | throw WebAuthnError.invalidSignature 148 | } 149 | case .algES512: 150 | let ecdsaSignature = try P521.Signing.ECDSASignature(derRepresentation: signature) 151 | guard try P521.Signing.PublicKey(rawRepresentation: rawRepresentation) 152 | .isValidSignature(ecdsaSignature, for: data) else { 153 | throw WebAuthnError.invalidSignature 154 | } 155 | default: 156 | throw WebAuthnError.unsupportedCredentialPublicKeyAlgorithm 157 | } 158 | } 159 | } 160 | 161 | struct RSAPublicKeyData: PublicKey, Sendable { 162 | let algorithm: COSEAlgorithmIdentifier 163 | // swiftlint:disable:next identifier_name 164 | let n: [UInt8] 165 | // swiftlint:disable:next identifier_name 166 | let e: [UInt8] 167 | 168 | var rawRepresentation: [UInt8] { n + e } 169 | 170 | init(publicKeyObject: CBOR, algorithm: COSEAlgorithmIdentifier) throws(WebAuthnError) { 171 | self.algorithm = algorithm 172 | 173 | guard let nRaw = publicKeyObject[COSEKey.n.cbor], 174 | case let .byteString(nBytes) = nRaw else { 175 | throw .invalidModulus 176 | } 177 | n = nBytes 178 | 179 | guard let eRaw = publicKeyObject[COSEKey.e.cbor], 180 | case let .byteString(eBytes) = eRaw else { 181 | throw .invalidExponent 182 | } 183 | e = eBytes 184 | } 185 | 186 | func verify(signature: some DataProtocol, data: some DataProtocol) throws { 187 | let rsaSignature = _RSA.Signing.RSASignature(rawRepresentation: signature) 188 | 189 | var rsaPadding: _RSA.Signing.Padding 190 | switch algorithm { 191 | case .algRS1, .algRS256, .algRS384, .algRS512: 192 | rsaPadding = .insecurePKCS1v1_5 193 | case .algPS256, .algPS384, .algPS512: 194 | rsaPadding = .PSS 195 | default: 196 | throw WebAuthnError.unsupportedCOSEAlgorithmForRSAPublicKey 197 | } 198 | 199 | let publicKey = try _RSA.Signing.PublicKey(n: n, e: e) 200 | guard publicKey.isValidSignature(rsaSignature, for: data, padding: rsaPadding) 201 | else { throw WebAuthnError.invalidSignature } 202 | } 203 | } 204 | 205 | /// Currently not in use 206 | struct OKPPublicKey: PublicKey, Sendable { 207 | let algorithm: COSEAlgorithmIdentifier 208 | let curve: UInt64 209 | let xCoordinate: [UInt8] 210 | 211 | init(publicKeyObject: CBOR, algorithm: COSEAlgorithmIdentifier) throws(WebAuthnError) { 212 | self.algorithm = algorithm 213 | // Curve is key -1, or NegativeInt 0 for SwiftCBOR 214 | guard let curveRaw = publicKeyObject[.negativeInt(0)], case let .unsignedInt(curve) = curveRaw else { 215 | throw .invalidCurve 216 | } 217 | self.curve = curve 218 | // X Coordinate is key -2, or NegativeInt 1 for SwiftCBOR 219 | guard let xCoordRaw = publicKeyObject[.negativeInt(1)], 220 | case let .byteString(xCoordinateBytes) = xCoordRaw else { 221 | throw .invalidXCoordinate 222 | } 223 | xCoordinate = xCoordinateBytes 224 | } 225 | 226 | func verify(signature: some DataProtocol, data: some DataProtocol) throws { 227 | throw WebAuthnError.unsupported 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /Sources/WebAuthn/Ceremonies/Shared/CredentialType.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2024 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | /// The type of credential being used. 15 | /// 16 | /// Only ``CredentialType/publicKey`` is supported by WebAuthn. 17 | /// - SeeAlso: [Credential Management Level 1 Editor's Draft §2.1.2. Credential Type Registry](https://w3c.github.io/webappsec-credential-management/#sctn-cred-type-registry) 18 | /// - SeeAlso: [WebAuthn Level 3 Editor's Draft §5.1. PublicKeyCredential Interface](https://w3c.github.io/webauthn/#iface-pkcredential) 19 | public struct CredentialType: UnreferencedStringEnumeration, Sendable { 20 | public var rawValue: String 21 | public init(_ rawValue: String) { 22 | self.rawValue = rawValue 23 | } 24 | 25 | /// A public key credential. 26 | /// - SeeAlso: [WebAuthn Level 3 Editor's Draft §5.1. PublicKeyCredential Interface](https://w3c.github.io/webauthn/#iface-pkcredential) 27 | public static let publicKey: Self = "public-key" 28 | } 29 | -------------------------------------------------------------------------------- /Sources/WebAuthn/Docs.docc/Example Implementation.md: -------------------------------------------------------------------------------- 1 | # Example Implementation 2 | 3 | Explains how to use this library in a server <-> website scenario. 4 | 5 | The library exposes four methods through ``WebAuthnManager``: 6 | 7 | - ``WebAuthnManager/beginRegistration(user:timeoutInSeconds:attestation:publicKeyCredentialParameters:)`` 8 | - ``WebAuthnManager/finishRegistration(challenge:credentialCreationData:requireUserVerification:supportedPublicKeyAlgorithms:pemRootCertificatesByFormat:confirmCredentialIDNotRegisteredYet:)`` 9 | - ``WebAuthnManager/beginAuthentication(timeout:allowCredentials:userVerification:)`` 10 | - ``WebAuthnManager/finishAuthentication(credential:expectedChallenge:credentialPublicKey:credentialCurrentSignCount:requireUserVerification:)`` 11 | 12 | > Important information in advance: 13 | Because bytes are not directly transmittable in either direction as JSON this library provides custom `Codable` implementations for a few types. 14 | When using `Codable` to encode ``PublicKeyCredentialCreationOptions`` and ``PublicKeyCredentialRequestOptions`` byte array properties will be encoded to base64url strings. 15 | When using `Codable` to decode ``RegistrationCredential`` and ``AuthenticationCredential`` base64url encoded strings will be decoded to byte arrays. 16 | When data transmission happens without JSON (e.g. through GRPC) the byte arrays can be transmitted directly. In that case don't use the default `Codable` implementation provided by this library. 17 | 18 | ## Limitations 19 | 20 | There are a few things this library currently does **not** support: 21 | 22 | 1. Currently RSA public keys are not support, we do however plan to add support for that. RSA keys are necessary for 23 | compatibility with Microsoft Windows platform authenticators. 24 | 25 | 2. Octet key pairs are not supported. 26 | 27 | 3. Attestation verification is currently not supported, we do however plan to add support for that. Some work has been 28 | done already, but there are more pieces missing. In most cases attestation verification is not recommended since it 29 | causes a lot of overhead. 30 | > [From Yubico](https://developers.yubico.com/WebAuthn/WebAuthn_Developer_Guide/Attestation.html): "If a service does not have a specific need for attestation information, namely a well defined policy for what to 31 | do with it and why, it is not recommended to verify authenticator attestations" 32 | 33 | ### Setup 34 | 35 | Configure your backend with a ``WebAuthnManager`` instance: 36 | 37 | ```swift 38 | let webAuthnManager = WebAuthnManager( 39 | configuration: .init( 40 | relyingPartyID: "example.com", 41 | relyingPartyName: "My Fancy Web App", 42 | relyingPartyOrigin: "https://example.com" 43 | ) 44 | ) 45 | ``` 46 | 47 | ### Registration 48 | 49 | Scenario: A user wants to signup on a website using WebAuthn. 50 | 51 | ![Registration flow overview](registration.svg) 52 | 53 | #### Explanation 54 | 55 | 1. When tapping the "Register" button the client sends a request to 56 | the backend. The relying party responds to this request with a call to ``WebAuthnManager/beginRegistration(user:timeoutInSeconds:attestation:publicKeyCredentialParameters:)`` which then returns a 57 | new ``PublicKeyCredentialRequestOptions``. This must be send back to the client so it can pass it to 58 | `navigator.credentials.create()`. 59 | 60 | 2. Whatever `navigator.credentials.create()` returns will be send back to the relying party, parsing it into 61 | ``RegistrationCredential``. 62 | ```swift 63 | let registrationCredential = try JSONDecoder().decode(RegistrationCredential.self) 64 | ``` 65 | 66 | 3. Next the backend calls ``WebAuthnManager/finishRegistration(challenge:credentialCreationData:requireUserVerification:supportedPublicKeyAlgorithms:pemRootCertificatesByFormat:confirmCredentialIDNotRegisteredYet:)`` with the previously 67 | generated challenge and the received ``RegistrationCredential``. If no error are thrown a new ``Credential`` 68 | object will be returned. This object contains information about the new credential, including an id and the generated public-key. Persist this data in e.g. a database and link the entry to the user. 69 | 70 | ##### Example implementation (using Vapor) 71 | 72 | ```swift 73 | authSessionRoutes.get("makeCredential") { req -> PublicKeyCredentialCreationOptions in 74 | let user = try req.auth.require(User.self) 75 | let options = try req.webAuthn.beginRegistration(user: user) 76 | req.session.data["challenge"] = options.challenge 77 | return options 78 | } 79 | 80 | authSessionRoutes.post("makeCredential") { req -> HTTPStatus in 81 | let user = try req.auth.require(User.self) 82 | guard let challenge = req.session.data["challenge"] else { throw Abort(.unauthorized) } 83 | let registrationCredential = try req.content.decode(RegistrationCredential.self) 84 | 85 | let credential = try await req.webAuthn.finishRegistration( 86 | challenge: challenge, 87 | credentialCreationData: registrationCredential, 88 | // this is likely to be removed soon 89 | confirmCredentialIDNotRegisteredYet: { credentialID in 90 | try await queryCredentialWithUser(id: credentialID) == nil 91 | } 92 | ) 93 | 94 | try await WebAuthnCredential(from: credential, userID: user.requireID()) 95 | .save(on: req.db) 96 | 97 | return .ok 98 | } 99 | ``` 100 | 101 | ### Authentication 102 | 103 | Scenario: A user wants to log in on a website using WebAuthn. 104 | 105 | ![Authentication flow overview](authentication.svg) 106 | 107 | #### Explanation 108 | 109 | 1. When tapping the "Login" button the client sends a request to 110 | the relying party. The relying party responds to this request with a call to ``WebAuthnManager/beginAuthentication(timeout:allowCredentials:userVerification:)`` which then in turn 111 | returns a new ``PublicKeyCredentialRequestOptions``. This must be sent back to the client so it can pass it to 112 | `navigator.credentials.get()`. 113 | 2. Whatever `navigator.credentials.get()` returns will be sent back to the relying party, parsing it into 114 | ``AuthenticationCredential``. 115 | ```swift 116 | let authenticationCredential = try JSONDecoder().decode(AuthenticationCredential.self) 117 | ``` 118 | 3. Next the backend calls 119 | ``WebAuthnManager/finishAuthentication(credential:expectedChallenge:credentialPublicKey:credentialCurrentSignCount:requireUserVerification:)``. 120 | - The `credential` parameter expects the decoded ``AuthenticationCredential`` 121 | - The `expectedChallenge` parameter expects the challenge previously generated 122 | from `beginAuthentication()` (obtained e.g. through a session). 123 | - Query the persisted credential from [Registration](#registration) using the credential id from the decoded 124 | `AuthenticationCredential`. Pass this credential in the `credentialPublicKey` parameter and it's sign count to 125 | `credentialCurrentSignCount`. 126 | 127 | 4. If `finishAuthentication` succeeds you can safely login the user linked to the credential! `finishAuthentication` 128 | will return a `VerifiedAuthentication` with the updated sign count and a few other pieces of information to be 129 | persisted. Use this to update the credential in the database. 130 | 131 | #### Example implementation (using Vapor) 132 | 133 | ```swift 134 | // this endpoint will be called on clicking "Login" 135 | authSessionRoutes.get("authenticate") { req -> PublicKeyCredentialRequestOptions in 136 | let options = try req.webAuthn.beginAuthentication() 137 | req.session.data["challenge"] = String.base64URL(fromBase64: options.challenge) 138 | return options 139 | } 140 | 141 | // this endpoint will be called after the user used e.g. TouchID. 142 | authSessionRoutes.post("authenticate") { req -> HTTPStatus in 143 | guard let challenge = req.session.data["challenge"] else { throw Abort(.unauthorized) } 144 | let data = try req.content.decode(AuthenticationCredential.self) 145 | guard let credential = try await queryCredentialWithUser(id: data.id) else { 146 | throw Abort(.unauthorized) 147 | } 148 | 149 | let verifiedAuthentication = try req.webAuthn.finishAuthentication( 150 | credential: data, 151 | expectedChallenge: challenge, 152 | credentialPublicKey: credential.publicKey, 153 | credentialCurrentSignCount: credential.currentSignCount 154 | ) 155 | 156 | credential.currentSignCount = verifiedAuthentication.newSignCount 157 | try await credential.save(on: req.db) 158 | 159 | req.auth.login(credential.user) 160 | 161 | return .ok 162 | } 163 | ``` 164 | -------------------------------------------------------------------------------- /Sources/WebAuthn/Docs.docc/index.md: -------------------------------------------------------------------------------- 1 | # ``WebAuthn`` 2 | 3 | A library for working with WebAuthn and Passkeys. 4 | 5 | ## Overview 6 | 7 | WebAuthn uses public key cryptography to allow applications to authenticate using passwordless methods 8 | such as biometrics, hardware security keys, or other devices. They're sometimes also referred to as Passkeys. The library also allows applications to implement two factor authentication using the [Universal 2nd Factor](https://en.wikipedia.org/wiki/Universal_2nd_Factor) to provide a secure second factor for authentication flows. 9 | 10 | This library aims to simplify the implementation of the Relying Party in an application. It is responsible 11 | for processing authenticator responses and deciding whether an authentication, or registration, attempt is valid 12 | or not. Usually the Relying Party persists the resulting public key of a successful registration attempt alongside with 13 | the corresponding user in a database. 14 | 15 | ![Graphic explaining WebAuthn parties](overview.svg) 16 | 17 | ## Topics 18 | 19 | ### Articles 20 | 21 | - 22 | 23 | ### Essentials 24 | 25 | - ``WebAuthnManager`` 26 | - ``WebAuthnManager/Configuration`` 27 | - ``PublicKeyCredentialUserEntity`` 28 | 29 | ### Responses 30 | 31 | - ``PublicKeyCredentialCreationOptions`` 32 | - ``PublicKeyCredentialRequestOptions`` 33 | - ``Credential`` 34 | - ``VerifiedAuthentication`` 35 | 36 | ### Serialization 37 | 38 | - ``RegistrationCredential`` 39 | - ``AuthenticationCredential`` 40 | 41 | ### Errors 42 | 43 | - ``WebAuthnError`` 44 | -------------------------------------------------------------------------------- /Sources/WebAuthn/Helpers/Base64Utilities.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2022 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import Foundation 15 | import Logging 16 | 17 | /// Container for base64 encoded data 18 | public struct EncodedBase64: ExpressibleByStringLiteral, Codable, Hashable, Equatable, Sendable { 19 | private let base64: String 20 | 21 | public init(_ string: String) { 22 | self.base64 = string 23 | } 24 | 25 | public init(stringLiteral value: StringLiteralType) { 26 | self.init(value) 27 | } 28 | 29 | public init(from decoder: any Decoder) throws { 30 | let container = try decoder.singleValueContainer() 31 | self.base64 = try container.decode(String.self) 32 | } 33 | 34 | public func encode(to encoder: any Encoder) throws { 35 | var container = encoder.singleValueContainer() 36 | try container.encode(self.base64) 37 | } 38 | 39 | /// Return as Base64URL 40 | public var urlEncoded: URLEncodedBase64 { 41 | return .init( 42 | self.base64.replacingOccurrences(of: "+", with: "-") 43 | .replacingOccurrences(of: "/", with: "_") 44 | .replacingOccurrences(of: "=", with: "") 45 | ) 46 | } 47 | 48 | /// Decodes Base64 string and transforms result into `Data` 49 | public var decoded: Data? { 50 | return Data(base64Encoded: self.base64) 51 | } 52 | 53 | /// Returns Base64 data as a String 54 | public func asString() -> String { 55 | return self.base64 56 | } 57 | } 58 | 59 | /// Container for URL encoded base64 data 60 | public struct URLEncodedBase64: ExpressibleByStringLiteral, Codable, Hashable, Equatable, Sendable { 61 | let base64URL: String 62 | 63 | /// Decodes Base64URL string and transforms result into `[UInt8]` 64 | public var decodedBytes: [UInt8]? { 65 | guard let base64DecodedData = urlDecoded.decoded else { return nil } 66 | return [UInt8](base64DecodedData) 67 | } 68 | 69 | public init(_ string: String) { 70 | self.base64URL = string 71 | } 72 | 73 | public init(stringLiteral value: StringLiteralType) { 74 | self.init(value) 75 | } 76 | 77 | public init(from decoder: Decoder) throws { 78 | let container = try decoder.singleValueContainer() 79 | self.base64URL = try container.decode(String.self) 80 | } 81 | 82 | public func encode(to encoder: Encoder) throws { 83 | var container = encoder.singleValueContainer() 84 | try container.encode(self.base64URL) 85 | } 86 | 87 | /// Decodes Base64URL into Base64 88 | public var urlDecoded: EncodedBase64 { 89 | var result = self.base64URL.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/") 90 | while result.count % 4 != 0 { 91 | result = result.appending("=") 92 | } 93 | return .init(result) 94 | } 95 | 96 | /// Return Base64URL as a String 97 | public func asString() -> String { 98 | return self.base64URL 99 | } 100 | } 101 | 102 | extension Array where Element == UInt8 { 103 | /// Encodes an array of bytes into a base64url-encoded string 104 | /// - Returns: A base64url-encoded string 105 | public func base64URLEncodedString() -> URLEncodedBase64 { 106 | let base64String = Data(bytes: self, count: self.count).base64EncodedString() 107 | return EncodedBase64(base64String).urlEncoded 108 | } 109 | 110 | /// Encodes an array of bytes into a base64 string 111 | /// - Returns: A base64-encoded string 112 | public func base64EncodedString() -> EncodedBase64 { 113 | return .init(Data(bytes: self, count: self.count).base64EncodedString()) 114 | } 115 | } 116 | 117 | extension Data { 118 | /// Encodes data into a base64url-encoded string 119 | /// - Returns: A base64url-encoded string 120 | public func base64URLEncodedString() -> URLEncodedBase64 { 121 | return [UInt8](self).base64URLEncodedString() 122 | } 123 | } 124 | 125 | extension String { 126 | func toBase64() -> EncodedBase64 { 127 | return .init(Data(self.utf8).base64EncodedString()) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /Sources/WebAuthn/Helpers/ByteCasting.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2024 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import Foundation 15 | 16 | extension BidirectionalCollection where Element == UInt8 { 17 | /// Cast a byte sequence into a trivial type like a primitive or a tuple of primitives. 18 | /// 19 | /// - Note: It is up to the caller to verify the receiver's size before casting it. 20 | @inlinable 21 | func casting() -> R { 22 | precondition(self.count == MemoryLayout.size, "self.count (\(self.count)) does not match MemoryLayout.size (\(MemoryLayout.size))") 23 | 24 | let result = self.withContiguousStorageIfAvailable({ 25 | $0.withUnsafeBytes { $0.loadUnaligned(as: R.self) } 26 | }) ?? Array(self).withUnsafeBytes { 27 | $0.loadUnaligned(as: R.self) 28 | } 29 | 30 | return result 31 | } 32 | } 33 | 34 | extension FixedWidthInteger { 35 | /// Initialize a fixed width integer from a contiguous sequence of Bytes representing a big endian type. 36 | /// - Parameter bigEndianBytes: The Bytes to interpret as a big endian integer. 37 | @inlinable 38 | init(bigEndianBytes: some BidirectionalCollection) { 39 | self.init(bigEndian: bigEndianBytes.casting()) 40 | } 41 | 42 | /// Initialize a fixed width integer from a contiguous sequence of Bytes representing a little endian type. 43 | /// - Parameter bigEndianBytes: The Bytes to interpret as a little endian integer. 44 | @inlinable 45 | init(littleEndianBytes: some BidirectionalCollection) { 46 | self.init(littleEndian: littleEndianBytes.casting()) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/WebAuthn/Helpers/ChallengeGenerator.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2023 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | package struct ChallengeGenerator: Sendable { 15 | var generate: @Sendable () -> [UInt8] 16 | 17 | package static var live: Self { 18 | .init(generate: { [UInt8].random(count: 32) }) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/WebAuthn/Helpers/Data+safeSubscript.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2023 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import Foundation 15 | 16 | extension Data { 17 | struct IndexOutOfBounds: Error {} 18 | 19 | subscript(safe range: Range) -> Data? { 20 | guard count >= range.upperBound else { return nil } 21 | return self[range] 22 | } 23 | 24 | /// Safely slices bytes from `pointer` to `pointer` + `length`. Updates the pointer afterwards. 25 | /// - Returns: The sliced bytes or nil if we're out of bounds. 26 | func safeSlice(length: Int, using pointer: inout Int) -> Data? { 27 | guard let value = self[safe: pointer..<(pointer + length)] else { return nil } 28 | pointer += length 29 | return value 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/WebAuthn/Helpers/Duration+Milliseconds.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2022 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | extension Duration { 15 | /// The value of a positive duration in milliseconds, suitable to be encoded in WebAuthn types. 16 | var milliseconds: Int64 { 17 | let (seconds, attoseconds) = self.components 18 | return Int64(seconds * 1000) + Int64(attoseconds/1_000_000_000_000_000) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/WebAuthn/Helpers/KeyedDecodingContainer+decodeURLEncoded.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2023 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import Foundation 15 | 16 | extension KeyedDecodingContainer { 17 | func decodeBytesFromURLEncodedBase64(forKey key: KeyedDecodingContainer.Key) throws -> [UInt8] { 18 | guard let bytes = try decode( 19 | URLEncodedBase64.self, 20 | forKey: key 21 | ).decodedBytes else { 22 | throw DecodingError.dataCorruptedError( 23 | forKey: key, 24 | in: self, 25 | debugDescription: "Failed to decode base64url encoded string at \(key) into bytes" 26 | ) 27 | } 28 | return bytes 29 | } 30 | 31 | func decodeBytesFromURLEncodedBase64IfPresent(forKey key: KeyedDecodingContainer.Key) throws -> [UInt8]? { 32 | guard let bytes = try decodeIfPresent( 33 | URLEncodedBase64.self, 34 | forKey: key 35 | ) else { return nil } 36 | 37 | guard let decodedBytes = bytes.decodedBytes else { 38 | throw DecodingError.dataCorruptedError( 39 | forKey: key, 40 | in: self, 41 | debugDescription: "Failed to decode base64url encoded string at \(key) into bytes" 42 | ) 43 | } 44 | return decodedBytes 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/WebAuthn/Helpers/UInt8+random.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2023 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | extension FixedWidthInteger { 15 | public static func random() -> Self { 16 | return Self.random(in: .min ... .max) 17 | } 18 | 19 | public static func random(using generator: inout T) -> Self where T: RandomNumberGenerator { 20 | return Self.random(in: .min ... .max, using: &generator) 21 | } 22 | } 23 | 24 | extension Array where Element: FixedWidthInteger { 25 | public static func random(count: Int) -> [Element] { 26 | var array: [Element] = .init(repeating: 0, count: count) 27 | (0..(count: Int, using generator: inout T) -> [Element] where T: RandomNumberGenerator { 32 | var array: [Element] = .init(repeating: 0, count: count) 33 | (0.. Bool { 30 | lhs.rawValue < rhs.rawValue 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/WebAuthn/WebAuthnError.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2022 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | /// An error that occured preparing or processing WebAuthn-related requests. 15 | public struct WebAuthnError: Error, Hashable, Sendable { 16 | enum Reason: Error { 17 | // MARK: Shared 18 | case attestedCredentialDataMissing 19 | case relyingPartyIDHashDoesNotMatch 20 | case userPresentFlagNotSet 21 | case invalidSignature 22 | 23 | // MARK: AttestationObject 24 | case userVerificationRequiredButFlagNotSet 25 | case attestationStatementMustBeEmpty 26 | case attestationVerificationNotSupported 27 | 28 | // MARK: WebAuthnManager 29 | case invalidUserID 30 | case unsupportedCredentialPublicKeyAlgorithm 31 | case credentialIDAlreadyExists 32 | case userVerifiedFlagNotSet 33 | case potentialReplayAttack 34 | case invalidAssertionCredentialType 35 | 36 | // MARK: ParsedAuthenticatorAttestationResponse 37 | case invalidAttestationObject 38 | case invalidAuthData 39 | case invalidFmt 40 | case missingAttStmt 41 | case attestationFormatNotSupported 42 | 43 | // MARK: ParsedCredentialCreationResponse 44 | case invalidCredentialCreationType 45 | case credentialRawIDTooLong 46 | 47 | // MARK: AuthenticatorData 48 | case authDataTooShort 49 | case attestedCredentialFlagNotSet 50 | case extensionDataMissing 51 | case leftOverBytesInAuthenticatorData 52 | case credentialIDTooLong 53 | case credentialIDTooShort 54 | case invalidPublicKeyLength 55 | 56 | // MARK: CredentialPublicKey 57 | case badPublicKeyBytes 58 | case invalidKeyType 59 | case invalidAlgorithm 60 | case invalidCurve 61 | case invalidXCoordinate 62 | case invalidYCoordinate 63 | case unsupportedCOSEAlgorithm 64 | case unsupportedCOSEAlgorithmForEC2PublicKey 65 | case invalidModulus 66 | case invalidExponent 67 | case unsupportedCOSEAlgorithmForRSAPublicKey 68 | case unsupported 69 | } 70 | 71 | let reason: Reason 72 | 73 | init(reason: Reason) { 74 | self.reason = reason 75 | } 76 | 77 | // MARK: Shared 78 | public static let attestedCredentialDataMissing = Self(reason: .attestedCredentialDataMissing) 79 | public static let relyingPartyIDHashDoesNotMatch = Self(reason: .relyingPartyIDHashDoesNotMatch) 80 | public static let userPresentFlagNotSet = Self(reason: .userPresentFlagNotSet) 81 | public static let invalidSignature = Self(reason: .invalidSignature) 82 | 83 | // MARK: AttestationObject 84 | public static let userVerificationRequiredButFlagNotSet = Self(reason: .userVerificationRequiredButFlagNotSet) 85 | public static let attestationStatementMustBeEmpty = Self(reason: .attestationStatementMustBeEmpty) 86 | public static let attestationVerificationNotSupported = Self(reason: .attestationVerificationNotSupported) 87 | 88 | // MARK: WebAuthnManager 89 | public static let invalidUserID = Self(reason: .invalidUserID) 90 | public static let unsupportedCredentialPublicKeyAlgorithm = Self(reason: .unsupportedCredentialPublicKeyAlgorithm) 91 | public static let credentialIDAlreadyExists = Self(reason: .credentialIDAlreadyExists) 92 | public static let userVerifiedFlagNotSet = Self(reason: .userVerifiedFlagNotSet) 93 | public static let potentialReplayAttack = Self(reason: .potentialReplayAttack) 94 | public static let invalidAssertionCredentialType = Self(reason: .invalidAssertionCredentialType) 95 | 96 | // MARK: ParsedAuthenticatorAttestationResponse 97 | public static let invalidAttestationObject = Self(reason: .invalidAttestationObject) 98 | public static let invalidAuthData = Self(reason: .invalidAuthData) 99 | public static let invalidFmt = Self(reason: .invalidFmt) 100 | public static let missingAttStmt = Self(reason: .missingAttStmt) 101 | public static let attestationFormatNotSupported = Self(reason: .attestationFormatNotSupported) 102 | 103 | // MARK: ParsedCredentialCreationResponse 104 | public static let invalidCredentialCreationType = Self(reason: .invalidCredentialCreationType) 105 | public static let credentialRawIDTooLong = Self(reason: .credentialRawIDTooLong) 106 | 107 | // MARK: AuthenticatorData 108 | public static let authDataTooShort = Self(reason: .authDataTooShort) 109 | public static let attestedCredentialFlagNotSet = Self(reason: .attestedCredentialFlagNotSet) 110 | public static let extensionDataMissing = Self(reason: .extensionDataMissing) 111 | public static let leftOverBytesInAuthenticatorData = Self(reason: .leftOverBytesInAuthenticatorData) 112 | public static let credentialIDTooLong = Self(reason: .credentialIDTooLong) 113 | public static let credentialIDTooShort = Self(reason: .credentialIDTooShort) 114 | public static let invalidPublicKeyLength = Self(reason: .invalidPublicKeyLength) 115 | 116 | // MARK: CredentialPublicKey 117 | public static let badPublicKeyBytes = Self(reason: .badPublicKeyBytes) 118 | public static let invalidKeyType = Self(reason: .invalidKeyType) 119 | public static let invalidAlgorithm = Self(reason: .invalidAlgorithm) 120 | public static let invalidCurve = Self(reason: .invalidCurve) 121 | public static let invalidXCoordinate = Self(reason: .invalidXCoordinate) 122 | public static let invalidYCoordinate = Self(reason: .invalidYCoordinate) 123 | public static let unsupportedCOSEAlgorithm = Self(reason: .unsupportedCOSEAlgorithm) 124 | public static let unsupportedCOSEAlgorithmForEC2PublicKey = Self(reason: .unsupportedCOSEAlgorithmForEC2PublicKey) 125 | public static let invalidModulus = Self(reason: .invalidModulus) 126 | public static let invalidExponent = Self(reason: .invalidExponent) 127 | public static let unsupportedCOSEAlgorithmForRSAPublicKey = Self(reason: .unsupportedCOSEAlgorithmForRSAPublicKey) 128 | public static let unsupported = Self(reason: .unsupported) 129 | } 130 | -------------------------------------------------------------------------------- /Sources/WebAuthn/WebAuthnManager+Configuration.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2022 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import Foundation 15 | 16 | extension WebAuthnManager { 17 | /// Configuration represents the WebAuthn configuration. 18 | public struct Configuration: Sendable { 19 | /// The relying party id is based on the host's domain. 20 | /// It does not include a scheme or port (like the `relyingPartyOrigin`). 21 | /// For example, if the origin is https://login.example.com:1337, then _login.example.com_ or _example.com_ are 22 | /// valid ids, but not _m.login.example.com_ and not _com_. 23 | public let relyingPartyID: String 24 | 25 | /// Configures the display name for the Relying Party Server. This can be any string. 26 | public let relyingPartyName: String 27 | 28 | /// The domain, with HTTP protocol (e.g. "https://example.com") 29 | public let relyingPartyOrigin: String 30 | 31 | /// Creates a new ``WebAuthnManager.Configuration`` with information about the Relying Party 32 | /// - Parameters: 33 | /// - relyingPartyID: The relying party id is based on the host's domain. (e.g. _login.example.com_) 34 | /// - relyingPartyName: Name for the Relying Party. Can be any string. 35 | /// - relyingPartyOrigin: The domain, with HTTP protocol (e.g. _https://login.example.com_) 36 | public init( 37 | relyingPartyID: String, 38 | relyingPartyName: String, 39 | relyingPartyOrigin: String 40 | ) { 41 | self.relyingPartyID = relyingPartyID 42 | self.relyingPartyName = relyingPartyName 43 | self.relyingPartyOrigin = relyingPartyOrigin 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/WebAuthn/WebAuthnManager.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2022 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import Foundation 15 | 16 | /// Main entrypoint for WebAuthn operations. 17 | /// 18 | /// Use this struct to perform registration and authentication ceremonies. 19 | /// 20 | /// Registration: To generate registration options, call `beginRegistration()`. Pass the resulting 21 | /// ``PublicKeyCredentialCreationOptions`` to the client. 22 | /// When the client has received the response from the authenticator, pass the response to 23 | /// `finishRegistration()`. 24 | /// 25 | /// Authentication: To generate authentication options, call `beginAuthentication()`. Pass the resulting 26 | /// ``PublicKeyCredentialRequestOptions`` to the client. 27 | /// When the client has received the response from the authenticator, pass the response to 28 | /// `finishAuthentication()`. 29 | public struct WebAuthnManager: Sendable { 30 | private let configuration: Configuration 31 | 32 | private let challengeGenerator: ChallengeGenerator 33 | 34 | /// Create a new WebAuthnManager using the given configuration. 35 | /// 36 | /// - Parameters: 37 | /// - configuration: The configuration to use for this manager. 38 | public init(configuration: Configuration) { 39 | self.init(configuration: configuration, challengeGenerator: .live) 40 | } 41 | 42 | package init(configuration: Configuration, challengeGenerator: ChallengeGenerator) { 43 | self.configuration = configuration 44 | self.challengeGenerator = challengeGenerator 45 | } 46 | 47 | /// Generate a new set of registration data to be sent to the client. 48 | /// 49 | /// This method will use the Relying Party information from the WebAuthnManager's configuration to create ``PublicKeyCredentialCreationOptions`` 50 | /// - Parameters: 51 | /// - user: The user to register. 52 | /// - timeout: How long the browser should give the user to choose an authenticator. This value 53 | /// is a *hint* and may be ignored by the browser. Defaults to 300000 milliseconds (5 minutes). 54 | /// - attestation: The Relying Party's preference regarding attestation. Defaults to `.none`. 55 | /// - publicKeyCredentialParameters: A list of public key algorithms the Relying Party chooses to restrict 56 | /// support to. Defaults to all supported algorithms. 57 | /// - Returns: Registration options ready for the browser. 58 | public func beginRegistration( 59 | user: PublicKeyCredentialUserEntity, 60 | timeout: Duration? = .seconds(5*60), 61 | attestation: AttestationConveyancePreference = .none, 62 | publicKeyCredentialParameters: [PublicKeyCredentialParameters] = .supported 63 | ) -> PublicKeyCredentialCreationOptions { 64 | let challenge = challengeGenerator.generate() 65 | 66 | return PublicKeyCredentialCreationOptions( 67 | challenge: challenge, 68 | user: user, 69 | relyingParty: .init(id: configuration.relyingPartyID, name: configuration.relyingPartyName), 70 | publicKeyCredentialParameters: publicKeyCredentialParameters, 71 | timeout: timeout, 72 | attestation: attestation 73 | ) 74 | } 75 | 76 | /// Take response from authenticator and client and verify credential against the user's credentials and 77 | /// session data. 78 | /// 79 | /// - Parameters: 80 | /// - challenge: The challenge passed to the authenticator within the preceding registration options. 81 | /// - credentialCreationData: The value returned from `navigator.credentials.create()` 82 | /// - requireUserVerification: Whether or not to require that the authenticator verified the user. 83 | /// - supportedPublicKeyAlgorithms: A list of public key algorithms the Relying Party chooses to restrict 84 | /// support to. Defaults to all supported algorithms. 85 | /// - pemRootCertificatesByFormat: A list of root certificates used for attestation verification. 86 | /// If attestation verification is not required (default behavior) this parameter does nothing. 87 | /// - confirmCredentialIDNotRegisteredYet: For a successful registration ceremony we need to verify that the 88 | /// `credentialId`, generated by the authenticator, is not yet registered for any user. This is a good place to 89 | /// handle that. 90 | /// - Returns: A new `Credential` with information about the authenticator and registration 91 | public func finishRegistration( 92 | challenge: [UInt8], 93 | credentialCreationData: RegistrationCredential, 94 | requireUserVerification: Bool = false, 95 | supportedPublicKeyAlgorithms: [PublicKeyCredentialParameters] = .supported, 96 | pemRootCertificatesByFormat: [AttestationFormat: [Data]] = [:], 97 | confirmCredentialIDNotRegisteredYet: (String) async throws -> Bool 98 | ) async throws -> Credential { 99 | let parsedData = try ParsedCredentialCreationResponse(from: credentialCreationData) 100 | let attestedCredentialData = try await parsedData.verify( 101 | storedChallenge: challenge, 102 | verifyUser: requireUserVerification, 103 | relyingPartyID: configuration.relyingPartyID, 104 | relyingPartyOrigin: configuration.relyingPartyOrigin, 105 | supportedPublicKeyAlgorithms: supportedPublicKeyAlgorithms, 106 | pemRootCertificatesByFormat: pemRootCertificatesByFormat 107 | ) 108 | 109 | // TODO: Step 18. -> Verify client extensions 110 | 111 | // Step 24. 112 | guard try await confirmCredentialIDNotRegisteredYet(parsedData.id.asString()) else { 113 | throw WebAuthnError.credentialIDAlreadyExists 114 | } 115 | 116 | // Step 25. 117 | return Credential( 118 | type: parsedData.type, 119 | id: parsedData.id.urlDecoded.asString(), 120 | publicKey: attestedCredentialData.publicKey, 121 | signCount: parsedData.response.attestationObject.authenticatorData.counter, 122 | backupEligible: parsedData.response.attestationObject.authenticatorData.flags.isBackupEligible, 123 | isBackedUp: parsedData.response.attestationObject.authenticatorData.flags.isCurrentlyBackedUp, 124 | attestationObject: parsedData.response.attestationObject, 125 | attestationClientDataJSON: parsedData.response.clientData 126 | ) 127 | } 128 | 129 | /// Generate options for retrieving a credential via navigator.credentials.get() 130 | /// 131 | /// - Parameters: 132 | /// - timeout: How long the browser should give the user to choose an authenticator. This value 133 | /// is a *hint* and may be ignored by the browser. Defaults to 60 seconds. 134 | /// - allowCredentials: A list of credentials registered to the user. 135 | /// - userVerification: The Relying Party's preference for the authenticator's enforcement of the 136 | /// "user verified" flag. 137 | /// - Returns: Authentication options ready for the browser. 138 | public func beginAuthentication( 139 | timeout: Duration? = .seconds(60), 140 | allowCredentials: [PublicKeyCredentialDescriptor]? = nil, 141 | userVerification: UserVerificationRequirement = .preferred 142 | ) -> PublicKeyCredentialRequestOptions { 143 | let challenge = challengeGenerator.generate() 144 | 145 | return PublicKeyCredentialRequestOptions( 146 | challenge: challenge, 147 | timeout: timeout, 148 | relyingPartyID: configuration.relyingPartyID, 149 | allowCredentials: allowCredentials, 150 | userVerification: userVerification 151 | ) 152 | } 153 | 154 | /// Verify a response from navigator.credentials.get() 155 | /// 156 | /// - Parameters: 157 | /// - credential: The value returned from `navigator.credentials.get()`. 158 | /// - expectedChallenge: The challenge passed to the authenticator within the preceding authentication options. 159 | /// - credentialPublicKey: The public key for the credential's ID as provided in a preceding authenticator 160 | /// registration ceremony. 161 | /// - credentialCurrentSignCount: The current known number of times the authenticator was used. 162 | /// - requireUserVerification: Whether or not to require that the authenticator verified the user. 163 | /// - Returns: Information about the authenticator 164 | public func finishAuthentication( 165 | credential: AuthenticationCredential, 166 | // clientExtensionResults: , 167 | expectedChallenge: [UInt8], 168 | credentialPublicKey: [UInt8], 169 | credentialCurrentSignCount: UInt32, 170 | requireUserVerification: Bool = false 171 | ) throws -> VerifiedAuthentication { 172 | guard credential.type == .publicKey 173 | else { throw WebAuthnError.invalidAssertionCredentialType } 174 | 175 | let parsedAssertion = try ParsedAuthenticatorAssertionResponse(from: credential.response) 176 | try parsedAssertion.verify( 177 | expectedChallenge: expectedChallenge, 178 | relyingPartyOrigin: configuration.relyingPartyOrigin, 179 | relyingPartyID: configuration.relyingPartyID, 180 | requireUserVerification: requireUserVerification, 181 | credentialPublicKey: credentialPublicKey, 182 | credentialCurrentSignCount: credentialCurrentSignCount 183 | ) 184 | 185 | return VerifiedAuthentication( 186 | credentialID: credential.id, 187 | newSignCount: parsedAssertion.authenticatorData.counter, 188 | credentialDeviceType: parsedAssertion.authenticatorData.flags.deviceType, 189 | credentialBackedUp: parsedAssertion.authenticatorData.flags.isCurrentlyBackedUp 190 | ) 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /Tests/WebAuthnTests/AuthenticatorAttestationGloballyUniqueIDTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2024 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import Foundation 15 | import Testing 16 | @testable import WebAuthn 17 | 18 | struct AuthenticatorAttestationGloballyUniqueIDTests { 19 | @Test 20 | func byteCoding() { 21 | let aaguid = AuthenticatorAttestationGloballyUniqueID(bytes: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]) 22 | #expect(aaguid != nil) 23 | #expect(aaguid?.bytes == [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f]) 24 | #expect(aaguid?.id == UUID(uuid: (0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f))) 25 | #expect(aaguid == AuthenticatorAttestationGloballyUniqueID(uuid: UUID(uuid: (0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f)))) 26 | #expect(aaguid == AuthenticatorAttestationGloballyUniqueID(uuidString: "00010203-0405-0607-0809-0A0B0C0D0E0F" )) 27 | } 28 | 29 | @Test 30 | func invalidByteDecoding() { 31 | #expect(AuthenticatorAttestationGloballyUniqueID(bytes: []) == nil) 32 | #expect(AuthenticatorAttestationGloballyUniqueID(bytes: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]) == nil) 33 | #expect(AuthenticatorAttestationGloballyUniqueID(bytes: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]) == nil) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Tests/WebAuthnTests/DurationTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2022 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import Testing 15 | @testable import WebAuthn 16 | 17 | struct DurationTests { 18 | @Test 19 | func milliseconds() { 20 | #expect(Duration.milliseconds(1234).milliseconds == 1234) 21 | #expect(Duration.milliseconds(-1234).milliseconds == -1234) 22 | #expect(Duration.microseconds(12345).milliseconds == 12) 23 | #expect(Duration.microseconds(-12345).milliseconds == -12) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Tests/WebAuthnTests/Formats/TPMAttestationTests/CertInfoTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2022 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | // @testable import WebAuthn 15 | // import XCTest 16 | 17 | // final class CertInfoTests: XCTestCase { 18 | // func testInitReturnsNilIfDataIsTooShort() { 19 | // XCTAssertNil(TPMAttestation.CertInfo(fromBytes: Data([UInt8](repeating: 0, count: 8)))) 20 | // XCTAssertNil(TPMAttestation.CertInfo(fromBytes: Data())) 21 | // } 22 | 23 | // func testVerifyThrowsIfMagicIsInvalid() throws { 24 | // let certInfo = TPMAttestation.CertInfo(fromBytes: Data([UInt8](repeating: 0, count: 80)))! 25 | // try assertThrowsError(certInfo.verify(), expect: TPMAttestation.CertInfoError.magicInvalid) 26 | // } 27 | 28 | // func testVerifyThrowsIfTypeIsInvalid() throws { 29 | // let certInfoBytes: [UInt8] = [0xFF, 0x54, 0x43, 0x47] + [UInt8](repeating: 0, count: 80) 30 | // let certInfo = TPMAttestation.CertInfo(fromBytes: Data(certInfoBytes))! 31 | // try assertThrowsError(certInfo.verify(), expect: TPMAttestation.CertInfoError.typeInvalid) 32 | // } 33 | // } 34 | -------------------------------------------------------------------------------- /Tests/WebAuthnTests/HelpersTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2023 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import Foundation 15 | import Testing 16 | @testable import WebAuthn 17 | 18 | struct HelpersTests { 19 | @Test 20 | func base64URLEncodeReturnsCorrectString() { 21 | let input: [UInt8] = [1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0] 22 | let expectedBase64 = "AQABAAEBAAEAAQEAAAABAA==" 23 | let expectedBase64URL = "AQABAAEBAAEAAQEAAAABAA" 24 | 25 | let base64Encoded = input.base64EncodedString() 26 | let base64URLEncoded = input.base64URLEncodedString() 27 | 28 | #expect(expectedBase64 == base64Encoded.asString()) 29 | #expect(expectedBase64URL == base64URLEncoded.asString()) 30 | } 31 | 32 | @Test 33 | func encodeBase64Codable() throws { 34 | let base64 = EncodedBase64("AQABAAEBAAEAAQEAAAABAA==") 35 | let json = try JSONEncoder().encode(base64) 36 | let decodedBase64 = try JSONDecoder().decode(EncodedBase64.self, from: json) 37 | #expect(base64 == decodedBase64) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Tests/WebAuthnTests/Mocks/MockChallengeGenerator.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2023 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | @testable import WebAuthn 15 | 16 | extension ChallengeGenerator { 17 | static func mock(generate: [UInt8]) -> Self { 18 | ChallengeGenerator(generate: { generate }) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Tests/WebAuthnTests/Mocks/MockUser.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2023 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import WebAuthn 15 | 16 | extension PublicKeyCredentialUserEntity { 17 | static let mock = PublicKeyCredentialUserEntity(id: [1, 2, 3], name: "John", displayName: "Johnny") 18 | } 19 | -------------------------------------------------------------------------------- /Tests/WebAuthnTests/Utils/Hexadecimal.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2023 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import Foundation 15 | 16 | extension String { 17 | /// Create `[UInt8]` from hexadecimal string representation 18 | var hexadecimal: [UInt8]? { 19 | let hex = self 20 | guard hex.count.isMultiple(of: 2) else { 21 | return nil 22 | } 23 | 24 | let chars = hex.map { $0 } 25 | let bytes = stride(from: 0, to: chars.count, by: 2) 26 | .map { String(chars[$0]) + String(chars[$0 + 1]) } 27 | .compactMap { UInt8($0, radix: 16) } 28 | 29 | guard hex.count / bytes.count == 2 else { return nil } 30 | 31 | return bytes 32 | } 33 | } 34 | 35 | extension Data { 36 | var hexadecimal: String { 37 | return map { String(format: "%02x", $0) } 38 | .joined() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Tests/WebAuthnTests/Utils/TestModels/TestAttestationObject.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2023 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import WebAuthn 15 | @preconcurrency import SwiftCBOR 16 | import Testing 17 | 18 | // protocol AttestationObjectParameter: CBOR {} 19 | 20 | struct TestAttestationObject { 21 | var fmt: CBOR? 22 | var attStmt: CBOR? 23 | var authData: AuthData = .none 24 | 25 | enum AuthData { 26 | case structured(TestAuthData) 27 | case cbor(CBOR) 28 | case none 29 | } 30 | 31 | var cborEncoded: [UInt8] { 32 | var attestationObject: [CBOR: CBOR] = [:] 33 | if let fmt { 34 | attestationObject[.utf8String("fmt")] = fmt 35 | } 36 | if let attStmt { 37 | attestationObject[.utf8String("attStmt")] = attStmt 38 | } 39 | switch authData { 40 | case .structured(let authData): 41 | attestationObject[.utf8String("authData")] = .byteString(authData.byteArrayRepresentation) 42 | case .cbor(let authData): 43 | attestationObject[.utf8String("authData")] = authData 44 | case .none: break 45 | } 46 | 47 | return [UInt8](CBOR.map(attestationObject).encode()) 48 | } 49 | } 50 | 51 | struct TestAttestationObjectBuilder { 52 | private var wrapped: TestAttestationObject 53 | 54 | init(wrapped: TestAttestationObject = TestAttestationObject()) { 55 | self.wrapped = wrapped 56 | } 57 | 58 | func keyAgnosticBase() -> Self { 59 | var temp = self 60 | temp.wrapped.fmt = .utf8String("none") 61 | temp.wrapped.attStmt = .map([:]) 62 | return temp 63 | } 64 | 65 | func validMockECDSA() -> Self { 66 | var temp = self.keyAgnosticBase() 67 | temp.wrapped.authData = .structured(TestAuthDataBuilder().validMockECDSA().build()) 68 | return temp 69 | } 70 | 71 | func validMockRSA() -> Self { 72 | var temp = self.keyAgnosticBase() 73 | temp.wrapped.authData = .structured(TestAuthDataBuilder().validMockRSA().build()) 74 | return temp 75 | } 76 | 77 | func build() -> TestAttestationObject { 78 | return wrapped 79 | } 80 | 81 | func buildBase64URLEncoded() -> URLEncodedBase64 { 82 | build().cborEncoded.base64URLEncodedString() 83 | } 84 | 85 | // MARK: fmt 86 | 87 | func invalidFmt() -> Self { 88 | var temp = self 89 | temp.wrapped.fmt = .double(1) 90 | return temp 91 | } 92 | 93 | func fmt(_ utf8String: String) -> Self { 94 | var temp = self 95 | temp.wrapped.fmt = .utf8String(utf8String) 96 | return temp 97 | } 98 | 99 | // MARK: attStmt 100 | 101 | func invalidAttStmt() -> Self { 102 | var temp = self 103 | temp.wrapped.attStmt = .double(1) 104 | return temp 105 | } 106 | 107 | func attStmt(_ cbor: CBOR) -> Self { 108 | var temp = self 109 | temp.wrapped.attStmt = cbor 110 | return temp 111 | } 112 | 113 | func emptyAttStmt() -> Self { 114 | var temp = self 115 | temp.wrapped.attStmt = .map([:]) 116 | return temp 117 | } 118 | 119 | func missingAttStmt() -> Self { 120 | var temp = self 121 | temp.wrapped.attStmt = nil 122 | return temp 123 | } 124 | 125 | // MARK: authData 126 | 127 | func invalidAuthData() -> Self { 128 | var temp = self 129 | temp.wrapped.authData = .cbor(.double(1)) 130 | return temp 131 | } 132 | 133 | func emptyAuthData() -> Self { 134 | var temp = self 135 | temp.wrapped.authData = .cbor(.byteString([])) 136 | return temp 137 | } 138 | 139 | func zeroAuthData(byteCount: Int) -> Self { 140 | var temp = self 141 | temp.wrapped.authData = .cbor(.byteString([UInt8](repeating: 0, count: byteCount))) 142 | return temp 143 | } 144 | 145 | func authData(_ builder: TestAuthDataBuilder) -> Self { 146 | var temp = self 147 | temp.wrapped.authData = .structured(builder.build()) 148 | return temp 149 | } 150 | 151 | func authData(builder: (TestAuthDataBuilder) -> TestAuthDataBuilder) -> Self { 152 | var temp = self 153 | switch temp.wrapped.authData { 154 | case .structured(let testAuthData): 155 | temp.wrapped.authData = .structured(builder(.init(wrapped: testAuthData)).build()) 156 | case .cbor: 157 | Issue.record("authData must be structured") 158 | case .none: 159 | temp.wrapped.authData = .structured(builder(.init()).build()) 160 | } 161 | return temp 162 | } 163 | 164 | // func authData(_ builder: ) 165 | } 166 | -------------------------------------------------------------------------------- /Tests/WebAuthnTests/Utils/TestModels/TestAuthData.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2023 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import Foundation 15 | import Crypto 16 | import WebAuthn 17 | 18 | struct TestAuthData { 19 | var relyingPartyIDHash: [UInt8]? 20 | var flags: UInt8? 21 | var counter: [UInt8]? 22 | var attestedCredData: [UInt8]? 23 | var extensions: [UInt8]? 24 | 25 | var byteArrayRepresentation: [UInt8] { 26 | var value: [UInt8] = [] 27 | if let relyingPartyIDHash { 28 | value += relyingPartyIDHash 29 | } 30 | if let flags { 31 | value += [flags] 32 | } 33 | if let counter { 34 | value += counter 35 | } 36 | if let attestedCredData { 37 | value += attestedCredData 38 | } 39 | if let extensions { 40 | value += extensions 41 | } 42 | return value 43 | } 44 | } 45 | 46 | struct TestAuthDataBuilder { 47 | private var wrapped: TestAuthData 48 | 49 | init(wrapped: TestAuthData = TestAuthData()) { 50 | self.wrapped = wrapped 51 | } 52 | 53 | func build() -> TestAuthData { 54 | wrapped 55 | } 56 | 57 | func buildAsBase64URLEncoded() -> URLEncodedBase64 { 58 | build().byteArrayRepresentation.base64URLEncodedString() 59 | } 60 | 61 | func validMockECDSA() -> Self { 62 | self 63 | .relyingPartyIDHash(fromRelyingPartyID: "example.com") 64 | .flags(0b11000101) 65 | .counter([0b00000000, 0b00000000, 0b00000000, 0b00000000]) 66 | .attestedCredData( 67 | credentialIDLength: [0b00000000, 0b00000001], 68 | credentialID: [0b00000001], 69 | credentialPublicKey: TestCredentialPublicKeyBuilder().validMockECDSA().buildAsByteArray() 70 | ) 71 | .extensions([UInt8](repeating: 0, count: 20)) 72 | } 73 | 74 | func validMockRSA() -> Self { 75 | self 76 | .relyingPartyIDHash(fromRelyingPartyID: "example.com") 77 | .flags(0b11000101) 78 | .counter([0b00000000, 0b00000000, 0b00000000, 0b00000000]) 79 | .attestedCredData( 80 | credentialIDLength: [0b00000000, 0b00000001], 81 | credentialID: [0b00000001], 82 | credentialPublicKey: TestCredentialPublicKeyBuilder().validMockRSA().buildAsByteArray() 83 | ) 84 | .extensions([UInt8](repeating: 0, count: 20)) 85 | } 86 | 87 | /// Creates a valid authData 88 | /// 89 | /// relyingPartyID = "example.com", user 90 | /// flags "extension data included", "user verified" and "user present" are set 91 | /// sign count is set to 0 92 | /// random extension data is included 93 | func validAuthenticationMock() -> Self { 94 | self 95 | .relyingPartyIDHash(fromRelyingPartyID: "example.com") 96 | .flags(0b10000101) 97 | .counter([0b00000000, 0b00000000, 0b00000000, 0b00000000]) 98 | .extensions([UInt8](repeating: 0, count: 20)) 99 | } 100 | 101 | func relyingPartyIDHash(fromRelyingPartyID relyingPartyID: String) -> Self { 102 | let relyingPartyIDData = Data(relyingPartyID.utf8) 103 | let relyingPartyIDHash = SHA256.hash(data: relyingPartyIDData) 104 | var temp = self 105 | temp.wrapped.relyingPartyIDHash = [UInt8](relyingPartyIDHash) 106 | return temp 107 | } 108 | 109 | /// ED AT __ BS BE UV __ UP 110 | /// e.g.: 0b 0 1 0 0 0 0 0 1 111 | func flags(_ byte: UInt8) -> Self { 112 | var temp = self 113 | temp.wrapped.flags = byte 114 | return temp 115 | } 116 | 117 | /// A valid counter has length 4 118 | func counter(_ counter: [UInt8]) -> Self { 119 | var temp = self 120 | temp.wrapped.counter = counter 121 | return temp 122 | } 123 | 124 | /// credentialIDLength length = 2 125 | /// credentialID length = credentialIDLength 126 | /// credentialPublicKey = variable 127 | func attestedCredData( 128 | authenticatorAttestationGUID: AAGUID = .anonymous, 129 | credentialIDLength: [UInt8] = [0b00000000, 0b00000001], 130 | credentialID: [UInt8] = [0b00000001], 131 | credentialPublicKey: [UInt8] 132 | ) -> Self { 133 | var temp = self 134 | temp.wrapped.attestedCredData = authenticatorAttestationGUID.bytes + credentialIDLength + credentialID + credentialPublicKey 135 | return temp 136 | } 137 | 138 | func noAttestedCredentialData() -> Self { 139 | var temp = self 140 | temp.wrapped.attestedCredData = nil 141 | return temp 142 | } 143 | 144 | func extensions(_ extensions: [UInt8]) -> Self { 145 | var temp = self 146 | temp.wrapped.extensions = extensions 147 | return temp 148 | } 149 | 150 | func noExtensionData() -> Self { 151 | var temp = self 152 | temp.wrapped.flags = temp.wrapped.flags.map{ $0 & 0b01111111 } 153 | temp.wrapped.extensions = nil 154 | return temp 155 | } 156 | } 157 | 158 | extension TestAuthData { 159 | static var valid: Self { 160 | TestAuthData( 161 | relyingPartyIDHash: [1], 162 | flags: 1, 163 | counter: [1], 164 | attestedCredData: [2], 165 | extensions: [1] 166 | ) 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /Tests/WebAuthnTests/Utils/TestModels/TestClientDataJSON.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2023 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import Foundation 15 | import WebAuthn 16 | 17 | struct TestClientDataJSON: Encodable { 18 | var type = "webauthn.create" 19 | var challenge: URLEncodedBase64 = TestConstants.mockChallenge.base64URLEncodedString() 20 | var origin = "https://example.com" 21 | var crossOrigin = false 22 | var randomOtherKey = "123" 23 | 24 | var base64URLEncoded: URLEncodedBase64 { 25 | jsonData.base64URLEncodedString() 26 | } 27 | 28 | /// Returns this `TestClientDataJSON` as encoded json. On **Linux** this is NOT idempotent. Subsequent calls 29 | /// will result in different `Data` 30 | var jsonData: Data { 31 | // swiftlint:disable:next force_try 32 | try! JSONEncoder().encode(self) 33 | } 34 | 35 | /// Returns this `TestClientDataJSON` as encoded json. On **Linux** this is NOT idempotent. Subsequent calls 36 | /// will result in different bytes 37 | var jsonBytes: [UInt8] { 38 | [UInt8](jsonData) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Tests/WebAuthnTests/Utils/TestModels/TestConstants.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2023 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import WebAuthn 15 | 16 | struct TestConstants { 17 | /// Byte representation of string "randomStringFromServer" 18 | static let mockChallenge: [UInt8] = "72616e646f6d537472696e6746726f6d536572766572".hexadecimal! 19 | static let mockCredentialID: [UInt8] = [0, 1, 2, 3, 4] 20 | } 21 | -------------------------------------------------------------------------------- /Tests/WebAuthnTests/Utils/TestModels/TestCredentialPublicKey.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2023 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | @testable import WebAuthn 15 | @preconcurrency import SwiftCBOR 16 | 17 | struct TestCredentialPublicKey { 18 | var kty: CBOR? 19 | var alg: CBOR? 20 | // EC2, OKP 21 | var crv: CBOR? 22 | var xCoordinate: CBOR? 23 | 24 | //EC2 25 | var yCoordinate: CBOR? 26 | 27 | // RSA 28 | var nCoordinate: CBOR? 29 | var eCoordinate: CBOR? 30 | 31 | var byteArrayRepresentation: [UInt8] { 32 | var value: [CBOR: CBOR] = [:] 33 | if let kty { 34 | value[COSEKey.kty.cbor] = kty 35 | } 36 | if let alg { 37 | value[COSEKey.alg.cbor] = alg 38 | } 39 | if let crv { 40 | value[COSEKey.crv.cbor] = crv 41 | } 42 | if let xCoordinate { 43 | value[COSEKey.x.cbor] = xCoordinate 44 | } 45 | if let yCoordinate { 46 | value[COSEKey.y.cbor] = yCoordinate 47 | } 48 | 49 | if let nCoordinate { 50 | value[COSEKey.n.cbor] = nCoordinate 51 | } 52 | 53 | if let eCoordinate { 54 | value[COSEKey.e.cbor] = eCoordinate 55 | } 56 | 57 | return CBOR.map(value).encode() 58 | } 59 | } 60 | 61 | struct TestCredentialPublicKeyBuilder { 62 | var wrapped: TestCredentialPublicKey 63 | 64 | init(wrapped: TestCredentialPublicKey = TestCredentialPublicKey()) { 65 | self.wrapped = wrapped 66 | } 67 | 68 | func buildAsByteArray() -> [UInt8] { 69 | return wrapped.byteArrayRepresentation 70 | } 71 | 72 | func validMockECDSA() -> Self { 73 | return self 74 | .kty(.ellipticKey) 75 | .crv(.p256) 76 | .alg(.algES256) 77 | .xCoordinate(TestECCKeyPair.publicKeyXCoordinate) 78 | .yCoordiante(TestECCKeyPair.publicKeyYCoordinate) 79 | } 80 | 81 | func validMockRSA() -> Self { 82 | return self 83 | .kty(.rsaKey) 84 | .alg(.algRS256) 85 | .nCoordinate(TestRSAKeyPair.publicKeyNCoordinate) 86 | .eCoordiante(TestRSAKeyPair.publicKeyECoordinate) 87 | } 88 | 89 | 90 | func kty(_ kty: COSEKeyType) -> Self { 91 | var temp = self 92 | temp.wrapped.kty = .unsignedInt(kty.rawValue) 93 | return temp 94 | } 95 | 96 | func crv(_ crv: COSECurve) -> Self { 97 | var temp = self 98 | temp.wrapped.crv = .unsignedInt(crv.rawValue) 99 | return temp 100 | } 101 | 102 | func alg(_ alg: COSEAlgorithmIdentifier) -> Self { 103 | var temp = self 104 | temp.wrapped.alg = .negativeInt(UInt64(abs(alg.rawValue) - 1)) 105 | return temp 106 | } 107 | 108 | func xCoordinate(_ xCoordinate: [UInt8]) -> Self { 109 | var temp = self 110 | temp.wrapped.xCoordinate = .byteString(xCoordinate) 111 | return temp 112 | } 113 | 114 | func yCoordiante(_ yCoordinate: [UInt8]) -> Self { 115 | var temp = self 116 | temp.wrapped.yCoordinate = .byteString(yCoordinate) 117 | return temp 118 | } 119 | 120 | func nCoordinate(_ nCoordinate: [UInt8]) -> Self { 121 | var temp = self 122 | temp.wrapped.nCoordinate = .byteString(nCoordinate) 123 | return temp 124 | } 125 | 126 | func eCoordiante(_ eCoordinate: [UInt8]) -> Self { 127 | var temp = self 128 | temp.wrapped.eCoordinate = .byteString(eCoordinate) 129 | return temp 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /Tests/WebAuthnTests/Utils/TestModels/TestECCKeyPair.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2023 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import Foundation 15 | import Crypto 16 | import WebAuthn 17 | 18 | struct TestECCKeyPair: TestSigner { 19 | static let privateKeyPEM = """ 20 | -----BEGIN PRIVATE KEY----- 21 | MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgUC6oOLmd9F3Ak32L 22 | WJCzQB1eF00UX5MCzYi47hNS+zqhRANCAASWIbQJIqS1L1E8G2Z5uNSPgQGZcsfz 23 | xk1shW3jTkWmRWY3MSr+CumivsCLz0YR4OkIHm8SAxGomGYF1dO0skj4 24 | -----END PRIVATE KEY----- 25 | """ 26 | 27 | static let publicKeyPEM = """ 28 | -----BEGIN PUBLIC KEY----- 29 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEliG0CSKktS9RPBtmebjUj4EBmXLH 30 | 88ZNbIVt405FpkVmNzEq/grpor7Ai89GEeDpCB5vEgMRqJhmBdXTtLJI+A== 31 | -----END PUBLIC KEY----- 32 | """ 33 | static let publicKeyXCoordinate = "9621b40922a4b52f513c1b6679b8d48f81019972c7f3c64d6c856de34e45a645".hexadecimal! 34 | static let publicKeyYCoordinate = "6637312afe0ae9a2bec08bcf4611e0e9081e6f120311a8986605d5d3b4b248f8".hexadecimal! 35 | 36 | static func sign(data: Data) throws -> [UInt8] { 37 | let privateKey = try P256.Signing.PrivateKey(pemRepresentation: privateKeyPEM) 38 | return Array(try privateKey.signature(for: data).derRepresentation) 39 | } 40 | 41 | static var signature: [UInt8] { 42 | get throws { 43 | let authenticatorData = TestAuthDataBuilder() 44 | .validAuthenticationMock() 45 | .buildAsBase64URLEncoded() 46 | 47 | // Create a signature. This part is usually performed by the authenticator 48 | let clientData: Data = TestClientDataJSON(type: "webauthn.get").jsonData 49 | let clientDataHash = SHA256.hash(data: clientData) 50 | let rawAuthenticatorData = authenticatorData.urlDecoded.decoded! 51 | let signatureBase = rawAuthenticatorData + clientDataHash 52 | 53 | return try sign(data: signatureBase) 54 | } 55 | } 56 | } 57 | 58 | extension TestKeyConfiguration { 59 | static let ecdsa = TestKeyConfiguration( 60 | signer: TestECCKeyPair.self, 61 | credentialPublicKeyBuilder: TestCredentialPublicKeyBuilder().validMockECDSA(), 62 | authDataBuilder: TestAuthDataBuilder().validMockECDSA(), 63 | attestationObjectBuilder: TestAttestationObjectBuilder().validMockECDSA() 64 | ) 65 | } 66 | -------------------------------------------------------------------------------- /Tests/WebAuthnTests/Utils/TestModels/TestKeyConfiguration.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2025 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import Foundation 15 | 16 | protocol TestSigner { 17 | static func sign(data: Data) throws -> [UInt8] 18 | 19 | static var signature: [UInt8] { get throws } 20 | } 21 | 22 | struct TestKeyConfiguration { 23 | var signer: any TestSigner.Type 24 | var credentialPublicKeyBuilder: TestCredentialPublicKeyBuilder 25 | var authDataBuilder: TestAuthDataBuilder 26 | var attestationObjectBuilder: TestAttestationObjectBuilder 27 | 28 | var credentialPublicKey: [UInt8] { 29 | credentialPublicKeyBuilder.buildAsByteArray() 30 | } 31 | var attestationObject: [UInt8] { 32 | attestationObjectBuilder.build().cborEncoded 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Tests/WebAuthnTests/Utils/TestModels/TestRSAKeyPair.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2023 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import Foundation 15 | import Crypto 16 | import WebAuthn 17 | import _CryptoExtras 18 | 19 | struct TestRSAKeyPair: TestSigner { 20 | static let privateKeyPEM = """ 21 | -----BEGIN RSA PRIVATE KEY----- 22 | MIIEpQIBAAKCAQEAngCfNRz1D1HvyvWxURSKpGtymY/qUOW0JfQ77jc8S6p1D/78 23 | w886pOdcPkfWQbR/qN7PbwfVDHFSJW1wbMSVdwwcUa9ELMpgQIqkLoBEjohWyAT2 24 | PGKfpEskSTZfq0K/CZ+ZZ4YwNNt/IH7mZhKGQHS5SHpgRAXJuATxQmt4vFSwBp+8 25 | aN4Wmbzl+S3w2vLY2JaEPT3rL0t5WNQa2QLhH4JWBpSywe0Jl1LxWj+gOZJdZJeN 26 | c1dZtvwnhHXrwg0EjLILFf8V3GglWj8Gg6xuPo8+IQi+gjQEnDiOJpm7uhK4h7qZ 27 | iK2FzUlu4PYm/4oha+LvK7IKcjjFgyAuwq6sKQIDAQABAoIBAEoE5JDPRgatTfb4 28 | 7t6bDvBD3eYOw6iuU5zMNB8/BSI1cq3RuLxKoqCKOm563ObfFkcYSnkrZCV2GROr 29 | l1V9KsAgjku+HeQV0s2ppYybToKvYGhH2ssjMMKY6SDbNipXFIP/nrAe7wp0IbQp 30 | fuoml3outHY9zkdPptZsilGhY2hmT6oAcoOt2ZWj8mITiQgxGzbT5vQBjkyppFMm 31 | k5h+C64nS6EiJuJUUDUbvkD5hE+nHFr+165oPUmPCXGYGBiayGh8j9j7AHfGAdH3 32 | zSW+PWDMX9vApJZZauQ4FA7FDXzzFjiT3Xcqyl+yyqL/D5YA+GsSE3pbPZJcLJi2 33 | ZQ0ShbkCgYEAzhItmWbRILbXULoDfqSXYxW2XXt6Z0JFy3AaOfhbAHYUazraqByY 34 | l0AKVMVp5cH9qk2Z/s/oqIcfsBa3P8H73yBRrDJuPQpcTWuT71qjGfgqQABqzInc 35 | 71IB2F6WZ2Dnkh1uPczS+wiy8kv7Z4nM3hJhvgJ/ZGInQffYNSoiQisCgYEAxEjt 36 | gTZPwXn9PyVmwW19CN1ZNR22nzTqMDZQZMvDYCbLcMEJ+Ls3TvVX5IA5hrGWGa6v 37 | n18CdvAWebmLBww7w0FX2KF4Ug0+YGEOH15wg352dtBQ1Jx8fqeX+z+OoCjNVdiH 38 | YDpTXd7xjcs2umotisH+vo6NHnQLuBnOcGc9ZPsCgYEAiiMtZhPCRIfMtlS7Wv3C 39 | ba10Xh4T43xNhR5Utl+BwUFmVqtRQDhLIbjQNBtR7a6o+KykemessqxB1aykkpza 40 | 1qu3lBMKSujTDyL6PA0qIJJ24Ahnj00rSVJT4lMlx47yLMSFze+rzpP6QOomUTXS 41 | m1r/InxSIVyarGIUES95X5kCgYEAvnct0FZNaibfsSiv3z5JOBLh/4LHtRF5tjLe 42 | LBD1kxXSD6Wh8XRppPq5wQcTyzoDtwQlcvaUw6kRhiifWcVrMHr1rUZyJNypDIjh 43 | VVskvtQ2S/C0nrsCqzwhZDI2Sf+N0KF+K8gtIUe3CaqJfraNXroEYhCdq1FcFdck 44 | 1Tm4/4UCgYEAj+raCSTOBazoE8Z+53WUJ8Y/ZrbqEc7y7ltl6FgbZLWArETglNCD 45 | FmTawde5HZJza2x+BUJpy+31ChbaIctdu6O2tZZCa2FwdtAXf86ZJe0By4fhmK9v 46 | m0Eq9qinAmFyVbkuIzqCJMGeC1FxUYIf/DkpAMOb/ACTyig+YFgFjdU= 47 | -----END RSA PRIVATE KEY----- 48 | """ 49 | 50 | static let publicKeyPEM = """ 51 | -----BEGIN PUBLIC KEY----- 52 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAngCfNRz1D1HvyvWxURSK 53 | pGtymY/qUOW0JfQ77jc8S6p1D/78w886pOdcPkfWQbR/qN7PbwfVDHFSJW1wbMSV 54 | dwwcUa9ELMpgQIqkLoBEjohWyAT2PGKfpEskSTZfq0K/CZ+ZZ4YwNNt/IH7mZhKG 55 | QHS5SHpgRAXJuATxQmt4vFSwBp+8aN4Wmbzl+S3w2vLY2JaEPT3rL0t5WNQa2QLh 56 | H4JWBpSywe0Jl1LxWj+gOZJdZJeNc1dZtvwnhHXrwg0EjLILFf8V3GglWj8Gg6xu 57 | Po8+IQi+gjQEnDiOJpm7uhK4h7qZiK2FzUlu4PYm/4oha+LvK7IKcjjFgyAuwq6s 58 | KQIDAQAB 59 | -----END PUBLIC KEY----- 60 | """ 61 | static let publicKeyNCoordinate = [UInt8](try! _RSA.Signing.PublicKey(pemRepresentation: publicKeyPEM).getKeyPrimitives().modulus) 62 | static let publicKeyECoordinate = [UInt8](try! _RSA.Signing.PublicKey(pemRepresentation: publicKeyPEM).getKeyPrimitives().publicExponent) 63 | 64 | static func sign(data: Data) throws -> [UInt8] { 65 | let privateKey = try _RSA.Signing.PrivateKey(pemRepresentation: privateKeyPEM) 66 | return Array(try privateKey.signature(for: data,padding:_RSA.Signing.Padding.insecurePKCS1v1_5).rawRepresentation) 67 | } 68 | 69 | static var signature: [UInt8] { 70 | get throws { 71 | let authenticatorData = TestAuthDataBuilder() 72 | .validAuthenticationMock() 73 | .buildAsBase64URLEncoded() 74 | 75 | // Create a signature. This part is usually performed by the authenticator 76 | let clientData: Data = TestClientDataJSON(type: "webauthn.get").jsonData 77 | let clientDataHash = SHA256.hash(data: clientData) 78 | let rawAuthenticatorData = authenticatorData.urlDecoded.decoded! 79 | let signatureBase = rawAuthenticatorData + clientDataHash 80 | 81 | return try sign(data: signatureBase) 82 | } 83 | } 84 | } 85 | 86 | extension TestKeyConfiguration { 87 | static let rsa = TestKeyConfiguration( 88 | signer: TestRSAKeyPair.self, 89 | credentialPublicKeyBuilder: TestCredentialPublicKeyBuilder().validMockRSA(), 90 | authDataBuilder: TestAuthDataBuilder().validMockRSA(), 91 | attestationObjectBuilder: TestAttestationObjectBuilder().validMockRSA() 92 | ) 93 | } 94 | -------------------------------------------------------------------------------- /Tests/WebAuthnTests/WebAuthnManagerAuthenticationTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2022 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | @testable import WebAuthn 15 | import Testing 16 | import Foundation 17 | import SwiftCBOR 18 | import Crypto 19 | 20 | struct WebAuthnManagerAuthenticationTests { 21 | var webAuthnManager: WebAuthnManager! 22 | 23 | let challenge: [UInt8] = [1, 0, 1] 24 | let relyingPartyID = "example.com" 25 | let relyingPartyName = "Testy test" 26 | let relyingPartyOrigin = "https://example.com" 27 | 28 | init() { 29 | let configuration = WebAuthnManager.Configuration( 30 | relyingPartyID: relyingPartyID, 31 | relyingPartyName: relyingPartyName, 32 | relyingPartyOrigin: relyingPartyOrigin 33 | ) 34 | webAuthnManager = .init(configuration: configuration, challengeGenerator: .mock(generate: challenge)) 35 | } 36 | 37 | @Test 38 | func beginAuthentication() async throws { 39 | let allowCredentials: [PublicKeyCredentialDescriptor] = [.init(type: .publicKey, id: [1, 0, 2, 30])] 40 | let options = webAuthnManager.beginAuthentication( 41 | timeout: .seconds(1234), 42 | allowCredentials: allowCredentials, 43 | userVerification: .preferred 44 | ) 45 | 46 | #expect(options.challenge == challenge) 47 | #expect(options.timeout == .seconds(1234)) 48 | #expect(options.relyingPartyID == relyingPartyID) 49 | #expect(options.allowCredentials == allowCredentials) 50 | #expect(options.userVerification == .preferred) 51 | } 52 | 53 | @Test(arguments: [ 54 | TestKeyConfiguration.ecdsa, 55 | TestKeyConfiguration.rsa, 56 | ]) 57 | func finishAuthenticationFailsIfCredentialTypeIsInvalid(keyConfiguration: TestKeyConfiguration) throws { 58 | #expect(throws: WebAuthnError.invalidAssertionCredentialType) { 59 | try finishAuthentication( 60 | signature: keyConfiguration.signer.signature, 61 | type: "invalid", 62 | credentialPublicKey: keyConfiguration.credentialPublicKey 63 | ) 64 | } 65 | } 66 | 67 | @Test(arguments: [ 68 | TestKeyConfiguration.ecdsa, 69 | TestKeyConfiguration.rsa, 70 | ]) 71 | func finishAuthenticationFailsIfClientDataJSONDecodingFails(keyConfiguration: TestKeyConfiguration) throws { 72 | #expect(throws: DecodingError.self) { 73 | try finishAuthentication( 74 | clientDataJSON: [0], 75 | signature: keyConfiguration.signer.signature, 76 | credentialPublicKey: keyConfiguration.credentialPublicKey 77 | ) 78 | } 79 | } 80 | 81 | @Test(arguments: [ 82 | TestKeyConfiguration.ecdsa, 83 | TestKeyConfiguration.rsa, 84 | ]) 85 | func finishAuthenticationFailsIfCeremonyTypeDoesNotMatch(keyConfiguration: TestKeyConfiguration) throws { 86 | var clientDataJSON = TestClientDataJSON() 87 | clientDataJSON.type = "webauthn.create" 88 | #expect(throws: CollectedClientData.CollectedClientDataVerifyError.ceremonyTypeDoesNotMatch) { 89 | try finishAuthentication( 90 | clientDataJSON: clientDataJSON.jsonBytes, 91 | signature: keyConfiguration.signer.signature, 92 | credentialPublicKey: keyConfiguration.credentialPublicKey 93 | ) 94 | } 95 | } 96 | 97 | @Test(arguments: [ 98 | TestKeyConfiguration.ecdsa, 99 | TestKeyConfiguration.rsa, 100 | ]) 101 | func finishAuthenticationFailsIfRelyingPartyIDHashDoesNotMatch(keyConfiguration: TestKeyConfiguration) throws { 102 | #expect(throws: WebAuthnError.relyingPartyIDHashDoesNotMatch) { 103 | try finishAuthentication( 104 | authenticatorData: TestAuthDataBuilder() 105 | .validAuthenticationMock() 106 | .relyingPartyIDHash(fromRelyingPartyID: "wrong-id.org") 107 | .build() 108 | .byteArrayRepresentation, 109 | signature: keyConfiguration.signer.signature, 110 | credentialPublicKey: keyConfiguration.credentialPublicKey 111 | ) 112 | } 113 | } 114 | 115 | @Test(arguments: [ 116 | TestKeyConfiguration.ecdsa, 117 | TestKeyConfiguration.rsa, 118 | ]) 119 | func finishAuthenticationFailsIfUserPresentFlagIsNotSet(keyConfiguration: TestKeyConfiguration) throws { 120 | #expect(throws: WebAuthnError.userPresentFlagNotSet) { 121 | try finishAuthentication( 122 | authenticatorData: TestAuthDataBuilder() 123 | .validAuthenticationMock() 124 | .flags(0b10000000) 125 | .build() 126 | .byteArrayRepresentation, 127 | signature: keyConfiguration.signer.signature, 128 | credentialPublicKey: keyConfiguration.credentialPublicKey 129 | ) 130 | } 131 | } 132 | 133 | @Test(arguments: [ 134 | TestKeyConfiguration.ecdsa, 135 | TestKeyConfiguration.rsa, 136 | ]) 137 | func finishAuthenticationFailsIfUserIsNotVerified(keyConfiguration: TestKeyConfiguration) throws { 138 | #expect(throws: WebAuthnError.userVerifiedFlagNotSet) { 139 | try finishAuthentication( 140 | authenticatorData: TestAuthDataBuilder() 141 | .validAuthenticationMock() 142 | .flags(0b10000001) 143 | .build() 144 | .byteArrayRepresentation, 145 | signature: keyConfiguration.signer.signature, 146 | credentialPublicKey: keyConfiguration.credentialPublicKey, 147 | requireUserVerification: true 148 | ) 149 | } 150 | } 151 | 152 | @Test(arguments: [ 153 | TestKeyConfiguration.ecdsa, 154 | TestKeyConfiguration.rsa, 155 | ]) 156 | func finishAuthenticationFailsIfCredentialCounterIsNotUpToDate(keyConfiguration: TestKeyConfiguration) throws { 157 | #expect(throws: WebAuthnError.potentialReplayAttack) { 158 | try finishAuthentication( 159 | authenticatorData: TestAuthDataBuilder() 160 | .validAuthenticationMock() 161 | .counter([0, 0, 0, 1]) // signCount = 1 162 | .build() 163 | .byteArrayRepresentation, 164 | signature: keyConfiguration.signer.signature, 165 | credentialPublicKey: keyConfiguration.credentialPublicKey, 166 | credentialCurrentSignCount: 2 167 | ) 168 | } 169 | } 170 | 171 | @Test(arguments: [ 172 | TestKeyConfiguration.ecdsa, 173 | TestKeyConfiguration.rsa, 174 | ]) 175 | func finishAuthenticationSucceeds(keyConfiguration: TestKeyConfiguration) throws { 176 | let credentialID = TestConstants.mockCredentialID 177 | let oldSignCount: UInt32 = 0 178 | 179 | let authenticatorData = TestAuthDataBuilder() 180 | .validAuthenticationMock() 181 | .counter([0, 0, 0, 1]) 182 | .build() 183 | .byteArrayRepresentation 184 | 185 | // Create a signature. This part is usually performed by the authenticator 186 | 187 | // ATTENTION: It is very important that we encode `TestClientDataJSON` only once!!! Subsequent calls to 188 | // `jsonBytes` will result in different json (and thus the signature verification will fail) 189 | // This has already cost me hours of troubleshooting twice 190 | let clientData = TestClientDataJSON(type: "webauthn.get").jsonBytes 191 | let clientDataHash = SHA256.hash(data: clientData) 192 | let signatureBase = Data(authenticatorData) + clientDataHash 193 | let signature = try keyConfiguration.signer.sign(data: signatureBase) 194 | 195 | let verifiedAuthentication = try finishAuthentication( 196 | credentialID: credentialID, 197 | clientDataJSON: clientData, 198 | authenticatorData: authenticatorData, 199 | signature: [UInt8](signature), 200 | credentialPublicKey: keyConfiguration.credentialPublicKey, 201 | credentialCurrentSignCount: oldSignCount 202 | ) 203 | 204 | #expect(verifiedAuthentication.credentialID == credentialID.base64URLEncodedString()) 205 | #expect(verifiedAuthentication.newSignCount == oldSignCount + 1) 206 | } 207 | 208 | /// Using the default parameters `finishAuthentication` should succeed. 209 | private func finishAuthentication( 210 | credentialID: [UInt8] = TestConstants.mockCredentialID, 211 | clientDataJSON: [UInt8] = TestClientDataJSON(type: "webauthn.get").jsonBytes, 212 | authenticatorData: [UInt8] = TestAuthDataBuilder().validAuthenticationMock().build().byteArrayRepresentation, 213 | signature: [UInt8], 214 | userHandle: [UInt8]? = "36323638424436452d303831452d344331312d413743332d334444304146333345433134".hexadecimal!, 215 | attestationObject: [UInt8]? = nil, 216 | authenticatorAttachment: AuthenticatorAttachment? = .platform, 217 | type: CredentialType = .publicKey, 218 | expectedChallenge: [UInt8] = TestConstants.mockChallenge, 219 | credentialPublicKey: [UInt8], 220 | credentialCurrentSignCount: UInt32 = 0, 221 | requireUserVerification: Bool = false 222 | ) throws -> VerifiedAuthentication { 223 | try webAuthnManager.finishAuthentication( 224 | credential: AuthenticationCredential( 225 | id: credentialID.base64URLEncodedString(), 226 | rawID: credentialID, 227 | response: AuthenticatorAssertionResponse( 228 | clientDataJSON: clientDataJSON, 229 | authenticatorData: authenticatorData, 230 | signature: signature, 231 | userHandle: userHandle, 232 | attestationObject: attestationObject 233 | ), 234 | authenticatorAttachment: authenticatorAttachment, 235 | type: type 236 | ), 237 | expectedChallenge: expectedChallenge, 238 | credentialPublicKey: credentialPublicKey, 239 | credentialCurrentSignCount: credentialCurrentSignCount, 240 | requireUserVerification: requireUserVerification 241 | ) 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /Tests/WebAuthnTests/WebAuthnManagerIntegrationTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift WebAuthn open source project 4 | // 5 | // Copyright (c) 2023 the Swift WebAuthn project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | @testable import WebAuthn 15 | import Foundation 16 | import Testing 17 | import Crypto 18 | 19 | struct WebAuthnManagerIntegrationTests { 20 | // swiftlint:disable:next function_body_length 21 | @Test(arguments: [ 22 | TestKeyConfiguration.ecdsa, 23 | TestKeyConfiguration.rsa, 24 | ]) 25 | func registrationAndAuthenticationSucceeds(keyConfiguration: TestKeyConfiguration) async throws { 26 | let configuration = WebAuthnManager.Configuration( 27 | relyingPartyID: "example.com", 28 | relyingPartyName: "Example RP", 29 | relyingPartyOrigin: "https://example.com" 30 | ) 31 | 32 | let mockChallenge = [UInt8](repeating: 0, count: 5) 33 | let challengeGenerator = ChallengeGenerator(generate: { mockChallenge }) 34 | let webAuthnManager = WebAuthnManager(configuration: configuration, challengeGenerator: challengeGenerator) 35 | 36 | // Step 1.: Begin Registration 37 | let mockUser = PublicKeyCredentialUserEntity.mock 38 | let timeout: Duration = .seconds(1234) 39 | let attestationPreference = AttestationConveyancePreference.none 40 | let publicKeyCredentialParameters: [PublicKeyCredentialParameters] = .supported 41 | 42 | let registrationOptions = webAuthnManager.beginRegistration( 43 | user: mockUser, 44 | timeout: timeout, 45 | attestation: attestationPreference, 46 | publicKeyCredentialParameters: publicKeyCredentialParameters 47 | ) 48 | 49 | #expect(registrationOptions.challenge == mockChallenge) 50 | #expect(registrationOptions.user.id == mockUser.id) 51 | #expect(registrationOptions.user.name == mockUser.name) 52 | #expect(registrationOptions.user.displayName == mockUser.displayName) 53 | #expect(registrationOptions.attestation == attestationPreference) 54 | #expect(registrationOptions.relyingParty.id == configuration.relyingPartyID) 55 | #expect(registrationOptions.relyingParty.name == configuration.relyingPartyName) 56 | #expect(registrationOptions.timeout == timeout) 57 | #expect(registrationOptions.publicKeyCredentialParameters == publicKeyCredentialParameters) 58 | 59 | // Now send `registrationOptions` to client, which in turn will send the authenticator's response back to us: 60 | // The following lines reflect what an authenticator normally produces 61 | let mockCredentialID = [UInt8](repeating: 1, count: 10) 62 | let mockClientDataJSON = TestClientDataJSON(challenge: mockChallenge.base64URLEncodedString()) 63 | let mockCredentialPublicKey = keyConfiguration.credentialPublicKey 64 | let mockAttestationObject = keyConfiguration.attestationObjectBuilder 65 | .authData { $0 66 | .attestedCredData(credentialPublicKey: mockCredentialPublicKey) 67 | .noExtensionData() 68 | }.build().cborEncoded 69 | 70 | let registrationResponse = RegistrationCredential( 71 | id: mockCredentialID.base64URLEncodedString(), 72 | type: .publicKey, 73 | rawID: mockCredentialID, 74 | attestationResponse: AuthenticatorAttestationResponse( 75 | clientDataJSON: mockClientDataJSON.jsonBytes, 76 | attestationObject: mockAttestationObject 77 | ) 78 | ) 79 | 80 | // Step 2.: Finish Registration 81 | let credential = try await webAuthnManager.finishRegistration( 82 | challenge: mockChallenge, 83 | credentialCreationData: registrationResponse, 84 | requireUserVerification: true, 85 | supportedPublicKeyAlgorithms: publicKeyCredentialParameters, 86 | pemRootCertificatesByFormat: [:], 87 | confirmCredentialIDNotRegisteredYet: { _ in true } 88 | ) 89 | 90 | #expect(credential.id == mockCredentialID.base64EncodedString().asString()) 91 | #expect(credential.attestationClientDataJSON.type == .create) 92 | #expect(credential.attestationClientDataJSON.origin == mockClientDataJSON.origin) 93 | #expect(credential.attestationClientDataJSON.challenge == mockChallenge.base64URLEncodedString()) 94 | #expect(credential.isBackedUp == false) 95 | #expect(credential.signCount == 0) 96 | #expect(credential.type == .publicKey) 97 | #expect(credential.publicKey == mockCredentialPublicKey) 98 | 99 | // Step 3.: Begin Authentication 100 | let authenticationTimeout: Duration = .seconds(4567) 101 | let userVerification: UserVerificationRequirement = .preferred 102 | let rememberedCredentials = [PublicKeyCredentialDescriptor( 103 | type: .publicKey, 104 | id: [UInt8](URLEncodedBase64(credential.id).urlDecoded.decoded!) 105 | )] 106 | 107 | let authenticationOptions = webAuthnManager.beginAuthentication( 108 | timeout: authenticationTimeout, 109 | allowCredentials: rememberedCredentials, 110 | userVerification: userVerification 111 | ) 112 | 113 | #expect(authenticationOptions.relyingPartyID == configuration.relyingPartyID) 114 | #expect(authenticationOptions.timeout == authenticationTimeout) 115 | #expect(authenticationOptions.challenge == mockChallenge) 116 | #expect(authenticationOptions.userVerification == userVerification) 117 | #expect(authenticationOptions.allowCredentials == rememberedCredentials) 118 | 119 | // Now send `authenticationOptions` to client, which in turn will send the authenticator's response back to us: 120 | // The following lines reflect what an authenticator normally produces 121 | let authenticatorData = TestAuthDataBuilder().validAuthenticationMock() 122 | .relyingPartyIDHash(fromRelyingPartyID: configuration.relyingPartyID) 123 | .counter([0, 0, 0, 1]) // we authenticated once now, so authenticator likely increments the sign counter 124 | .build() 125 | .byteArrayRepresentation 126 | 127 | // Authenticator creates a signature with private key 128 | 129 | // ATTENTION: It is very important that we encode `TestClientDataJSON` only once!!! Subsequent calls to 130 | // `jsonBytes` will result in different json (and thus the signature verification will fail) 131 | // This has already cost me hours of troubleshooting twice 132 | let clientData = TestClientDataJSON( 133 | type: "webauthn.get", 134 | challenge: mockChallenge.base64URLEncodedString() 135 | ).jsonBytes 136 | let clientDataHash = SHA256.hash(data: clientData) 137 | let signatureBase = Data(authenticatorData + clientDataHash) 138 | let signature = try keyConfiguration.signer.sign(data: signatureBase) 139 | 140 | let authenticationCredential = AuthenticationCredential( 141 | id: mockCredentialID.base64URLEncodedString(), 142 | rawID: mockCredentialID, 143 | response: AuthenticatorAssertionResponse( 144 | clientDataJSON: clientData, 145 | authenticatorData: authenticatorData, 146 | signature: [UInt8](signature), 147 | userHandle: mockUser.id, 148 | attestationObject: nil 149 | ), 150 | authenticatorAttachment: .platform, 151 | type: .publicKey 152 | ) 153 | 154 | // Step 4.: Finish Authentication 155 | let oldSignCount: UInt32 = 0 156 | let successfullAuthentication = try webAuthnManager.finishAuthentication( 157 | credential: authenticationCredential, 158 | expectedChallenge: mockChallenge, 159 | credentialPublicKey: keyConfiguration.credentialPublicKey, 160 | credentialCurrentSignCount: oldSignCount, 161 | requireUserVerification: false 162 | ) 163 | 164 | #expect(successfullAuthentication.newSignCount == 1) 165 | #expect(successfullAuthentication.credentialBackedUp == false) 166 | #expect(successfullAuthentication.credentialDeviceType == .singleDevice) 167 | #expect(successfullAuthentication.credentialID == mockCredentialID.base64URLEncodedString()) 168 | 169 | // We did it! 170 | } 171 | } 172 | --------------------------------------------------------------------------------