Skip to content

Commit f752685

Browse files
committed
Move challenge logic into helper functions
1 parent 653cff4 commit f752685

File tree

6 files changed

+108
-44
lines changed

6 files changed

+108
-44
lines changed

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import PackageDescription
1717

1818
let package = Package(
19-
name: "WebAuthn",
19+
name: "webauthn-swift",
2020
platforms: [
2121
.macOS(.v12)
2222
],

Sources/WebAuthn/AssertionCredential.swift renamed to Sources/WebAuthn/AuthenticationResponse.swift

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,23 +14,26 @@
1414

1515
import Foundation
1616

17-
public struct AssertionCredential: Codable {
17+
public struct AuthenticationResponse: Codable {
1818
public let id: String
19-
public let type: String
20-
public let response: AssertionCredentialResponse
2119
public let rawID: String
22-
20+
public let response: AuthenticatorAssertionResponse
21+
public let authenticatorAttachment: String?
22+
/// This is the public-key
23+
public let type: String
24+
2325
enum CodingKeys: String, CodingKey {
2426
case id
2527
case rawID = "rawId"
26-
case type
2728
case response
29+
case authenticatorAttachment
30+
case type
2831
}
2932
}
3033

31-
public struct AssertionCredentialResponse: Codable {
32-
let authenticatorData: String
34+
public struct AuthenticatorAssertionResponse: Codable {
3335
let clientDataJSON: String
36+
let authenticatorData: String
3437
let signature: String
35-
let userHandle: String
38+
let userHandle: String?
3639
}

Sources/WebAuthn/AuthenticatorFlags.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
//===----------------------------------------------------------------------===//
1414

1515
struct AuthenticatorFlags {
16-
16+
1717
/**
1818
Taken from https://w3c.github.io/webauthn/#sctn-authenticator-data
1919
Bit 0: User Present Result
@@ -23,26 +23,26 @@ struct AuthenticatorFlags {
2323
Bit 6: Attested credential data included
2424
Bit 7: Extension data include
2525
*/
26-
26+
2727
enum Bit: UInt8 {
2828
case userPresent = 0
2929
case userVerified = 2
3030
case attestedCredentialDataIncluded = 6
3131
case extensionDataIncluded = 7
3232
}
33-
33+
3434
let userPresent: Bool
3535
let userVerified: Bool
3636
let attestedCredentialData: Bool
3737
let extensionDataIncluded: Bool
38-
38+
3939
init(_ byte: UInt8) {
4040
userPresent = Self.isFlagSet(on: byte, at: .userPresent)
4141
userVerified = Self.isFlagSet(on: byte, at: .userVerified)
4242
attestedCredentialData = Self.isFlagSet(on: byte, at: .attestedCredentialDataIncluded)
4343
extensionDataIncluded = Self.isFlagSet(on: byte, at: .extensionDataIncluded)
4444
}
45-
45+
4646
static func isFlagSet(on byte: UInt8, at position: Bit) -> Bool {
4747
(byte & (1 << position.rawValue)) != 0
4848
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import Foundation
2+
import Logging
3+
4+
extension WebAuthn {
5+
struct ChallengeGeneratorError: Error {}
6+
/// Generate a suitably random value to be used as an attestation or assertion challenge
7+
/// - Throws: An error if something went wrong while generating random byte
8+
/// - Returns: 32 bytes
9+
public static func generateChallenge() throws -> [UInt8] {
10+
var bytes = [UInt8](repeating: 0, count: 32)
11+
let status = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
12+
guard status == errSecSuccess else { throw ChallengeGeneratorError() }
13+
return bytes
14+
}
15+
}
16+
17+
extension Array where Element == UInt8 {
18+
/// Encodes an array of bytes into a base64url-encoded string
19+
/// - Returns: A base64url-encoded string
20+
public func base64URLEncode() -> String {
21+
let base64String = Data(bytes: self, count: self.count).base64EncodedString()
22+
return base64String.replacingOccurrences(of: "+", with: "-")
23+
.replacingOccurrences(of: "/", with: "_")
24+
.replacingOccurrences(of: "=", with: "")
25+
}
26+
}
27+
28+
extension String {
29+
/// Decode a base64url-encoded `String` to a base64 `String`
30+
/// - Returns: A base64-encoded `String`
31+
public static func base64(fromBase64URLEncoded base64URLEncoded: String) -> Self {
32+
return base64URLEncoded.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/")
33+
}
34+
}

Sources/WebAuthn/RegisterWebAuthnCredentialData.swift renamed to Sources/WebAuthn/RegistrationResponse.swift

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@
1414

1515
import Foundation
1616

17-
public struct RegisterWebAuthnCredentialData: Codable {
17+
public struct RegistrationResponse: Codable {
1818
public let id: String
1919
let rawID: String
20+
/// This is the public-key
2021
let type: String
21-
let response: RegisterCredentialsResponse
22-
22+
let response: AuthenticatorAttestationResponse
23+
2324
enum CodingKeys: String, CodingKey {
2425
case id
2526
case rawID = "rawId"
@@ -28,7 +29,7 @@ public struct RegisterWebAuthnCredentialData: Codable {
2829
}
2930
}
3031

31-
public struct RegisterCredentialsResponse: Codable {
32-
let attestationObject: String
32+
public struct AuthenticatorAttestationResponse: Codable {
3333
let clientDataJSON: String
34+
let attestationObject: String
3435
}

Sources/WebAuthn/WebAuthn.swift

Lines changed: 51 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,36 @@ import Logging
1818
import Foundation
1919

2020
public enum WebAuthn {
21-
public static func validateAssertion(_ data: AssertionCredential, challengeProvided: String, publicKey: P256.Signing.PublicKey, logger: Logger) throws {
21+
/// Verify that the user has legitimately completed the login process
22+
///
23+
/// - Parameters:
24+
/// - data: The response to verify
25+
/// - expectedChallenge: The expected base64url-encoded challenge
26+
/// - publicKey: The users public key
27+
/// - logger: A logger
28+
/// - Throws:
29+
/// - An error if the authentication response isn't valid
30+
public static func verifyAuthenticationResponse(
31+
_ data: AuthenticationResponse,
32+
expectedChallenge: String,
33+
publicKey: P256.Signing.PublicKey,
34+
// requireUserVerification: Bool = false
35+
logger: Logger
36+
) throws {
2237
guard let clientObjectData = data.response.clientDataJSON.base64URLDecodedData else {
2338
throw WebAuthnError.badRequestData
2439
}
2540
let clientObject = try JSONDecoder().decode(ClientDataObject.self, from: clientObjectData)
26-
guard challengeProvided == clientObject.challenge else {
41+
guard expectedChallenge == clientObject.challenge else {
2742
throw WebAuthnError.validationError
2843
}
2944
let clientDataJSONHash = SHA256.hash(data: clientObjectData)
30-
45+
3146
guard let authenticatorData = data.response.authenticatorData.base64URLDecodedData else {
3247
throw WebAuthnError.badRequestData
3348
}
3449
let signedData = authenticatorData + clientDataJSONHash
35-
50+
3651
guard let signatureData = data.response.signature.base64URLDecodedData else {
3752
throw WebAuthnError.badRequestData
3853
}
@@ -41,8 +56,13 @@ public enum WebAuthn {
4156
throw WebAuthnError.validationError
4257
}
4358
}
44-
45-
public static func parseRegisterCredentials(_ data: RegisterWebAuthnCredentialData, challengeProvided: String, origin: String, logger: Logger) throws -> Credential {
59+
60+
public static func parseRegisterCredentials(
61+
_ data: RegistrationResponse,
62+
challengeProvided: String,
63+
origin: String,
64+
logger: Logger
65+
) throws -> Credential {
4666
guard let clientObjectData = data.response.clientDataJSON.base64URLDecodedData else {
4767
throw WebAuthnError.badRequestData
4868
}
@@ -64,7 +84,7 @@ public enum WebAuthn {
6484
throw WebAuthnError.badRequestData
6585
}
6686
logger.debug("Got COBR decoded data: \(decodedAttestationObject)")
67-
87+
6888
// Ignore format/statement for now
6989
guard let authData = decodedAttestationObject["authData"], case let .byteString(authDataBytes) = authData else {
7090
throw WebAuthnError.badRequestData
@@ -86,11 +106,11 @@ public enum WebAuthn {
86106
// https://github.com/unrelentingtech/SwiftCBOR#swiftcbor
87107
// Negative integers are decoded as NegativeInt(UInt), where the actual number is -1 - i
88108
let algorithm: Int = -1 - Int(algorithmNegative)
89-
109+
90110
// Curve is key -1 - or -0 for SwiftCBOR
91111
// X Coordinate is key -2, or NegativeInt 1 for SwiftCBOR
92112
// Y Coordinate is key -3, or NegativeInt 2 for SwiftCBOR
93-
113+
94114
guard let curveRaw = publicKeyObject[.negativeInt(0)], case let .unsignedInt(curve) = curveRaw else {
95115
throw WebAuthnError.badRequestData
96116
}
@@ -100,54 +120,60 @@ public enum WebAuthn {
100120
guard let yCoordRaw = publicKeyObject[.negativeInt(2)], case let .byteString(yCoordinateBytes) = yCoordRaw else {
101121
throw WebAuthnError.badRequestData
102122
}
103-
123+
104124
logger.debug("Key type was \(keyType)")
105125
logger.debug("Algorithm was \(algorithm)")
106126
logger.debug("Curve was \(curve)")
107-
127+
108128
let key = try P256.Signing.PublicKey(rawRepresentation: xCoordinateBytes + yCoordinateBytes)
109129
logger.debug("Key is \(key.pemRepresentation)")
110-
130+
111131
return Credential(credentialID: data.id, publicKey: key)
112132
}
113-
114-
static func parseAttestedData(_ data: [UInt8], logger: Logger) throws -> AttestedCredentialData {
133+
134+
static func parseAttestedData(
135+
_ data: [UInt8],
136+
logger: Logger
137+
) throws -> AttestedCredentialData {
115138
// We've parsed the first 37 bytes so far, the next bytes now should be the attested credential data
116139
// See https://w3c.github.io/webauthn/#sctn-attested-credential-data
117140
let aaguidLength = 16
118141
let aaguid = data[37..<(37 + aaguidLength)] // To byte at index 52
119-
142+
120143
let idLengthBytes = data[53..<55] // Length is 2 bytes
121144
let idLengthData = Data(idLengthBytes)
122145
let idLength: UInt16 = idLengthData.toInteger(endian: .big)
123146
let credentialIDEndIndex = Int(idLength) + 55
124-
147+
125148
let credentialID = data[55..<credentialIDEndIndex]
126149
let publicKeyBytes = data[credentialIDEndIndex...]
127-
150+
128151
return AttestedCredentialData(aaguid: Array(aaguid), credentialID: Array(credentialID), publicKey: Array(publicKeyBytes))
129152
}
130-
131-
static func parseAttestationObject(_ bytes: [UInt8], logger: Logger) throws -> AttestedCredentialData? {
153+
154+
static func parseAttestationObject(
155+
_ bytes: [UInt8],
156+
logger: Logger
157+
) throws -> AttestedCredentialData? {
132158
let minAuthDataLength = 37
133159
let minAttestedAuthLength = 55
134160
// TODO - fix
135161
// let maxCredentialIDLength = 1023
136162
// What to do when we don't have this
137163
var credentialsData: AttestedCredentialData? = nil
138-
164+
139165
guard bytes.count >= minAuthDataLength else {
140166
throw WebAuthnError.authDataTooShort
141167
}
142-
168+
143169
// TODO: Use
144170
// let rpIDHashData = bytes[..<32]
145171
let flags = AuthenticatorFlags(bytes[32])
146172
// TODO: Use
147173
// let counter: UInt32 = Data(bytes[33..<37]).toInteger(endian: .big)
148-
174+
149175
var remainingCount = bytes.count - minAuthDataLength
150-
176+
151177
if flags.attestedCredentialData {
152178
guard bytes.count > minAttestedAuthLength else {
153179
throw WebAuthnError.attestedCredentialDataMissing
@@ -162,15 +188,15 @@ public enum WebAuthn {
162188
throw WebAuthnError.attestedCredentialFlagNotSet
163189
}
164190
}
165-
191+
166192
if flags.extensionDataIncluded {
167193
guard remainingCount != 0 else {
168194
throw WebAuthnError.extensionDataMissing
169195
}
170196
let extensionData = bytes[(bytes.count - remainingCount)...]
171197
remainingCount -= extensionData.count
172198
}
173-
199+
174200
guard remainingCount == 0 else {
175201
throw WebAuthnError.leftOverBytes
176202
}

0 commit comments

Comments
 (0)