Skip to content

Refactor tests, add documentation and fix bugs #19

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 35 commits into from
Mar 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
1444fe3
add swiftlint package plugin
marius-se Feb 11, 2023
aa0fed9
remove deprecated (by WebAuthn) token bindings
marius-se Feb 11, 2023
8d10736
add packed attestation format
marius-se Feb 11, 2023
fddc05e
add comment
marius-se Feb 11, 2023
3478bf6
remove lint plugin since its broken
marius-se Feb 11, 2023
8551399
clean up authentication flow
marius-se Feb 12, 2023
fca7208
add authentication tests
marius-se Feb 12, 2023
a9628d5
add wip tpm attestation format
marius-se Feb 12, 2023
a885b8c
first readme draft
marius-se Feb 3, 2023
efee659
add more content
marius-se Feb 3, 2023
52b2141
Update README.md
marius-se Feb 8, 2023
8021e66
update readme
marius-se Feb 19, 2023
3dcf451
wip tpm verify
marius-se Feb 19, 2023
a2f2a45
add TPM pubArea parsing
marius-se Feb 21, 2023
0da2398
add wip fuzzying
marius-se Feb 21, 2023
39a5934
Merge branch 'main' into marius
marius-se Feb 21, 2023
f68f50d
wip TPM attestation format
marius-se Feb 22, 2023
acb4417
add attestation option
marius-se Feb 24, 2023
058e55b
add swift certificates
marius-se Mar 7, 2023
47c7c8f
Merge branch 'main' into marius
marius-se Mar 7, 2023
cc775cd
drop rsa and okp support temporarily
marius-se Mar 7, 2023
d3a4b1a
wip refactor tests
marius-se Mar 7, 2023
a4e6607
update tests
marius-se Mar 8, 2023
f749353
fix authentication succeeds test
marius-se Mar 14, 2023
e2f4a9d
rename User to WebAuthnUser
marius-se Mar 14, 2023
3160d60
fix ci
marius-se Mar 14, 2023
603f3aa
fix ci
marius-se Mar 14, 2023
ff58bbd
fix happy path test
marius-se Mar 15, 2023
d4fa8c8
add coments and refactor WebAuthnConfig
marius-se Mar 15, 2023
2f5c9be
make VerifiedAuthentication properties public
marius-se Mar 15, 2023
e3e691f
add limitations
marius-se Mar 15, 2023
1e5f67e
add comments
marius-se Mar 20, 2023
4f8f597
Split tests into two classes
marius-se Mar 20, 2023
a1d0cf1
Add WebAuthnConfig comments
marius-se Mar 20, 2023
c7626a7
small fixes
marius-se Mar 20, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
disabled_rules:
- comment_spacing
- comment_spacing
excluded:
- .build
- .build


identifier_name:
excluded:
- id

line_length:
ignores_comments: true
6 changes: 4 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ let package = Package(
dependencies: [
.package(url: "https://github.com/unrelentingtech/SwiftCBOR.git", from: "0.4.5"),
.package(url: "https://github.com/apple/swift-crypto.git", from: "2.0.0"),
.package(url: "https://github.com/apple/swift-log.git", from: "1.0.0")
.package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-certificates.git", branch: "main")
],
targets: [
.target(
Expand All @@ -35,7 +36,8 @@ let package = Package(
"SwiftCBOR",
.product(name: "Crypto", package: "swift-crypto"),
.product(name: "_CryptoExtras", package: "swift-crypto"),
.product(name: "Logging", package: "swift-log")
.product(name: "Logging", package: "swift-log"),
.product(name: "X509", package: "swift-certificates")
]
),
.testTarget(name: "WebAuthnTests", dependencies: [
Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,21 @@ interface with a client that will handle calling the WebAuthn API:
- `public typealias URLEncodedBase64 = String`
- `public typealias EncodedBase64 = String`

## Limitations

There are a few things this library currently does **not** support:

1. Currently RSA public keys are not support, we do however plan to add support for that. RSA keys are necessary for
compatibility with Microsoft Windows platform authenticators.

2. Octet key pairs are not supported.

3. Attestation verification is currently not supported, we do however plan to add support for that. Some work has been
done already, but there are more pieces missing. In most cases attestation verification is not recommended since it
causes a lot of overhead. [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
do with it and why, it is not recommended to verify authenticator attestations"

### Setup

Configure your backend with a `WebAuthnManager` instance:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
//
//===----------------------------------------------------------------------===//

import Foundation
import Crypto

/// This is what the authenticator device returned after we requested it to authenticate a user.
public struct AuthenticatorAssertionResponse: Codable {
/// Representation of what we passed to `navigator.credentials.get()`
Expand All @@ -29,3 +32,72 @@ public struct AuthenticatorAssertionResponse: Codable {
/// data is provided directly in an AuthenticatorAssertionResponse structure.
public let attestationObject: String?
}

struct ParsedAuthenticatorAssertionResponse {
let rawClientData: Data
let clientData: CollectedClientData
let rawAuthenticatorData: Data
let authenticatorData: AuthenticatorData
let signature: URLEncodedBase64
let userHandle: String?

init(from authenticatorAssertionResponse: AuthenticatorAssertionResponse) throws {
guard let clientDataData = authenticatorAssertionResponse.clientDataJSON.urlDecoded.decoded else {
throw WebAuthnError.invalidClientDataJSON
}
rawClientData = clientDataData
clientData = try JSONDecoder().decode(CollectedClientData.self, from: clientDataData)

guard let authenticatorDataBytes = authenticatorAssertionResponse.authenticatorData.urlDecoded.decoded else {
throw WebAuthnError.invalidAuthenticatorData
}
rawAuthenticatorData = authenticatorDataBytes
authenticatorData = try AuthenticatorData(bytes: authenticatorDataBytes)
signature = authenticatorAssertionResponse.signature
userHandle = authenticatorAssertionResponse.userHandle
}

// swiftlint:disable:next function_parameter_count
func verify(
expectedChallenge: URLEncodedBase64,
relyingPartyOrigin: String,
relyingPartyID: String,
requireUserVerification: Bool,
credentialPublicKey: [UInt8],
credentialCurrentSignCount: UInt32
) throws {
try clientData.verify(
storedChallenge: expectedChallenge,
ceremonyType: .assert,
relyingPartyOrigin: relyingPartyOrigin
)

guard let expectedRpIDData = relyingPartyID.data(using: .utf8) else {
throw WebAuthnError.invalidRelyingPartyID
}
let expectedRpIDHash = SHA256.hash(data: expectedRpIDData)
guard expectedRpIDHash == authenticatorData.relyingPartyIDHash else {
throw WebAuthnError.relyingPartyIDHashDoesNotMatch
}

guard authenticatorData.flags.userPresent else { throw WebAuthnError.userPresentFlagNotSet }
if requireUserVerification {
guard authenticatorData.flags.userVerified else { throw WebAuthnError.userVerifiedFlagNotSet }
}

if authenticatorData.counter > 0 || credentialCurrentSignCount > 0 {
guard authenticatorData.counter > credentialCurrentSignCount else {
// This is a signal that the authenticator may be cloned, i.e. at least two copies of the credential
// private key may exist and are being used in parallel.
throw WebAuthnError.potentialReplayAttack
}
}

let clientDataHash = SHA256.hash(data: rawClientData)
let signatureBase = rawAuthenticatorData + clientDataHash

let credentialPublicKey = try CredentialPublicKey(publicKeyBytes: credentialPublicKey)
guard let signatureData = signature.urlDecoded.decoded else { throw WebAuthnError.invalidSignature }
try credentialPublicKey.verify(signature: signatureData, data: signatureBase)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,31 +16,49 @@ import Foundation

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

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

enum CodingKeys: String, CodingKey {
case type, id, transports
}

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

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

/// The Relying Party may require user verification for some of its operations but not for others, and may use this
/// type to express its needs.
public enum UserVerificationRequirement: String, Codable {
/// The Relying Party requires user verification for the operation and will fail the overall ceremony if the
/// user wasn't verified.
case required
/// The Relying Party prefers user verification for the operation if possible, but will not fail the operation.
case preferred
/// The Relying Party does not want user verification employed during the operation (e.g., in the interest of
/// minimizing disruption to the user interaction flow).
case discouraged
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@ public struct VerifiedAuthentication {
case multiDevice = "multi_device"
}

let credentialID: URLEncodedBase64
let newSignCount: UInt32
let credentialDeviceType: CredentialDeviceType
let credentialBackedUp: Bool
/// The credential id associated with the public key
public let credentialID: URLEncodedBase64
/// The updated sign count after the authentication ceremony
public let newSignCount: UInt32
/// Whether the authenticator is a single- or multi-device authenticator. This value is determined after
/// registration and will not change afterwards.
public let credentialDeviceType: CredentialDeviceType
/// Whether the authenticator is known to be backed up currently
public let credentialBackedUp: Bool
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the WebAuthn Swift open source project
//
// Copyright (c) 2022 the WebAuthn Swift project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of WebAuthn Swift project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

/// Options to specify the Relying Party's preference regarding attestation conveyance during credential generation.
///
/// Currently only supports `none`.
public enum AttestationConveyancePreference: String, Codable {
/// Indicates the Relying Party is not interested in authenticator attestation.
case none
// case indirect
// case direct
// case enterprise
}
43 changes: 40 additions & 3 deletions Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,24 @@
//
//===----------------------------------------------------------------------===//

import Foundation
import Crypto
import SwiftCBOR

/// Contains the cryptographic attestation that a new key pair was created by that authenticator.
public struct AttestationObject: Equatable {
public struct AttestationObject {
let authenticatorData: AuthenticatorData
let rawAuthenticatorData: [UInt8]
let rawAuthenticatorData: Data
let format: AttestationFormat
let attestationStatement: CBOR

func verify(relyingPartyID: String, verificationRequired: Bool, clientDataHash: SHA256.Digest) throws {
func verify(
relyingPartyID: String,
verificationRequired: Bool,
clientDataHash: SHA256.Digest,
supportedPublicKeyAlgorithms: [PublicKeyCredentialParameters],
pemRootCertificatesByFormat: [AttestationFormat: [Data]] = [:]
) async throws -> AttestedCredentialData {
let relyingPartyIDHash = SHA256.hash(data: relyingPartyID.data(using: .utf8)!)

guard relyingPartyIDHash == authenticatorData.relyingPartyIDHash else {
Expand All @@ -39,14 +46,44 @@ public struct AttestationObject: Equatable {
}
}

guard let attestedCredentialData = authenticatorData.attestedData else {
throw WebAuthnError.attestedCredentialDataMissing
}

// Step 17.
let credentialPublicKey = try CredentialPublicKey(publicKeyBytes: attestedCredentialData.publicKey)
guard supportedPublicKeyAlgorithms.map(\.alg).contains(credentialPublicKey.key.algorithm) else {
throw WebAuthnError.unsupportedCredentialPublicKeyAlgorithm
}

// let pemRootCertificates = pemRootCertificatesByFormat[format] ?? []
switch format {
case .none:
// if format is `none` statement must be empty
guard attestationStatement == .map([:]) else {
throw WebAuthnError.attestationStatementMustBeEmpty
}
// case .packed:
// try await PackedAttestation.verify(
// attStmt: attestationStatement,
// authenticatorData: rawAuthenticatorData,
// clientDataHash: Data(clientDataHash),
// credentialPublicKey: credentialPublicKey,
// pemRootCertificates: pemRootCertificates
// )
// case .tpm:
// try TPMAttestation.verify(
// attStmt: attestationStatement,
// authenticatorData: rawAuthenticatorData,
// attestedCredentialData: attestedCredentialData,
// clientDataHash: Data(clientDataHash),
// credentialPublicKey: credentialPublicKey,
// pemRootCertificates: pemRootCertificates
// )
Comment on lines +66 to +82
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove if not needed

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll need this once attestation verification is ready. If uncommented it should compile, but the verification flow is not done yet.

default:
throw WebAuthnError.attestationVerificationNotSupported
}

return attestedCredentialData
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
//===----------------------------------------------------------------------===//

// Contains the new public key created by the authenticator.
struct AttestedCredentialData: Codable, Equatable {
struct AttestedCredentialData: Equatable {
let aaguid: [UInt8]
let credentialID: [UInt8]
let publicKey: [UInt8]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ struct ParsedAuthenticatorAttestationResponse {

attestationObject = AttestationObject(
authenticatorData: try AuthenticatorData(bytes: Data(authDataBytes)),
rawAuthenticatorData: authDataBytes,
rawAuthenticatorData: Data(authDataBytes),
format: attestationFormat,
attestationStatement: attestationStatement
)
Expand Down
Loading