Skip to content

Commit db58569

Browse files
committed
add statement verification
1 parent 462c3ab commit db58569

9 files changed

+159
-33
lines changed

Sources/WebAuthn/Authenticator/AttestationObject/AttestationObject.swift

Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,15 @@
1313
//===----------------------------------------------------------------------===//
1414

1515
import Crypto
16+
import SwiftCBOR
1617

1718
struct AttestationObject {
1819
let authenticatorData: AuthenticatorData
1920
let rawAuthenticatorData: [UInt8]
2021
let format: AttestationFormat
21-
let attestationStatement: [String: Any]
22+
let attestationStatement: CBOR
2223

23-
func verify(relyingPartyID: String, verificationRequired: Bool) throws {
24+
func verify(relyingPartyID: String, verificationRequired: Bool, clientDataHash: SHA256.Digest) throws {
2425
let relyingPartyIDHash = SHA256.hash(data: relyingPartyID.data(using: .utf8)!)
2526

2627
// Step 12.
@@ -40,31 +41,29 @@ struct AttestationObject {
4041
}
4142
}
4243

43-
// Step 15.
44-
if authenticatorData.flags.isBackupEligible {
45-
fatalError("Not implemented yet")
46-
}
47-
48-
// Step 16.
49-
if authenticatorData.flags.isCurrentlyBackedUp {
50-
fatalError("Not implemented yet")
51-
}
52-
5344
// Step 17. happening somewhere else (maybe we can move it here?)
5445

5546
// Attestation format already determined. Skipping step 19.
5647

5748
// Step 20.
58-
// TODO: Implement case .packed first! fatalError the rest
59-
// switch format {
60-
// case .androidKey:
61-
// case .androidSafetynet:
62-
// case .apple:
63-
// case .fidoU2F:
64-
// case .packed:
65-
// case .tpm:
66-
// case .none:
67-
// guard attestationStatement.isEmpty else { throw WebAuthnError.attestationStatementMissing }
68-
// }
49+
switch format {
50+
case .androidKey:
51+
fatalError("Not implemented")
52+
case .androidSafetynet:
53+
fatalError("Not implemented")
54+
case .apple:
55+
fatalError("Not implemented")
56+
case .fidoU2F:
57+
fatalError("Not implemented")
58+
case .packed:
59+
try AttestationStatementVerification.verifyPacked(attestationObject: self, clientDataHash: clientDataHash)
60+
case .tpm:
61+
fatalError("Not implemented")
62+
case .none:
63+
// if format is `none` statement must be empty
64+
guard attestationStatement == .map([:]) else {
65+
throw WebAuthnError.attestationStatementMissing
66+
}
67+
}
6968
}
7069
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import SwiftCBOR
2+
import Crypto
3+
4+
struct AttestationStatementVerification {
5+
static func verifyPacked(attestationObject: AttestationObject, clientDataHash: SHA256.Digest) throws {
6+
let statement = attestationObject.attestationStatement
7+
// guard let formatCBOR = decodedAttestationObject["fmt"], case let .utf8String(format) = formatCBOR else {
8+
guard let algCBOR = statement["alg"],
9+
case let .negativeInt(alg) = algCBOR,
10+
let coseAlg = COSEAlgorithmIdentifier(rawValue: -1 - Int(alg)) else {
11+
throw PackedError.invalidOrMissingAlg
12+
}
13+
14+
guard let sigCBOR = statement["sig"],
15+
case let .byteString(sig) = sigCBOR else {
16+
throw PackedError.invalidOrMissingSig
17+
}
18+
19+
let verificationData = attestationObject.rawAuthenticatorData + clientDataHash
20+
21+
if let x5cCBOR = statement["x5c"],
22+
case let .array(certificates) = x5cCBOR,
23+
case let .byteString(attestnCert) = certificates.first {
24+
// Basic or AttCA attestation
25+
26+
print("X5C CERTIFICATE CHAIN VALIDATION NOT IMPLEMENTED YET")
27+
} else {
28+
// Self Attestation
29+
30+
}
31+
}
32+
}
33+
34+
extension AttestationStatementVerification {
35+
enum PackedError: Error {
36+
case invalidOrMissingAlg
37+
case invalidOrMissingSig
38+
}
39+
}

Sources/WebAuthn/Authenticator/AuthenticatorAttestationResponse.swift

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,12 @@ struct ParsedAuthenticatorAttestationResponse {
4949
guard let formatCBOR = decodedAttestationObject["fmt"], case let .utf8String(format) = formatCBOR else {
5050
throw WebAuthnError.formatError
5151
}
52-
let attestationStatement = decodedAttestationObject["attStmt"]
52+
53+
guard let attestationStatement = decodedAttestationObject["attStmt"] else {
54+
throw WebAuthnError.missingAttestationFormat
55+
}
56+
57+
// use `format` to decode attestationStatement
5358

5459
guard let attestationFormat = AttestationFormat(rawValue: format) else {
5560
throw WebAuthnError.unsupportedAttestationFormat
@@ -59,10 +64,14 @@ struct ParsedAuthenticatorAttestationResponse {
5964
authenticatorData: try ParsedAuthenticatorAttestationResponse.parseAuthenticatorData(authDataBytes),
6065
rawAuthenticatorData: authDataBytes,
6166
format: attestationFormat,
62-
attestationStatement: [:]
67+
attestationStatement: attestationStatement
6368
)
6469
}
6570

71+
private static func parseAttestationStatement(format: AttestationFormat, statement: CBOR) throws {
72+
73+
}
74+
6675
private static func parseAuthenticatorData(_ bytes: [UInt8]) throws -> AuthenticatorData {
6776
let minAuthDataLength = 37
6877
guard bytes.count >= minAuthDataLength else {

Sources/WebAuthn/Ceremonies/Registration/CredentialCreationResponse.swift

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,27 @@ import Foundation
1616

1717
/// The unprocessed response received from `navigator.credentials.create()`.
1818
/// Internally this will be parsed into a more readable `ParsedCredentialCreationResponse`.
19-
public struct CredentialCreationResponse: Codable {
20-
let id: String
21-
let type: String
22-
let rawID: URLEncodedBase64
19+
public struct CredentialCreationResponse {
20+
public let id: String
21+
public let type: String
22+
public let rawID: URLEncodedBase64
2323
/// Likely the wrong datatype, it should be more like [String: Any]?
24-
let clientExtensionResults: [String: String]?
25-
let attestationResponse: AuthenticatorAttestationResponse
24+
public let clientExtensionResults: [String: String]?
25+
public let attestationResponse: AuthenticatorAttestationResponse
26+
27+
public init(
28+
id: String,
29+
type: String,
30+
rawID: URLEncodedBase64,
31+
clientExtensionResults: [String: String]?,
32+
attestationResponse: AuthenticatorAttestationResponse
33+
) {
34+
self.id = id
35+
self.type = type
36+
self.rawID = rawID
37+
self.clientExtensionResults = clientExtensionResults
38+
self.attestationResponse = attestationResponse
39+
}
2640

2741
enum CodingKeys: String, CodingKey {
2842
case id
@@ -32,3 +46,7 @@ public struct CredentialCreationResponse: Codable {
3246
case attestationResponse = "response"
3347
}
3448
}
49+
50+
extension CredentialCreationResponse: Codable {
51+
52+
}

Sources/WebAuthn/Ceremonies/Registration/ParsedCredentialCreationResponse.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ struct ParsedCredentialCreationResponse {
6060
// Step 12. - 17.
6161
try response.attestationObject.verify(
6262
relyingPartyID: relyingPartyID,
63-
verificationRequired: verifyUser
63+
verificationRequired: verifyUser,
64+
clientDataHash: hash
6465
)
6566
}
6667
}

Sources/WebAuthn/Helpers/Base64Utilities.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import Foundation
1616
import Logging
1717

18-
typealias URLEncodedBase64 = String
18+
public typealias URLEncodedBase64 = String
1919

2020
extension Array where Element == UInt8 {
2121
/// Encodes an array of bytes into a base64url-encoded string

Sources/WebAuthn/WebAuthnError.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public enum WebAuthnError: Error {
2929
case relyingPartyIDHashDoesNotMatch
3030
case attestationStatementMissing
3131
case missingAttestedCredentialData
32+
case missingAttestationFormat
3233

3334
case invalidRawID
3435
case invalidCredentialCreationType
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import XCTest
2+
@testable import WebAuthn
3+
4+
final class CredentialCreationResponseTests: XCTestCase {
5+
func testDecodingFromJSONSucceeds() throws {
6+
// swiftlint:disable line_length
7+
let json = """
8+
{"id":"Oj7lbcq6vsDvL0t_DuKOETF8LPf_lygwRA1j_Lqn4ms","rawId":"Oj7lbcq6vsDvL0t_DuKOETF8LPf_lygwRA1j_Lqn4ms","type":"public-key","response":{"attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEcwRQIgNTRtpI_SOOZVzU1pN_4cX-osqUPiHMOW48qqq91DXfUCIQC-MHiaIxt2OdIxgqYnyUDHceevNOMfPibenabQGvXgjGhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAK3OAAI1vMYKZIsLJfHwVQMAIDo-5W3Kur7A7y9Lfw7ijhExfCz3_5coMEQNY_y6p-JrpQECAyYgASFYIJr_yLoYbYWgcf7aQcd7pcjUj-3o8biafWQH28WijQSvIlggPI2KqqRQ26KKuFaJ0yH7nouCBrzHu8qRONW-CPa9VDM","clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiY21GdVpHOXRVM1J5YVc1blJuSnZiVk5sY25abGNnIiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwIiwiY3Jvc3NPcmlnaW4iOmZhbHNlLCJvdGhlcl9rZXlzX2Nhbl9iZV9hZGRlZF9oZXJlIjoiZG8gbm90IGNvbXBhcmUgY2xpZW50RGF0YUpTT04gYWdhaW5zdCBhIHRlbXBsYXRlLiBTZWUgaHR0cHM6Ly9nb28uZ2wveWFiUGV4In0"}}
9+
"""
10+
11+
let response = try JSONDecoder().decode(CredentialCreationResponse.self, from: json.data(using: .utf8)!)
12+
13+
XCTAssertEqual(response.id, "Oj7lbcq6vsDvL0t_DuKOETF8LPf_lygwRA1j_Lqn4ms")
14+
XCTAssertEqual(response.rawID, "Oj7lbcq6vsDvL0t_DuKOETF8LPf_lygwRA1j_Lqn4ms")
15+
XCTAssertEqual(response.type, "public-key")
16+
XCTAssertEqual(
17+
response.attestationResponse.clientDataJSON,
18+
"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiY21GdVpHOXRVM1J5YVc1blJuSnZiVk5sY25abGNnIiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwIiwiY3Jvc3NPcmlnaW4iOmZhbHNlLCJvdGhlcl9rZXlzX2Nhbl9iZV9hZGRlZF9oZXJlIjoiZG8gbm90IGNvbXBhcmUgY2xpZW50RGF0YUpTT04gYWdhaW5zdCBhIHRlbXBsYXRlLiBTZWUgaHR0cHM6Ly9nb28uZ2wveWFiUGV4In0"
19+
)
20+
XCTAssertEqual(
21+
response.attestationResponse.attestationObject,
22+
"o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEcwRQIgNTRtpI_SOOZVzU1pN_4cX-osqUPiHMOW48qqq91DXfUCIQC-MHiaIxt2OdIxgqYnyUDHceevNOMfPibenabQGvXgjGhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAK3OAAI1vMYKZIsLJfHwVQMAIDo-5W3Kur7A7y9Lfw7ijhExfCz3_5coMEQNY_y6p-JrpQECAyYgASFYIJr_yLoYbYWgcf7aQcd7pcjUj-3o8biafWQH28WijQSvIlggPI2KqqRQ26KKuFaJ0yH7nouCBrzHu8qRONW-CPa9VDM"
23+
)
24+
// swiftlint:enable line_length
25+
}
26+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import XCTest
2+
@testable import WebAuthn
3+
4+
final class ParsedCredentialCreationResponseTests: XCTestCase {
5+
func testParsingSucceeds() throws {
6+
// swiftlint:disable line_length
7+
let response = CredentialCreationResponse(
8+
id: "Oj7lbcq6vsDvL0t_DuKOETF8LPf_lygwRA1j_Lqn4ms",
9+
type: "public-key",
10+
rawID: "Oj7lbcq6vsDvL0t_DuKOETF8LPf_lygwRA1j_Lqn4ms",
11+
clientExtensionResults: nil,
12+
attestationResponse: AuthenticatorAttestationResponse(
13+
clientDataJSON: "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiY21GdVpHOXRVM1J5YVc1blJuSnZiVk5sY25abGNnIiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwIiwiY3Jvc3NPcmlnaW4iOmZhbHNlLCJvdGhlcl9rZXlzX2Nhbl9iZV9hZGRlZF9oZXJlIjoiZG8gbm90IGNvbXBhcmUgY2xpZW50RGF0YUpTT04gYWdhaW5zdCBhIHRlbXBsYXRlLiBTZWUgaHR0cHM6Ly9nb28uZ2wveWFiUGV4In0",
14+
attestationObject: "o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEcwRQIgNTRtpI_SOOZVzU1pN_4cX-osqUPiHMOW48qqq91DXfUCIQC-MHiaIxt2OdIxgqYnyUDHceevNOMfPibenabQGvXgjGhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAK3OAAI1vMYKZIsLJfHwVQMAIDo-5W3Kur7A7y9Lfw7ijhExfCz3_5coMEQNY_y6p-JrpQECAyYgASFYIJr_yLoYbYWgcf7aQcd7pcjUj-3o8biafWQH28WijQSvIlggPI2KqqRQ26KKuFaJ0yH7nouCBrzHu8qRONW-CPa9VDM"
15+
)
16+
)
17+
18+
let parsedResponse = try ParsedCredentialCreationResponse(from: response)
19+
20+
print(parsedResponse)
21+
22+
let expectedRawID: [UInt8] = [58, 62, 229, 109, 202, 186, 190, 192, 239, 47, 75, 127, 14, 226, 142, 17, 49, 124, 44, 247, 255, 151, 40, 48, 68, 13, 99, 252, 186, 167, 226, 107]
23+
// let expectedResponse = ParsedAuthenticatorAttestationResponse(clientData: .init, attestationObject: .init(clientDataJSON: URLEncodedBase64, attestationObject: String))
24+
25+
XCTAssertEqual(parsedResponse.id, "Oj7lbcq6vsDvL0t_DuKOETF8LPf_lygwRA1j_Lqn4ms")
26+
XCTAssertEqual(parsedResponse.type, "public-key")
27+
XCTAssertEqual(parsedResponse.rawID, Data(bytes: expectedRawID, count: expectedRawID.count))
28+
XCTAssertEqual(parsedResponse.clientExtensionResults, nil)
29+
// XCTAssertEqual(parsedResponse.response, nil)
30+
31+
// swiftlint:enable line_length
32+
}
33+
}

0 commit comments

Comments
 (0)