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

Migrate to Swift Testing #99

Merged
merged 12 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 4 additions & 6 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,8 @@ on:
jobs:
unit-tests:
name: Unit tests
uses: apple/swift-nio/.github/workflows/unit_tests.yml@main
uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main
with:
linux_5_9_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error"
linux_5_10_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error"
linux_6_0_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
linux_nightly_6_1_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
exclude_swift_versions: "[{\"swift_version\": \"5.8\"}, {\"swift_version\": \"5.9\"}, {\"swift_version\": \"5.10\"}]"
swift_flags: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
swift_nightly_flags: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
17 changes: 5 additions & 12 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,13 @@ jobs:
license_header_check_project_name: "Swift WebAuthn"
shell_check_enabled: false
format_check_enabled: false
license_header_check_enabled: false
docs_check_enabled: false
yamllint_check_enabled: false

unit-tests:
name: Unit tests
uses: apple/swift-nio/.github/workflows/unit_tests.yml@main
uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main
with:
linux_5_9_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error"
linux_5_10_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error"
linux_6_0_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
linux_nightly_6_1_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"

cxx-interop:
name: Cxx interop
uses: apple/swift-nio/.github/workflows/cxx_interop.yml@main
linux_exclude_swift_versions: "[{\"swift_version\": \"5.8\"}, {\"swift_version\": \"5.9\"}, {\"swift_version\": \"5.10\"}]"
windows_exclude_swift_versions: "[{\"swift_version\": \"5.8\"}, {\"swift_version\": \"5.9\"}, {\"swift_version\": \"5.10\"}]"
swift_flags: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
swift_nightly_flags: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
12 changes: 12 additions & 0 deletions .license_header_template
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@@===----------------------------------------------------------------------===@@
@@
@@ This source file is part of the Swift WebAuthn open source project
@@
@@ Copyright (c) YEARS the Swift WebAuthn project authors
@@ Licensed under Apache License v2.0
@@
@@ See LICENSE.txt for license information
@@
@@ SPDX-License-Identifier: Apache-2.0
@@
@@===----------------------------------------------------------------------===@@
2 changes: 1 addition & 1 deletion .spi.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version: 1
builder:
configs:
- documentation_targets: [WebAuthn]
- documentation_targets: [WebAuthn]
2 changes: 1 addition & 1 deletion .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ identifier_name:
- rp

line_length:
ignores_comments: true
ignores_comments: true
8 changes: 3 additions & 5 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:5.9
// swift-tools-version: 6.0
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift WebAuthn open source project
Expand Down Expand Up @@ -36,15 +36,13 @@ let package = Package(
.product(name: "Crypto", package: "swift-crypto"),
.product(name: "_CryptoExtras", package: "swift-crypto"),
.product(name: "Logging", package: "swift-log"),
],
swiftSettings: [.enableExperimentalFeature("StrictConcurrency=complete")]
]
),
.testTarget(
name: "WebAuthnTests",
dependencies: [
.target(name: "WebAuthn")
],
swiftSettings: [.enableExperimentalFeature("StrictConcurrency=complete")]
]
)
]
)
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ For an authentication ceremony use the following two methods:
- `WebAuthnManager.beginAuthentication()`
- `WebAuthnManager.finishAuthentication()`

## Contributing

If you add any new files, please run the following command at the root of the repo to identify any missing license headers:
```bash
% 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)"
```

## Credits

Swift WebAuthn is heavily inspired by existing WebAuthn libraries like
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public struct AuthenticationCredential: Sendable {
}

extension AuthenticationCredential: Decodable {
public init(from decoder: Decoder) throws {
public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

id = try container.decode(URLEncodedBase64.self, forKey: .id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public struct AuthenticatorAssertionResponse: Sendable {
}

extension AuthenticatorAssertionResponse: Decodable {
public init(from decoder: Decoder) throws {
public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

clientDataJSON = try container.decodeBytesFromURLEncodedBase64(forKey: .clientDataJSON)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public struct PublicKeyCredentialRequestOptions: Encodable, Sendable {

// let extensions: [String: Any]

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

try container.encode(challenge.base64URLEncodedString(), forKey: .challenge)
Expand Down Expand Up @@ -107,7 +107,7 @@ public struct PublicKeyCredentialDescriptor: Equatable, Encodable, Sendable {
self.transports = transports
}

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

try container.encode(type, forKey: .type)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public struct AuthenticatorAttestationResponse: Sendable {
}

extension AuthenticatorAttestationResponse: Decodable {
public init(from decoder: Decoder) throws {
public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

clientDataJSON = try container.decodeBytesFromURLEncodedBase64(forKey: .clientDataJSON)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public struct PublicKeyCredentialCreationOptions: Encodable, Sendable {
/// supported.
public let attestation: AttestationConveyancePreference

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

try container.encode(challenge.base64URLEncodedString(), forKey: .challenge)
Expand Down Expand Up @@ -142,7 +142,7 @@ public struct PublicKeyCredentialUserEntity: Encodable, Sendable {
self.displayName = displayName
}

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

try container.encode(id.base64URLEncodedString(), forKey: .id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public struct RegistrationCredential: Sendable {
}

extension RegistrationCredential: Decodable {
public init(from decoder: Decoder) throws {
public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

id = try container.decode(URLEncodedBase64.self, forKey: .id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,12 @@ public struct AuthenticatorAttestationGloballyUniqueID: Hashable, Sendable {
public typealias AAGUID = AuthenticatorAttestationGloballyUniqueID

extension AuthenticatorAttestationGloballyUniqueID: Codable {
public init(from decoder: Decoder) throws {
public init(from decoder: any Decoder) throws {
let container = try decoder.singleValueContainer()
id = try container.decode(UUID.self)
}

public func encode(to encoder: Encoder) throws {
public func encode(to encoder: any Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(id)
}
Expand Down
36 changes: 19 additions & 17 deletions Sources/WebAuthn/Ceremonies/Shared/AuthenticatorData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ struct AuthenticatorData: Equatable, Sendable {
}

extension AuthenticatorData {
init(bytes: [UInt8]) throws {
init(bytes: [UInt8]) throws(WebAuthnError) {
let minAuthDataLength = 37
guard bytes.count >= minAuthDataLength else {
throw WebAuthnError.authDataTooShort
throw .authDataTooShort
}

let relyingPartyIDHash = Array(bytes[..<32])
Expand All @@ -44,29 +44,29 @@ extension AuthenticatorData {
if flags.attestedCredentialData {
let minAttestedAuthLength = 37 + AAGUID.size + 2
guard bytes.count > minAttestedAuthLength else {
throw WebAuthnError.attestedCredentialDataMissing
throw .attestedCredentialDataMissing
}
let (data, length) = try Self.parseAttestedData(bytes)
attestedCredentialData = data
remainingCount -= length
// For assertion signatures, the AT flag MUST NOT be set and the attestedCredentialData MUST NOT be included.
} else {
if !flags.extensionDataIncluded && bytes.count != minAuthDataLength {
throw WebAuthnError.attestedCredentialFlagNotSet
throw .attestedCredentialFlagNotSet
}
}

var extensionData: [UInt8]?
if flags.extensionDataIncluded {
guard remainingCount != 0 else {
throw WebAuthnError.extensionDataMissing
throw .extensionDataMissing
}
extensionData = Array(bytes[(bytes.count - remainingCount)...])
remainingCount -= extensionData!.count
}

guard remainingCount == 0 else {
throw WebAuthnError.leftOverBytesInAuthenticatorData
throw .leftOverBytesInAuthenticatorData
}

self.relyingPartyIDHash = relyingPartyIDHash
Expand All @@ -81,31 +81,33 @@ extension AuthenticatorData {
///
/// This is assumed to take place after the first 37 bytes of `data`, which is always of fixed size.
/// - 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) {
private static func parseAttestedData(_ data: [UInt8]) throws(WebAuthnError) -> (AttestedCredentialData, Int) {
/// **aaguid** (16): The AAGUID of the authenticator.
guard let aaguid = AAGUID(bytes: data[37..<(37 + AAGUID.size)]) // Bytes [37-52]
else { throw WebAuthnError.attestedCredentialDataMissing }
else { throw .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(bigEndianBytes: idLengthData)

guard idLength <= 1023
else { throw WebAuthnError.credentialIDTooLong }
else { throw .credentialIDTooLong }

let credentialIDEndIndex = Int(idLength) + 55
guard data.count >= credentialIDEndIndex
else { throw WebAuthnError.credentialIDTooShort }
else { throw .credentialIDTooShort }

/// **credentialId** (L): Credential ID
let credentialID = data[55..<credentialIDEndIndex]

/// **credentialPublicKey** (variable): The credential public key encoded in `COSE_Key` format, as defined in [Section 7](https://tools.ietf.org/html/rfc9052#section-7) of [RFC9052], using the CTAP2 canonical CBOR encoding form.
/// Assuming valid CBOR, verify the public key's length by decoding the next CBOR item.
/// Assuming valid CBOR, verify the public key's length by decoding the next CBOR item, and checking how much data is left on the stream.
let inputStream = ByteInputStream(data[credentialIDEndIndex...])
let decoder = CBORDecoder(stream: inputStream, options: CBOROptions(maximumDepth: 16))
_ = try decoder.decodeItem()
do {
let decoder = CBORDecoder(stream: inputStream, options: CBOROptions(maximumDepth: 16))
_ = try decoder.decodeItem()
} catch { throw .invalidPublicKeyLength }
let publicKeyBytes = data[credentialIDEndIndex..<(data.count - inputStream.remainingBytes)]

let data = AttestedCredentialData(
Expand All @@ -132,13 +134,13 @@ class ByteInputStream: CBORInputStream {
/// The remaining bytes in the original data buffer.
var remainingBytes: Int { slice.count }

func popByte() throws -> UInt8 {
if slice.count < 1 { throw CBORError.unfinishedSequence }
func popByte() throws(CBORError) -> UInt8 {
if slice.count < 1 { throw .unfinishedSequence }
return slice.removeFirst()
}

func popBytes(_ n: Int) throws -> ArraySlice<UInt8> {
if slice.count < n { throw CBORError.unfinishedSequence }
func popBytes(_ n: Int) throws(CBORError) -> ArraySlice<UInt8> {
if slice.count < n { throw .unfinishedSequence }
let result = slice.prefix(n)
slice = slice.dropFirst(n)
return result
Expand Down
8 changes: 4 additions & 4 deletions Sources/WebAuthn/Ceremonies/Shared/CollectedClientData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ public struct CollectedClientData: Codable, Hashable, Sendable {
public let challenge: URLEncodedBase64
public let origin: String

func verify(storedChallenge: [UInt8], ceremonyType: CeremonyType, relyingPartyOrigin: String) throws {
guard type == ceremonyType else { throw CollectedClientDataVerifyError.ceremonyTypeDoesNotMatch }
func verify(storedChallenge: [UInt8], ceremonyType: CeremonyType, relyingPartyOrigin: String) throws(CollectedClientDataVerifyError) {
guard type == ceremonyType else { throw .ceremonyTypeDoesNotMatch }
guard challenge == storedChallenge.base64URLEncodedString() else {
throw CollectedClientDataVerifyError.challengeDoesNotMatch
throw .challengeDoesNotMatch
}
guard origin == relyingPartyOrigin else { throw CollectedClientDataVerifyError.originDoesNotMatch }
guard origin == relyingPartyOrigin else { throw .originDoesNotMatch }
}
}
20 changes: 10 additions & 10 deletions Sources/WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ struct EC2PublicKey: PublicKey, Sendable {
self.yCoordinate = yCoordinate
}

init(publicKeyObject: CBOR, algorithm: COSEAlgorithmIdentifier) throws {
init(publicKeyObject: CBOR, algorithm: COSEAlgorithmIdentifier) throws(WebAuthnError) {
self.algorithm = algorithm

// Curve is key -1 - or -0 for SwiftCBOR
Expand All @@ -117,18 +117,18 @@ struct EC2PublicKey: PublicKey, Sendable {
guard let curveRaw = publicKeyObject[COSEKey.crv.cbor],
case let .unsignedInt(curve) = curveRaw,
let coseCurve = COSECurve(rawValue: curve) else {
throw WebAuthnError.invalidCurve
throw .invalidCurve
}
self.curve = coseCurve

guard let xCoordRaw = publicKeyObject[COSEKey.x.cbor],
case let .byteString(xCoordinateBytes) = xCoordRaw else {
throw WebAuthnError.invalidXCoordinate
throw .invalidXCoordinate
}
xCoordinate = xCoordinateBytes
guard let yCoordRaw = publicKeyObject[COSEKey.y.cbor],
case let .byteString(yCoordinateBytes) = yCoordRaw else {
throw WebAuthnError.invalidYCoordinate
throw .invalidYCoordinate
}
yCoordinate = yCoordinateBytes
}
Expand Down Expand Up @@ -167,18 +167,18 @@ struct RSAPublicKeyData: PublicKey, Sendable {

var rawRepresentation: [UInt8] { n + e }

init(publicKeyObject: CBOR, algorithm: COSEAlgorithmIdentifier) throws {
init(publicKeyObject: CBOR, algorithm: COSEAlgorithmIdentifier) throws(WebAuthnError) {
self.algorithm = algorithm

guard let nRaw = publicKeyObject[COSEKey.n.cbor],
case let .byteString(nBytes) = nRaw else {
throw WebAuthnError.invalidModulus
throw .invalidModulus
}
n = nBytes

guard let eRaw = publicKeyObject[COSEKey.e.cbor],
case let .byteString(eBytes) = eRaw else {
throw WebAuthnError.invalidExponent
throw .invalidExponent
}
e = eBytes
}
Expand Down Expand Up @@ -213,17 +213,17 @@ struct OKPPublicKey: PublicKey, Sendable {
let curve: UInt64
let xCoordinate: [UInt8]

init(publicKeyObject: CBOR, algorithm: COSEAlgorithmIdentifier) throws {
init(publicKeyObject: CBOR, algorithm: COSEAlgorithmIdentifier) throws(WebAuthnError) {
self.algorithm = algorithm
// Curve is key -1, or NegativeInt 0 for SwiftCBOR
guard let curveRaw = publicKeyObject[.negativeInt(0)], case let .unsignedInt(curve) = curveRaw else {
throw WebAuthnError.invalidCurve
throw .invalidCurve
}
self.curve = curve
// X Coordinate is key -2, or NegativeInt 1 for SwiftCBOR
guard let xCoordRaw = publicKeyObject[.negativeInt(1)],
case let .byteString(xCoordinateBytes) = xCoordRaw else {
throw WebAuthnError.invalidXCoordinate
throw .invalidXCoordinate
}
xCoordinate = xCoordinateBytes
}
Expand Down
Loading