Skip to content
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

Swift Client Implementation #70

Draft
wants to merge 29 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
1c14d1b
Configured minimum requirements for other apple platforms
dimitribouniol Feb 24, 2024
60e6815
Added new AuthenticatorProtocol for modular authenticators
dimitribouniol Feb 24, 2024
b90a0f4
Added AuthenticatorCredentialSourceIdentifier for representing creden…
dimitribouniol Feb 24, 2024
bab5128
Updated PublicKeyCredentialParameters to be hashable
dimitribouniol Feb 26, 2024
2b1fe1a
Updated AuthenticatorProtocol to include essential members for regist…
dimitribouniol Feb 24, 2024
8d7b74f
Updated AuthenticatorData and related objects to be byte encodable
dimitribouniol Feb 15, 2024
e6bd5b4
Added AttestationRegistrationRequest to assist with registration atte…
dimitribouniol Feb 24, 2024
ea34e76
Updated PublicKeyCredentialParameters to be Codable
dimitribouniol Feb 25, 2024
7bc6bda
Added KeyPairAuthenticator for authenticating with unattested private…
dimitribouniol Feb 24, 2024
9c97bea
Added start of WebAuthn client registration implementation
dimitribouniol Feb 12, 2024
179e0e3
Added basic key management and signing procedures
dimitribouniol Feb 13, 2024
4db01ce
Added byte encoding support to CredentialPublicKey
dimitribouniol Feb 15, 2024
ff2fcaa
Added a typed initializer to AuthenticatorData
dimitribouniol Feb 25, 2024
b1c51af
Implemented credential generation procedure
dimitribouniol Feb 16, 2024
924d3fd
Added helper types for dealing with continuations that can be cancelled
dimitribouniol Feb 17, 2024
09e080d
Updated client to properly call into registration attestation before …
dimitribouniol Feb 17, 2024
4c6515f
Updated AuthenticatorData members to be mutable
dimitribouniol Feb 25, 2024
a6779d3
Implemented remaining client registration procedure
dimitribouniol Feb 18, 2024
05cf264
Updated RegistrationCredential to be Codable
dimitribouniol Feb 18, 2024
37bd51c
Updated PublicKeyCredentialCreationOptions to be Codable
dimitribouniol Feb 21, 2024
233870a
Added AssertionAuthenticationRequest to assist with authentication as…
dimitribouniol Feb 24, 2024
52f5a3e
Added basic outline for client authentication
dimitribouniol Feb 19, 2024
c416dcb
Updated PublicKeyCredentialRequestOptions to be Codable
dimitribouniol Feb 20, 2024
c05656a
Updated AuthenticationCredential to be Codable
dimitribouniol Feb 20, 2024
2f0dc0c
Added documentation steps for client authentication
dimitribouniol Feb 20, 2024
d4caffa
Added basic client authentication ceremony
dimitribouniol Feb 23, 2024
b192e4a
Added assertion signing to authenticators
dimitribouniol Feb 23, 2024
f21ce6f
Added client registration and authentication integration tests
dimitribouniol Feb 28, 2024
817b02e
Updated client to use task groups instead of custom cancellable child…
dimitribouniol Jun 11, 2024
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
5 changes: 4 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ import PackageDescription
let package = Package(
name: "swift-webauthn",
platforms: [
.macOS(.v13)
.macOS(.v13),
.iOS(.v16),
.tvOS(.v16),
.watchOS(.v9),
],
products: [
.library(name: "WebAuthn", targets: ["WebAuthn"])
Expand Down
215 changes: 215 additions & 0 deletions Sources/WebAuthn/Authenticators/KeyPairAuthenticator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the WebAuthn Swift open source project
//
// Copyright (c) 2024 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
//
//===----------------------------------------------------------------------===//

import Foundation
@preconcurrency import Crypto

public struct KeyPairAuthenticator: AuthenticatorProtocol, Sendable {
public let attestationGloballyUniqueID: AAGUID
public let attachmentModality: AuthenticatorAttachment
public let supportedPublicKeyCredentialParameters: Set<PublicKeyCredentialParameters>

/// As the credentials are directly supplied by the caller, ``KeyPairAuthenticator``s are always capable of performing user verification, though they can be initialized to indicate silent authorization was performed if relevant.
public let canPerformUserVerification: Bool = true
public let canStoreCredentialSourceClientSide: Bool = true

/// The specific subset the client fully supports, in case more are added over time.
static let implementedPublicKeyCredentialParameterSubset: Set<PublicKeyCredentialParameters> = [
PublicKeyCredentialParameters(alg: .algES256),
PublicKeyCredentialParameters(alg: .algES384),
PublicKeyCredentialParameters(alg: .algES512),
]

/// Generate credentials for the full subset the implementation supports.
///
/// This list must match those supported in ``KeyPairAuthenticator/implementedPublicKeyCredentialParameterSubset``.
static func generateCredentialSourceKey(for chosenCredentialParameters: PublicKeyCredentialParameters) -> CredentialSource.Key {
switch chosenCredentialParameters.alg {
case .algES256: .es256(P256.Signing.PrivateKey(compactRepresentable: false))
case .algES384: .es384(P384.Signing.PrivateKey(compactRepresentable: false))
case .algES512: .es521(P521.Signing.PrivateKey(compactRepresentable: false))
}
}

/// Initialize a key-pair based authenticator with a globally unique ID representing your application.
/// - Note: To generate an AAGUID, run `% uuidgen` in your terminal. This value should generally not change across installations or versions of your app, and should be the same for every user.
/// - Parameter attestationGloballyUniqueID: The AAGUID associated with the authenticator.
/// - Parameter attachmentModality: The connected-nature of the authenticator to the device the client is running on. If credential keys can roam between devices, specify ``AuthenticatorModality/crossPlatform``. Set to ``AuthenticatorModality/platform`` by default.
/// - Parameter supportedPublicKeyCredentialParameters: A customized set of key credentials the authenticator will limit support to.
public init(
attestationGloballyUniqueID: AAGUID,
attachmentModality: AuthenticatorAttachment = .platform,
supportedPublicKeyCredentialParameters: Set<PublicKeyCredentialParameters> = .supported
) {
self.attestationGloballyUniqueID = attestationGloballyUniqueID
self.attachmentModality = attachmentModality
self.supportedPublicKeyCredentialParameters = supportedPublicKeyCredentialParameters.intersection(Self.implementedPublicKeyCredentialParameterSubset)
}

public func generateCredentialSource(
requiresClientSideKeyStorage: Bool,
credentialParameters: PublicKeyCredentialParameters,
relyingPartyID: PublicKeyCredentialRelyingPartyEntity.ID,
userHandle: PublicKeyCredentialUserEntity.ID
) async throws -> CredentialSource {
CredentialSource(
id: UUID(),
key: Self.generateCredentialSourceKey(for: credentialParameters),
relyingPartyID: relyingPartyID,
userHandle: userHandle,
counter: 0
)
}

public func filteredCredentialDescriptors(
credentialDescriptors: [PublicKeyCredentialDescriptor],
relyingPartyID: PublicKeyCredentialRelyingPartyEntity.ID
) -> [PublicKeyCredentialDescriptor] {
return credentialDescriptors
}

public func collectAuthorizationGesture(
requiresUserVerification: Bool,
requiresUserPresence: Bool,
credentialOptions: [CredentialSource]
) async throws -> CredentialSource {
guard let credentialSource = credentialOptions.first
else { throw WebAuthnError.authorizationGestureNotAllowed }

return credentialSource
}
}

extension KeyPairAuthenticator {
public struct CredentialSource: AuthenticatorCredentialSourceProtocol, Sendable {
public enum Key: Sendable {
case es256(P256.Signing.PrivateKey)
case es384(P384.Signing.PrivateKey)
case es521(P521.Signing.PrivateKey)
}

public var id: UUID
public var key: Key
public var relyingPartyID: PublicKeyCredentialRelyingPartyEntity.ID
public var userHandle: PublicKeyCredentialUserEntity.ID
public var counter: UInt32

public var credentialParameters: PublicKeyCredentialParameters {
switch key {
case .es256: PublicKeyCredentialParameters(alg: .algES256)
case .es384: PublicKeyCredentialParameters(alg: .algES384)
case .es521: PublicKeyCredentialParameters(alg: .algES512)
}
}

public var rawKeyData: Data {
switch key {
case .es256(let privateKey): privateKey.rawRepresentation
case .es384(let privateKey): privateKey.rawRepresentation
case .es521(let privateKey): privateKey.rawRepresentation
}
}

public var publicKey: PublicKey {
switch key {
case .es256(let privateKey): EC2PublicKey(privateKey.publicKey)
case .es384(let privateKey): EC2PublicKey(privateKey.publicKey)
case .es521(let privateKey): EC2PublicKey(privateKey.publicKey)
}
}

public init(
id: ID,
key: Key,
relyingPartyID: PublicKeyCredentialRelyingPartyEntity.ID,
userHandle: PublicKeyCredentialUserEntity.ID,
counter: UInt32
) {
self.id = id
self.key = key
self.relyingPartyID = relyingPartyID
self.userHandle = userHandle
self.counter = 0
}

public init(
id: ID,
credentialParameters: PublicKeyCredentialParameters,
rawKeyData: some ContiguousBytes,
relyingPartyID: PublicKeyCredentialRelyingPartyEntity.ID,
userHandle: PublicKeyCredentialUserEntity.ID,
counter: UInt32
) throws {
guard credentialParameters.type == .publicKey
else { throw WebAuthnError.unsupportedCredentialPublicKeyType }

self.id = id
switch credentialParameters.alg {
case .algES256: key = .es256(try P256.Signing.PrivateKey(rawRepresentation: rawKeyData))
case .algES384: key = .es384(try P384.Signing.PrivateKey(rawRepresentation: rawKeyData))
case .algES512: key = .es521(try P521.Signing.PrivateKey(rawRepresentation: rawKeyData))
}
self.relyingPartyID = relyingPartyID
self.userHandle = userHandle
self.counter = counter
}

public func signAssertion(
authenticatorData: [UInt8],
clientDataHash: SHA256Digest
) throws -> [UInt8] {
let digest = authenticatorData + clientDataHash
return switch key {
case .es256(let privateKey): Array(try privateKey.signature(for: digest).derRepresentation)
case .es384(let privateKey): Array(try privateKey.signature(for: digest).derRepresentation)
case .es521(let privateKey): Array(try privateKey.signature(for: digest).derRepresentation)
}
}
}
}

extension KeyPairAuthenticator.CredentialSource: Codable {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

try self.init(
id: try container.decode(UUID.self, forKey: .id),
credentialParameters: try container.decode(PublicKeyCredentialParameters.self, forKey: .credentialParameters),
rawKeyData: try container.decode(Data.self, forKey: .key),
relyingPartyID: try container.decode(String.self, forKey: .relyingPartyID),
userHandle: PublicKeyCredentialUserEntity.ID(try container.decode(Data.self, forKey: .userHandle)),
counter: try container.decode(UInt32.self, forKey: .counter)
)
}

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)

try container.encode(id, forKey: .id)
try container.encode(credentialParameters, forKey: .credentialParameters)
try container.encode(rawKeyData, forKey: .key)
try container.encode(relyingPartyID, forKey: .relyingPartyID)
try container.encode(Data(userHandle), forKey: .userHandle)
try container.encode(counter, forKey: .counter)
}

enum CodingKeys: CodingKey {
case id
case credentialParameters
case key
case relyingPartyID
case userHandle
case counter
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the WebAuthn Swift open source project
//
// Copyright (c) 2024 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
//
//===----------------------------------------------------------------------===//

@preconcurrency import Crypto

public struct AssertionAuthenticationRequest: Sendable {
public var options: PublicKeyCredentialRequestOptions
public var clientDataHash: SHA256Digest

init(
options: PublicKeyCredentialRequestOptions,
clientDataHash: SHA256Digest
) {
self.options = options
self.clientDataHash = clientDataHash
}
}

extension AssertionAuthenticationRequest {
public struct Results: Sendable {
public var credentialID: [UInt8]
public var authenticatorData: [UInt8]
public var signature: [UInt8]
public var userHandle: [UInt8]?
public var authenticatorAttachment: AuthenticatorAttachment

public init(
credentialID: [UInt8],
authenticatorData: [UInt8],
signature: [UInt8],
userHandle: [UInt8]? = nil,
authenticatorAttachment: AuthenticatorAttachment
) {
self.credentialID = credentialID
self.authenticatorData = authenticatorData
self.signature = signature
self.userHandle = userHandle
self.authenticatorAttachment = authenticatorAttachment
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the WebAuthn Swift open source project
//
// Copyright (c) 2024 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
//
//===----------------------------------------------------------------------===//

@preconcurrency import Crypto
import SwiftCBOR

public struct AttestationRegistrationRequest: Sendable {
var options: PublicKeyCredentialCreationOptions
var publicKeyCredentialParameters: [PublicKeyCredentialParameters]
var clientDataHash: SHA256Digest

init(
options: PublicKeyCredentialCreationOptions,
publicKeyCredentialParameters: [PublicKeyCredentialParameters],
clientDataHash: SHA256Digest
) {
self.options = options
self.publicKeyCredentialParameters = publicKeyCredentialParameters
self.clientDataHash = clientDataHash
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the WebAuthn Swift open source project
//
// Copyright (c) 2024 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
//
//===----------------------------------------------------------------------===//

import Foundation

public protocol AuthenticatorCredentialSourceIdentifier: Hashable, Sendable {
init?(bytes: some BidirectionalCollection<UInt8>)
var bytes: [UInt8] { get }
}

extension UUID: AuthenticatorCredentialSourceIdentifier {
public init?(bytes: some BidirectionalCollection<UInt8>) {
let uuidSize = MemoryLayout<uuid_t>.size
guard bytes.count == uuidSize else { return nil }

/// Either load it directly, or copy it to a new array to load the uuid from there.
let uuid = bytes.withContiguousStorageIfAvailable {
$0.withUnsafeBytes {
$0.loadUnaligned(as: uuid_t.self)
}
} ?? Array(bytes).withUnsafeBytes {
$0.loadUnaligned(as: uuid_t.self)
}
self = UUID(uuid: uuid)
}

public var bytes: [UInt8] { withUnsafeBytes(of: self) { Array($0) } }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the WebAuthn Swift open source project
//
// Copyright (c) 2024 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
//
//===----------------------------------------------------------------------===//

import Foundation
import Crypto

public protocol AuthenticatorCredentialSourceProtocol: Sendable, Identifiable where ID: AuthenticatorCredentialSourceIdentifier {

var id: ID { get }
var credentialParameters: PublicKeyCredentialParameters { get }
var relyingPartyID: PublicKeyCredentialRelyingPartyEntity.ID { get }
var userHandle: PublicKeyCredentialUserEntity.ID { get }
var counter: UInt32 { get }

var publicKey: PublicKey { get }

func signAssertion(
authenticatorData: [UInt8],
clientDataHash: SHA256Digest
) async throws -> [UInt8]
}
Loading