diff --git a/Sources/WebAuthn/Ceremonies/Registration/AttestedCredentialData.swift b/Sources/WebAuthn/Ceremonies/Registration/AttestedCredentialData.swift index d264e97..ffed88d 100644 --- a/Sources/WebAuthn/Ceremonies/Registration/AttestedCredentialData.swift +++ b/Sources/WebAuthn/Ceremonies/Registration/AttestedCredentialData.swift @@ -14,7 +14,7 @@ // Contains the new public key created by the authenticator. struct AttestedCredentialData: Equatable { - let aaguid: [UInt8] + let authenticatorAttestationGUID: AAGUID let credentialID: [UInt8] let publicKey: [UInt8] } diff --git a/Sources/WebAuthn/Ceremonies/Shared/AuthenticatorAttestationGloballyUniqueID.swift b/Sources/WebAuthn/Ceremonies/Shared/AuthenticatorAttestationGloballyUniqueID.swift new file mode 100644 index 0000000..548ac45 --- /dev/null +++ b/Sources/WebAuthn/Ceremonies/Shared/AuthenticatorAttestationGloballyUniqueID.swift @@ -0,0 +1,77 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +/// A globally unique ID identifying an authenticator. +/// +/// 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. +/// +/// 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. +/// - SeeAlso: [WebAuthn Leven 3 Editor's Draft §6. WebAuthn Authenticator Model](https://w3c.github.io/webauthn/#aaguid) +public struct AuthenticatorAttestationGloballyUniqueID: Hashable, Sendable { + /// The underlying UUID for the authenticator. + public let id: UUID + + /// Initialize an AAGUID with a UUID. + @inlinable + public init(uuid: UUID) { + self.id = uuid + } + + /// Initialize an AAGUID with a byte sequence. + /// + /// This sequence must be of length ``AuthenticatorAttestationGloballyUniqueID/size``. + @inlinable + public init?(bytes: some BidirectionalCollection) { + let uuidSize = MemoryLayout.size + assert(uuidSize == Self.size, "Size of uuid_t (\(uuidSize)) does not match Self.size (\(Self.size))!") + guard bytes.count == uuidSize else { return nil } + self.init(uuid: UUID(uuid: bytes.casting())) + } + + /// Initialize an AAGUID with a string-based UUID. + @inlinable + public init?(uuidString: String) { + guard let uuid = UUID(uuidString: uuidString) + else { return nil } + + self.init(uuid: uuid) + } + + /// Access the AAGUID as an encoded byte sequence. + @inlinable + public var bytes: [UInt8] { withUnsafeBytes(of: id) { Array($0) } } + + /// The identifier of an anonymized authenticator, set to a byte sequence of 16 zeros. + public static let anonymous = AuthenticatorAttestationGloballyUniqueID(bytes: Array(repeating: 0, count: Self.size))! + + /// The byte length of an encoded identifer. + public static let size: Int = 16 +} + +/// A shorthand for an ``AuthenticatorAttestationGloballyUniqueID`` +public typealias AAGUID = AuthenticatorAttestationGloballyUniqueID + +extension AuthenticatorAttestationGloballyUniqueID: Codable { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + id = try container.decode(UUID.self) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(id) + } +} diff --git a/Sources/WebAuthn/Ceremonies/Shared/AuthenticatorData.swift b/Sources/WebAuthn/Ceremonies/Shared/AuthenticatorData.swift index d39203b..41cc250 100644 --- a/Sources/WebAuthn/Ceremonies/Shared/AuthenticatorData.swift +++ b/Sources/WebAuthn/Ceremonies/Shared/AuthenticatorData.swift @@ -36,14 +36,14 @@ extension AuthenticatorData { let relyingPartyIDHash = Array(bytes[..<32]) let flags = AuthenticatorFlags(bytes[32]) - let counter: UInt32 = Data(bytes[33..<37]).toInteger(endian: .big) + let counter = UInt32(bigEndianBytes: bytes[33..<37]) var remainingCount = bytes.count - minAuthDataLength var attestedCredentialData: AttestedCredentialData? // For attestation signatures, the authenticator MUST set the AT flag and include the attestedCredentialData. if flags.attestedCredentialData { - let minAttestedAuthLength = 55 + let minAttestedAuthLength = 37 + AAGUID.size + 2 guard bytes.count > minAttestedAuthLength else { throw WebAuthnError.attestedCredentialDataMissing } @@ -84,13 +84,13 @@ extension AuthenticatorData { /// - SeeAlso: [WebAuthn Level 3 Editor's Draft §6.5.1. Attested Credential Data]( https://w3c.github.io/webauthn/#sctn-attested-credential-data) private static func parseAttestedData(_ data: [UInt8]) throws -> (AttestedCredentialData, Int) { /// **aaguid** (16): The AAGUID of the authenticator. - let aaguidLength = 16 - let aaguid = data[37..<(37 + aaguidLength)] // To byte at index 52 + guard let aaguid = AAGUID(bytes: data[37..<(37 + AAGUID.size)]) // Bytes [37-52] + else { throw WebAuthnError.attestedCredentialDataMissing } /// **credentialIdLength** (2): Byte length L of credentialId, 16-bit unsigned big-endian integer. Value MUST be ≤ 1023. let idLengthBytes = data[53..<55] // Length is 2 bytes let idLengthData = Data(idLengthBytes) - let idLength: UInt16 = idLengthData.toInteger(endian: .big) + let idLength = UInt16(bigEndianBytes: idLengthData) guard idLength <= 1023 else { throw WebAuthnError.credentialIDTooLong } @@ -110,13 +110,13 @@ extension AuthenticatorData { let publicKeyBytes = data[credentialIDEndIndex..<(data.count - inputStream.remainingBytes)] let data = AttestedCredentialData( - aaguid: Array(aaguid), + authenticatorAttestationGUID: aaguid, credentialID: Array(credentialID), publicKey: Array(publicKeyBytes) ) /// `2` is the size of **credentialIdLength** - let length = data.aaguid.count + 2 + data.credentialID.count + data.publicKey.count + let length = AAGUID.size + 2 + data.credentialID.count + data.publicKey.count return (data, length) } diff --git a/Sources/WebAuthn/Helpers/ByteCasting.swift b/Sources/WebAuthn/Helpers/ByteCasting.swift new file mode 100644 index 0000000..611f02d --- /dev/null +++ b/Sources/WebAuthn/Helpers/ByteCasting.swift @@ -0,0 +1,49 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +extension BidirectionalCollection where Element == UInt8 { + /// Cast a byte sequence into a trivial type like a primitive or a tuple of primitives. + /// + /// - Note: It is up to the caller to verify the receiver's size before casting it. + @inlinable + func casting() -> R { + precondition(self.count == MemoryLayout.size, "self.count (\(self.count)) does not match MemoryLayout.size (\(MemoryLayout.size))") + + let result = self.withContiguousStorageIfAvailable({ + $0.withUnsafeBytes { $0.loadUnaligned(as: R.self) } + }) ?? Array(self).withUnsafeBytes { + $0.loadUnaligned(as: R.self) + } + + return result + } +} + +extension FixedWidthInteger { + /// Initialize a fixed width integer from a contiguous sequence of Bytes representing a big endian type. + /// - Parameter bigEndianBytes: The Bytes to interpret as a big endian integer. + @inlinable + init(bigEndianBytes: some BidirectionalCollection) { + self.init(bigEndian: bigEndianBytes.casting()) + } + + /// Initialize a fixed width integer from a contiguous sequence of Bytes representing a little endian type. + /// - Parameter bigEndianBytes: The Bytes to interpret as a little endian integer. + @inlinable + init(littleEndianBytes: some BidirectionalCollection) { + self.init(littleEndian: littleEndianBytes.casting()) + } +} diff --git a/Sources/WebAuthn/Helpers/Numbers+Bytes.swift b/Sources/WebAuthn/Helpers/Numbers+Bytes.swift deleted file mode 100644 index d02ee1f..0000000 --- a/Sources/WebAuthn/Helpers/Numbers+Bytes.swift +++ /dev/null @@ -1,34 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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 -// -//===----------------------------------------------------------------------===// - -import Foundation - -public enum Endian { - case big, little -} - -protocol IntegerTransform: Sequence where Element: FixedWidthInteger { - func toInteger(endian: Endian) -> I -} - -extension IntegerTransform { - func toInteger(endian: Endian) -> I { - // swiftlint:disable:next identifier_name - let f = { (accum: I, next: Element) in accum &<< next.bitWidth | I(next) } - return endian == .big ? reduce(0, f) : reversed().reduce(0, f) - } -} - -extension Data: IntegerTransform {} -extension Array: IntegerTransform where Element: FixedWidthInteger {} diff --git a/Tests/WebAuthnTests/AuthenticatorAttestationGloballyUniqueIDTests.swift b/Tests/WebAuthnTests/AuthenticatorAttestationGloballyUniqueIDTests.swift new file mode 100644 index 0000000..4161761 --- /dev/null +++ b/Tests/WebAuthnTests/AuthenticatorAttestationGloballyUniqueIDTests.swift @@ -0,0 +1,33 @@ +//===----------------------------------------------------------------------===// +// +// 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 XCTest +@testable import WebAuthn + +final class AuthenticatorAttestationGloballyUniqueIDTests: XCTestCase { + func testByteCoding() throws { + let aaguid = AuthenticatorAttestationGloballyUniqueID(bytes: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]) + XCTAssertNotNil(aaguid) + XCTAssertEqual(aaguid?.bytes, [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f]) + XCTAssertEqual(aaguid?.id, UUID(uuid: (0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f))) + XCTAssertEqual(aaguid, AuthenticatorAttestationGloballyUniqueID(uuid: UUID(uuid: (0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f)))) + XCTAssertEqual(aaguid, AuthenticatorAttestationGloballyUniqueID(uuidString: "00010203-0405-0607-0809-0A0B0C0D0E0F" )) + } + + func testInvalidByteDecoding() throws { + XCTAssertNil(AuthenticatorAttestationGloballyUniqueID(bytes: [])) + XCTAssertNil(AuthenticatorAttestationGloballyUniqueID(bytes: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14])) + XCTAssertNil(AuthenticatorAttestationGloballyUniqueID(bytes: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16])) + } +} diff --git a/Tests/WebAuthnTests/Utils/TestModels/TestAuthData.swift b/Tests/WebAuthnTests/Utils/TestModels/TestAuthData.swift index 899c79b..856ff33 100644 --- a/Tests/WebAuthnTests/Utils/TestModels/TestAuthData.swift +++ b/Tests/WebAuthnTests/Utils/TestModels/TestAuthData.swift @@ -65,7 +65,6 @@ struct TestAuthDataBuilder { .flags(0b11000101) .counter([0b00000000, 0b00000000, 0b00000000, 0b00000000]) .attestedCredData( - aaguid: [UInt8](repeating: 0, count: 16), credentialIDLength: [0b00000000, 0b00000001], credentialID: [0b00000001], credentialPublicKey: TestCredentialPublicKeyBuilder().validMock().buildAsByteArray() @@ -110,18 +109,17 @@ struct TestAuthDataBuilder { return temp } - /// aaguid length = 16 /// credentialIDLength length = 2 /// credentialID length = credentialIDLength /// credentialPublicKey = variable func attestedCredData( - aaguid: [UInt8] = [UInt8](repeating: 0, count: 16), + authenticatorAttestationGUID: AAGUID = .anonymous, credentialIDLength: [UInt8] = [0b00000000, 0b00000001], credentialID: [UInt8] = [0b00000001], credentialPublicKey: [UInt8] ) -> Self { var temp = self - temp.wrapped.attestedCredData = aaguid + credentialIDLength + credentialID + credentialPublicKey + temp.wrapped.attestedCredData = authenticatorAttestationGUID.bytes + credentialIDLength + credentialID + credentialPublicKey return temp } diff --git a/Tests/WebAuthnTests/WebAuthnManagerRegistrationTests.swift b/Tests/WebAuthnTests/WebAuthnManagerRegistrationTests.swift index 58b3b79..fa1f081 100644 --- a/Tests/WebAuthnTests/WebAuthnManagerRegistrationTests.swift +++ b/Tests/WebAuthnTests/WebAuthnManagerRegistrationTests.swift @@ -301,7 +301,6 @@ final class WebAuthnManagerRegistrationTests: XCTestCase { TestAuthDataBuilder() .validMock() .attestedCredData( - aaguid: Array(repeating: 0, count: 16), credentialIDLength: [0b000_00011, 0b1111_1111], credentialID: Array(repeating: 0, count: 1023), credentialPublicKey: TestCredentialPublicKeyBuilder().validMock().buildAsByteArray() @@ -320,7 +319,6 @@ final class WebAuthnManagerRegistrationTests: XCTestCase { TestAuthDataBuilder() .validMock() .attestedCredData( - aaguid: Array(repeating: 0, count: 16), credentialIDLength: [0b000_00100, 0b0000_0000], credentialID: Array(repeating: 0, count: 1024), credentialPublicKey: TestCredentialPublicKeyBuilder().validMock().buildAsByteArray()