Skip to content

Commit d92d409

Browse files
authored
Refactor tests, add documentation and fix bugs (#19)
* add swiftlint package plugin * remove deprecated (by WebAuthn) token bindings * add packed attestation format * add comment * remove lint plugin since its broken * clean up authentication flow * add authentication tests * add wip tpm attestation format * first readme draft * add more content * Update README.md * update readme * wip tpm verify * add TPM pubArea parsing * add wip fuzzying * wip TPM attestation format * add attestation option * add swift certificates * drop rsa and okp support temporarily * wip refactor tests * update tests * fix authentication succeeds test * rename User to WebAuthnUser * fix ci * fix ci * fix happy path test * add coments and refactor WebAuthnConfig * make VerifiedAuthentication properties public * add limitations * add comments * Split tests into two classes * Add WebAuthnConfig comments * small fixes
1 parent 1ce64d6 commit d92d409

38 files changed

+2066
-540
lines changed

.swiftlint.yml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
disabled_rules:
2-
- comment_spacing
2+
- comment_spacing
33
excluded:
4-
- .build
4+
- .build
5+
6+
7+
identifier_name:
8+
excluded:
9+
- id
10+
11+
line_length:
12+
ignores_comments: true

Package.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ let package = Package(
2626
dependencies: [
2727
.package(url: "https://github.com/unrelentingtech/SwiftCBOR.git", from: "0.4.5"),
2828
.package(url: "https://github.com/apple/swift-crypto.git", from: "2.0.0"),
29-
.package(url: "https://github.com/apple/swift-log.git", from: "1.0.0")
29+
.package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"),
30+
.package(url: "https://github.com/apple/swift-certificates.git", branch: "main")
3031
],
3132
targets: [
3233
.target(
@@ -35,7 +36,8 @@ let package = Package(
3536
"SwiftCBOR",
3637
.product(name: "Crypto", package: "swift-crypto"),
3738
.product(name: "_CryptoExtras", package: "swift-crypto"),
38-
.product(name: "Logging", package: "swift-log")
39+
.product(name: "Logging", package: "swift-log"),
40+
.product(name: "X509", package: "swift-certificates")
3941
]
4042
),
4143
.testTarget(name: "WebAuthnTests", dependencies: [

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,21 @@ interface with a client that will handle calling the WebAuthn API:
4545
- `public typealias URLEncodedBase64 = String`
4646
- `public typealias EncodedBase64 = String`
4747

48+
## Limitations
49+
50+
There are a few things this library currently does **not** support:
51+
52+
1. Currently RSA public keys are not support, we do however plan to add support for that. RSA keys are necessary for
53+
compatibility with Microsoft Windows platform authenticators.
54+
55+
2. Octet key pairs are not supported.
56+
57+
3. Attestation verification is currently not supported, we do however plan to add support for that. Some work has been
58+
done already, but there are more pieces missing. In most cases attestation verification is not recommended since it
59+
causes a lot of overhead. [From Yubico](https://developers.yubico.com/WebAuthn/WebAuthn_Developer_Guide/Attestation.html):
60+
> "If a service does not have a specific need for attestation information, namely a well defined policy for what to
61+
do with it and why, it is not recommended to verify authenticator attestations"
62+
4863
### Setup
4964

5065
Configure your backend with a `WebAuthnManager` instance:

Sources/WebAuthn/Ceremonies/Authentication/AuthenticatorAssertionResponse.swift

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15+
import Foundation
16+
import Crypto
17+
1518
/// This is what the authenticator device returned after we requested it to authenticate a user.
1619
public struct AuthenticatorAssertionResponse: Codable {
1720
/// Representation of what we passed to `navigator.credentials.get()`
@@ -29,3 +32,72 @@ public struct AuthenticatorAssertionResponse: Codable {
2932
/// data is provided directly in an AuthenticatorAssertionResponse structure.
3033
public let attestationObject: String?
3134
}
35+
36+
struct ParsedAuthenticatorAssertionResponse {
37+
let rawClientData: Data
38+
let clientData: CollectedClientData
39+
let rawAuthenticatorData: Data
40+
let authenticatorData: AuthenticatorData
41+
let signature: URLEncodedBase64
42+
let userHandle: String?
43+
44+
init(from authenticatorAssertionResponse: AuthenticatorAssertionResponse) throws {
45+
guard let clientDataData = authenticatorAssertionResponse.clientDataJSON.urlDecoded.decoded else {
46+
throw WebAuthnError.invalidClientDataJSON
47+
}
48+
rawClientData = clientDataData
49+
clientData = try JSONDecoder().decode(CollectedClientData.self, from: clientDataData)
50+
51+
guard let authenticatorDataBytes = authenticatorAssertionResponse.authenticatorData.urlDecoded.decoded else {
52+
throw WebAuthnError.invalidAuthenticatorData
53+
}
54+
rawAuthenticatorData = authenticatorDataBytes
55+
authenticatorData = try AuthenticatorData(bytes: authenticatorDataBytes)
56+
signature = authenticatorAssertionResponse.signature
57+
userHandle = authenticatorAssertionResponse.userHandle
58+
}
59+
60+
// swiftlint:disable:next function_parameter_count
61+
func verify(
62+
expectedChallenge: URLEncodedBase64,
63+
relyingPartyOrigin: String,
64+
relyingPartyID: String,
65+
requireUserVerification: Bool,
66+
credentialPublicKey: [UInt8],
67+
credentialCurrentSignCount: UInt32
68+
) throws {
69+
try clientData.verify(
70+
storedChallenge: expectedChallenge,
71+
ceremonyType: .assert,
72+
relyingPartyOrigin: relyingPartyOrigin
73+
)
74+
75+
guard let expectedRpIDData = relyingPartyID.data(using: .utf8) else {
76+
throw WebAuthnError.invalidRelyingPartyID
77+
}
78+
let expectedRpIDHash = SHA256.hash(data: expectedRpIDData)
79+
guard expectedRpIDHash == authenticatorData.relyingPartyIDHash else {
80+
throw WebAuthnError.relyingPartyIDHashDoesNotMatch
81+
}
82+
83+
guard authenticatorData.flags.userPresent else { throw WebAuthnError.userPresentFlagNotSet }
84+
if requireUserVerification {
85+
guard authenticatorData.flags.userVerified else { throw WebAuthnError.userVerifiedFlagNotSet }
86+
}
87+
88+
if authenticatorData.counter > 0 || credentialCurrentSignCount > 0 {
89+
guard authenticatorData.counter > credentialCurrentSignCount else {
90+
// This is a signal that the authenticator may be cloned, i.e. at least two copies of the credential
91+
// private key may exist and are being used in parallel.
92+
throw WebAuthnError.potentialReplayAttack
93+
}
94+
}
95+
96+
let clientDataHash = SHA256.hash(data: rawClientData)
97+
let signatureBase = rawAuthenticatorData + clientDataHash
98+
99+
let credentialPublicKey = try CredentialPublicKey(publicKeyBytes: credentialPublicKey)
100+
guard let signatureData = signature.urlDecoded.decoded else { throw WebAuthnError.invalidSignature }
101+
try credentialPublicKey.verify(signature: signatureData, data: signatureBase)
102+
}
103+
}

Sources/WebAuthn/Ceremonies/Authentication/PublicKeyCredentialRequestOptions.swift

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,31 +16,49 @@ import Foundation
1616

1717
/// The `PublicKeyCredentialRequestOptions` gets passed to the WebAuthn API (`navigator.credentials.get()`)
1818
public struct PublicKeyCredentialRequestOptions: Codable {
19+
/// A challenge that the authenticator signs, along with other data, when producing an authentication assertion
1920
public let challenge: EncodedBase64
21+
/// A `TimeInterval`, that the Relying Party is willing to wait for the call to complete. The value is treated
22+
/// as a hint, and may be overridden by the client.
2023
public let timeout: TimeInterval?
24+
/// The Relying Party ID.
2125
public let rpId: String?
26+
/// Optionally used by the client to find authenticators eligible for this authentication ceremony.
2227
public let allowCredentials: [PublicKeyCredentialDescriptor]?
28+
/// Specifies whether the user should be verified during the authentication ceremony.
2329
public let userVerification: UserVerificationRequirement?
24-
public let attestation: String?
25-
public let attestationFormats: [String]?
2630
// let extensions: [String: Any]
2731
}
2832

29-
public struct PublicKeyCredentialDescriptor: Codable {
30-
public enum AuthenticatorTransport: String, Codable {
33+
/// Information about a generated credential.
34+
public struct PublicKeyCredentialDescriptor: Codable, Equatable {
35+
/// Defines hints as to how clients might communicate with a particular authenticator in order to obtain an
36+
/// assertion for a specific credential
37+
public enum AuthenticatorTransport: String, Codable, Equatable {
38+
/// Indicates the respective authenticator can be contacted over removable USB.
3139
case usb
40+
/// Indicates the respective authenticator can be contacted over Near Field Communication (NFC).
3241
case nfc
42+
/// Indicates the respective authenticator can be contacted over Bluetooth Smart (Bluetooth Low Energy / BLE).
3343
case ble
44+
/// Indicates the respective authenticator can be contacted using a combination of (often separate)
45+
/// data-transport and proximity mechanisms. This supports, for example, authentication on a desktop
46+
/// computer using a smartphone.
3447
case hybrid
48+
/// Indicates the respective authenticator is contacted using a client device-specific transport, i.e., it is
49+
/// a platform authenticator. These authenticators are not removable from the client device.
3550
case `internal`
3651
}
3752

3853
enum CodingKeys: String, CodingKey {
3954
case type, id, transports
4055
}
4156

57+
/// Will always be 'public-key'
4258
public let type: String
59+
/// The sequence of bytes representing the credential's ID
4360
public let id: [UInt8]
61+
/// The types of connections to the client/browser the authenticator supports
4462
public let transports: [AuthenticatorTransport]
4563

4664
public init(type: String, id: [UInt8], transports: [AuthenticatorTransport] = []) {
@@ -58,8 +76,15 @@ public struct PublicKeyCredentialDescriptor: Codable {
5876
}
5977
}
6078

79+
/// The Relying Party may require user verification for some of its operations but not for others, and may use this
80+
/// type to express its needs.
6181
public enum UserVerificationRequirement: String, Codable {
82+
/// The Relying Party requires user verification for the operation and will fail the overall ceremony if the
83+
/// user wasn't verified.
6284
case required
85+
/// The Relying Party prefers user verification for the operation if possible, but will not fail the operation.
6386
case preferred
87+
/// The Relying Party does not want user verification employed during the operation (e.g., in the interest of
88+
/// minimizing disruption to the user interaction flow).
6489
case discouraged
6590
}

Sources/WebAuthn/Ceremonies/Authentication/VerifiedAuthentication.swift

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,13 @@ public struct VerifiedAuthentication {
77
case multiDevice = "multi_device"
88
}
99

10-
let credentialID: URLEncodedBase64
11-
let newSignCount: UInt32
12-
let credentialDeviceType: CredentialDeviceType
13-
let credentialBackedUp: Bool
10+
/// The credential id associated with the public key
11+
public let credentialID: URLEncodedBase64
12+
/// The updated sign count after the authentication ceremony
13+
public let newSignCount: UInt32
14+
/// Whether the authenticator is a single- or multi-device authenticator. This value is determined after
15+
/// registration and will not change afterwards.
16+
public let credentialDeviceType: CredentialDeviceType
17+
/// Whether the authenticator is known to be backed up currently
18+
public let credentialBackedUp: Bool
1419
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the WebAuthn Swift open source project
4+
//
5+
// Copyright (c) 2022 the WebAuthn Swift project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of WebAuthn Swift project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
/// Options to specify the Relying Party's preference regarding attestation conveyance during credential generation.
16+
///
17+
/// Currently only supports `none`.
18+
public enum AttestationConveyancePreference: String, Codable {
19+
/// Indicates the Relying Party is not interested in authenticator attestation.
20+
case none
21+
// case indirect
22+
// case direct
23+
// case enterprise
24+
}

Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,24 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15+
import Foundation
1516
import Crypto
1617
import SwiftCBOR
1718

1819
/// Contains the cryptographic attestation that a new key pair was created by that authenticator.
19-
public struct AttestationObject: Equatable {
20+
public struct AttestationObject {
2021
let authenticatorData: AuthenticatorData
21-
let rawAuthenticatorData: [UInt8]
22+
let rawAuthenticatorData: Data
2223
let format: AttestationFormat
2324
let attestationStatement: CBOR
2425

25-
func verify(relyingPartyID: String, verificationRequired: Bool, clientDataHash: SHA256.Digest) throws {
26+
func verify(
27+
relyingPartyID: String,
28+
verificationRequired: Bool,
29+
clientDataHash: SHA256.Digest,
30+
supportedPublicKeyAlgorithms: [PublicKeyCredentialParameters],
31+
pemRootCertificatesByFormat: [AttestationFormat: [Data]] = [:]
32+
) async throws -> AttestedCredentialData {
2633
let relyingPartyIDHash = SHA256.hash(data: relyingPartyID.data(using: .utf8)!)
2734

2835
guard relyingPartyIDHash == authenticatorData.relyingPartyIDHash else {
@@ -39,14 +46,44 @@ public struct AttestationObject: Equatable {
3946
}
4047
}
4148

49+
guard let attestedCredentialData = authenticatorData.attestedData else {
50+
throw WebAuthnError.attestedCredentialDataMissing
51+
}
52+
53+
// Step 17.
54+
let credentialPublicKey = try CredentialPublicKey(publicKeyBytes: attestedCredentialData.publicKey)
55+
guard supportedPublicKeyAlgorithms.map(\.alg).contains(credentialPublicKey.key.algorithm) else {
56+
throw WebAuthnError.unsupportedCredentialPublicKeyAlgorithm
57+
}
58+
59+
// let pemRootCertificates = pemRootCertificatesByFormat[format] ?? []
4260
switch format {
4361
case .none:
4462
// if format is `none` statement must be empty
4563
guard attestationStatement == .map([:]) else {
4664
throw WebAuthnError.attestationStatementMustBeEmpty
4765
}
66+
// case .packed:
67+
// try await PackedAttestation.verify(
68+
// attStmt: attestationStatement,
69+
// authenticatorData: rawAuthenticatorData,
70+
// clientDataHash: Data(clientDataHash),
71+
// credentialPublicKey: credentialPublicKey,
72+
// pemRootCertificates: pemRootCertificates
73+
// )
74+
// case .tpm:
75+
// try TPMAttestation.verify(
76+
// attStmt: attestationStatement,
77+
// authenticatorData: rawAuthenticatorData,
78+
// attestedCredentialData: attestedCredentialData,
79+
// clientDataHash: Data(clientDataHash),
80+
// credentialPublicKey: credentialPublicKey,
81+
// pemRootCertificates: pemRootCertificates
82+
// )
4883
default:
4984
throw WebAuthnError.attestationVerificationNotSupported
5085
}
86+
87+
return attestedCredentialData
5188
}
5289
}

Sources/WebAuthn/Ceremonies/Registration/AttestedCredentialData.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
//===----------------------------------------------------------------------===//
1414

1515
// Contains the new public key created by the authenticator.
16-
struct AttestedCredentialData: Codable, Equatable {
16+
struct AttestedCredentialData: Equatable {
1717
let aaguid: [UInt8]
1818
let credentialID: [UInt8]
1919
let publicKey: [UInt8]

Sources/WebAuthn/Ceremonies/Registration/AuthenticatorAttestationResponse.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ struct ParsedAuthenticatorAttestationResponse {
5656

5757
attestationObject = AttestationObject(
5858
authenticatorData: try AuthenticatorData(bytes: Data(authDataBytes)),
59-
rawAuthenticatorData: authDataBytes,
59+
rawAuthenticatorData: Data(authDataBytes),
6060
format: attestationFormat,
6161
attestationStatement: attestationStatement
6262
)

0 commit comments

Comments
 (0)