From 1d84f44f97d03ea00e7b0b11f218a5ca345f7902 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Scre=CC=80ve?= Date: Tue, 28 Jan 2025 11:28:55 +0100 Subject: [PATCH 1/2] Make Challenge generation more customisable --- .../WebAuthn/Helpers/ChallengeGenerator.swift | 16 +++++++++++++--- Sources/WebAuthn/WebAuthnManager.swift | 10 ++++++---- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/Sources/WebAuthn/Helpers/ChallengeGenerator.swift b/Sources/WebAuthn/Helpers/ChallengeGenerator.swift index ecbaa69..a6cac33 100644 --- a/Sources/WebAuthn/Helpers/ChallengeGenerator.swift +++ b/Sources/WebAuthn/Helpers/ChallengeGenerator.swift @@ -11,10 +11,20 @@ // //===----------------------------------------------------------------------===// -package struct ChallengeGenerator: Sendable { +import Foundation + +public struct ChallengeGenerator: Sendable { var generate: @Sendable () -> [UInt8] - package static var live: Self { - .init(generate: { [UInt8].random(count: 32) }) + public static var live: Self { + .init(generate: { + // try to use secured random generator + var bytes = [UInt8](repeating: 0, count: 32) + let result = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes) + guard result == errSecSuccess else { + return [UInt8].random(count: 32) + } + return bytes + }) } } diff --git a/Sources/WebAuthn/WebAuthnManager.swift b/Sources/WebAuthn/WebAuthnManager.swift index 1da063b..17297d9 100644 --- a/Sources/WebAuthn/WebAuthnManager.swift +++ b/Sources/WebAuthn/WebAuthnManager.swift @@ -59,9 +59,10 @@ public struct WebAuthnManager: Sendable { user: PublicKeyCredentialUserEntity, timeout: Duration? = .seconds(5*60), attestation: AttestationConveyancePreference = .none, - publicKeyCredentialParameters: [PublicKeyCredentialParameters] = .supported + publicKeyCredentialParameters: [PublicKeyCredentialParameters] = .supported, + challenge : [UInt8]? = nil ) -> PublicKeyCredentialCreationOptions { - let challenge = challengeGenerator.generate() + let challenge = challenge ?? challengeGenerator.generate() return PublicKeyCredentialCreationOptions( challenge: challenge, @@ -138,9 +139,10 @@ public struct WebAuthnManager: Sendable { public func beginAuthentication( timeout: Duration? = .seconds(60), allowCredentials: [PublicKeyCredentialDescriptor]? = nil, - userVerification: UserVerificationRequirement = .preferred + userVerification: UserVerificationRequirement = .preferred, + challenge : [UInt8]? = nil ) throws -> PublicKeyCredentialRequestOptions { - let challenge = challengeGenerator.generate() + let challenge = challenge ?? challengeGenerator.generate() return PublicKeyCredentialRequestOptions( challenge: challenge, From 352487d45b27e3608516792c3b2358a80cd0e47c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Scre=CC=80ve?= Date: Wed, 29 Jan 2025 12:17:41 +0100 Subject: [PATCH 2/2] Allow custom data addon on challenge while keeping random generation internal --- .../WebAuthn/Helpers/ChallengeGenerator.swift | 20 ++++----- Sources/WebAuthn/WebAuthnManager.swift | 24 +++++++++-- .../Mocks/MockChallengeGenerator.swift | 2 +- .../WebAuthnManagerAuthenticationTests.swift | 1 + .../WebAuthnManagerChallengeTests.swift | 43 +++++++++++++++++++ .../WebAuthnManagerIntegrationTests.swift | 8 ++-- 6 files changed, 78 insertions(+), 20 deletions(-) create mode 100644 Tests/WebAuthnTests/WebAuthnManagerChallengeTests.swift diff --git a/Sources/WebAuthn/Helpers/ChallengeGenerator.swift b/Sources/WebAuthn/Helpers/ChallengeGenerator.swift index a6cac33..a6bfb49 100644 --- a/Sources/WebAuthn/Helpers/ChallengeGenerator.swift +++ b/Sources/WebAuthn/Helpers/ChallengeGenerator.swift @@ -13,18 +13,16 @@ import Foundation -public struct ChallengeGenerator: Sendable { - var generate: @Sendable () -> [UInt8] +package struct ChallengeGenerator: Sendable { + static let challengeSize: Int = 32 + + var generate: @Sendable (_ : [UInt8]) -> [UInt8] - public static var live: Self { - .init(generate: { - // try to use secured random generator - var bytes = [UInt8](repeating: 0, count: 32) - let result = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes) - guard result == errSecSuccess else { - return [UInt8].random(count: 32) - } - return bytes + package static var live: Self { + .init(generate: { challengeData in + var randomData = [UInt8].random(count: challengeSize) + randomData.append(contentsOf: challengeData) + return randomData }) } } diff --git a/Sources/WebAuthn/WebAuthnManager.swift b/Sources/WebAuthn/WebAuthnManager.swift index 17297d9..1612caf 100644 --- a/Sources/WebAuthn/WebAuthnManager.swift +++ b/Sources/WebAuthn/WebAuthnManager.swift @@ -43,6 +43,22 @@ public struct WebAuthnManager: Sendable { self.configuration = configuration self.challengeGenerator = challengeGenerator } + + /// Extract challenge custom data from challenge + /// + /// - Parameters: + /// - challenge: The challenge to extract data from + /// + /// - Returns: The custom data part of the challenge + public func extractChallengeData(challenge : [UInt8]) -> [UInt8] { + if challenge.count <= ChallengeGenerator.challengeSize { + return [] + } + + let arrayslice = challenge.suffix(from: ChallengeGenerator.challengeSize) + return Array(arrayslice) + } + /// Generate a new set of registration data to be sent to the client. /// @@ -60,9 +76,9 @@ public struct WebAuthnManager: Sendable { timeout: Duration? = .seconds(5*60), attestation: AttestationConveyancePreference = .none, publicKeyCredentialParameters: [PublicKeyCredentialParameters] = .supported, - challenge : [UInt8]? = nil + challengeData : [UInt8] = [] ) -> PublicKeyCredentialCreationOptions { - let challenge = challenge ?? challengeGenerator.generate() + let challenge = challengeGenerator.generate(challengeData) return PublicKeyCredentialCreationOptions( challenge: challenge, @@ -140,9 +156,9 @@ public struct WebAuthnManager: Sendable { timeout: Duration? = .seconds(60), allowCredentials: [PublicKeyCredentialDescriptor]? = nil, userVerification: UserVerificationRequirement = .preferred, - challenge : [UInt8]? = nil + challengeData : [UInt8] = [] ) throws -> PublicKeyCredentialRequestOptions { - let challenge = challenge ?? challengeGenerator.generate() + let challenge = challengeGenerator.generate(challengeData) return PublicKeyCredentialRequestOptions( challenge: challenge, diff --git a/Tests/WebAuthnTests/Mocks/MockChallengeGenerator.swift b/Tests/WebAuthnTests/Mocks/MockChallengeGenerator.swift index a1aa4c1..8953a3b 100644 --- a/Tests/WebAuthnTests/Mocks/MockChallengeGenerator.swift +++ b/Tests/WebAuthnTests/Mocks/MockChallengeGenerator.swift @@ -15,6 +15,6 @@ extension ChallengeGenerator { static func mock(generate: [UInt8]) -> Self { - ChallengeGenerator(generate: { generate }) + ChallengeGenerator(generate: { _ in generate }) } } diff --git a/Tests/WebAuthnTests/WebAuthnManagerAuthenticationTests.swift b/Tests/WebAuthnTests/WebAuthnManagerAuthenticationTests.swift index 52f3752..665c5f5 100644 --- a/Tests/WebAuthnTests/WebAuthnManagerAuthenticationTests.swift +++ b/Tests/WebAuthnTests/WebAuthnManagerAuthenticationTests.swift @@ -191,4 +191,5 @@ final class WebAuthnManagerAuthenticationTests: XCTestCase { requireUserVerification: requireUserVerification ) } + } diff --git a/Tests/WebAuthnTests/WebAuthnManagerChallengeTests.swift b/Tests/WebAuthnTests/WebAuthnManagerChallengeTests.swift new file mode 100644 index 0000000..13b7233 --- /dev/null +++ b/Tests/WebAuthnTests/WebAuthnManagerChallengeTests.swift @@ -0,0 +1,43 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift WebAuthn open source project +// +// Copyright (c) 2022 the Swift WebAuthn project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@testable import WebAuthn +import XCTest +import Crypto + +final class WebAuthnManagerChallengeTests: XCTestCase { + var webAuthnManager: WebAuthnManager! + + let relyingPartyID = "example.com" + let relyingPartyName = "Testy test" + let relyingPartyOrigin = "https://example.com" + + override func setUp() { + let configuration = WebAuthnManager.Configuration( + relyingPartyID: relyingPartyID, + relyingPartyName: relyingPartyName, + relyingPartyOrigin: relyingPartyOrigin + ) + webAuthnManager = .init(configuration: configuration) + } + + func testChallengeData() async throws { + let challengeGenerator = ChallengeGenerator.live + let challengeData : [UInt8] = [12,15,48,64] + + let challenge = challengeGenerator.generate(challengeData) + let extractedData = webAuthnManager.extractChallengeData(challenge: challenge) + + XCTAssertEqual(challengeData, extractedData) + } +} diff --git a/Tests/WebAuthnTests/WebAuthnManagerIntegrationTests.swift b/Tests/WebAuthnTests/WebAuthnManagerIntegrationTests.swift index 0102045..8cc6e9d 100644 --- a/Tests/WebAuthnTests/WebAuthnManagerIntegrationTests.swift +++ b/Tests/WebAuthnTests/WebAuthnManagerIntegrationTests.swift @@ -25,7 +25,7 @@ final class WebAuthnManagerIntegrationTests: XCTestCase { ) let mockChallenge = [UInt8](repeating: 0, count: 5) - let challengeGenerator = ChallengeGenerator(generate: { mockChallenge }) + let challengeGenerator = ChallengeGenerator.mock(generate: mockChallenge ) let webAuthnManager = WebAuthnManager(configuration: configuration, challengeGenerator: challengeGenerator) // Step 1.: Begin Registration @@ -83,12 +83,12 @@ final class WebAuthnManagerIntegrationTests: XCTestCase { ) XCTAssertEqual(credential.id, mockCredentialID.base64EncodedString().asString()) - XCTAssertEqual(credential.attestationClientDataJSON.type, .create) + XCTAssertEqual(credential.attestationClientDataJSON.type, CollectedClientData.CeremonyType.create) XCTAssertEqual(credential.attestationClientDataJSON.origin, mockClientDataJSON.origin) XCTAssertEqual(credential.attestationClientDataJSON.challenge, mockChallenge.base64URLEncodedString()) XCTAssertEqual(credential.isBackedUp, false) XCTAssertEqual(credential.signCount, 0) - XCTAssertEqual(credential.type, .publicKey) + XCTAssertEqual(credential.type, CredentialType.publicKey) XCTAssertEqual(credential.publicKey, mockCredentialPublicKey) // Step 3.: Begin Authentication @@ -158,7 +158,7 @@ final class WebAuthnManagerIntegrationTests: XCTestCase { XCTAssertEqual(successfullAuthentication.newSignCount, 1) XCTAssertEqual(successfullAuthentication.credentialBackedUp, false) - XCTAssertEqual(successfullAuthentication.credentialDeviceType, .singleDevice) + XCTAssertEqual(successfullAuthentication.credentialDeviceType, VerifiedAuthentication.CredentialDeviceType.singleDevice) XCTAssertEqual(successfullAuthentication.credentialID, mockCredentialID.base64URLEncodedString()) // We did it!