Skip to content

Commit 27dcb3c

Browse files
committed
add more tests
1 parent 7b94b64 commit 27dcb3c

9 files changed

+160
-15
lines changed

Sources/WebAuthn/Ceremonies/Registration/AttestationFormat.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15-
public enum AttestationFormat: String, RawRepresentable {
15+
public enum AttestationFormat: String, RawRepresentable, Equatable {
1616
case packed
1717
case tpm
1818
case androidKey = "android-key"

Sources/WebAuthn/Ceremonies/Registration/AttestationObject.swift

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

1818
/// Contains the cryptographic attestation that a new key pair was created by that authenticator.
19-
public struct AttestationObject {
19+
public struct AttestationObject: Equatable {
2020
let authenticatorData: AuthenticatorData
2121
let rawAuthenticatorData: [UInt8]
2222
let format: AttestationFormat

Sources/WebAuthn/Ceremonies/Registration/AttestedCredentialData.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
//===----------------------------------------------------------------------===//
1414

1515
// Contains the new public key created by the authenticator.
16-
struct AttestedCredentialData: Codable {
16+
struct AttestedCredentialData: Codable, Equatable {
1717
let aaguid: [UInt8]
1818
let credentialID: [UInt8]
1919
let publicKey: [UInt8]

Sources/WebAuthn/Ceremonies/Registration/AuthenticatorAttestationResponse.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,9 @@ struct ParsedAuthenticatorAttestationResponse {
3535
self.clientData = clientData
3636

3737
// Step 11. (assembling attestationObject)
38-
guard let attestationData = rawResponse.attestationObject.base64URLDecodedData,
39-
let decodedAttestationObject = try CBOR.decode([UInt8](attestationData)) else {
40-
throw WebAuthnError.invalidAttestationData
38+
guard let attestationObjectData = rawResponse.attestationObject.base64URLDecodedData,
39+
let decodedAttestationObject = try CBOR.decode([UInt8](attestationObjectData)) else {
40+
throw WebAuthnError.invalidAttestationObject
4141
}
4242

4343
guard let authData = decodedAttestationObject["authData"],
@@ -51,7 +51,7 @@ struct ParsedAuthenticatorAttestationResponse {
5151
}
5252

5353
guard let attestationStatement = decodedAttestationObject["attStmt"] else {
54-
throw WebAuthnError.invalidAttStmt
54+
throw WebAuthnError.missingAttStmt
5555
}
5656

5757
attestationObject = AttestationObject(

Sources/WebAuthn/Ceremonies/Shared/AuthenticatorData.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import Crypto
1717

1818
/// Data created and/ or used by the authenticator during authentication/ registration.
1919
/// The data contains, for example, whether a user was present or verified.
20-
struct AuthenticatorData {
20+
struct AuthenticatorData: Equatable {
2121
let relyingPartyIDHash: [UInt8]
2222
let flags: AuthenticatorFlags
2323
let counter: UInt32

Sources/WebAuthn/Ceremonies/Shared/AuthenticatorFlags.swift

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

15-
struct AuthenticatorFlags {
15+
struct AuthenticatorFlags: Equatable {
1616

1717
/**
1818
Taken from https://w3c.github.io/webauthn/#sctn-authenticator-data
@@ -40,6 +40,12 @@ struct AuthenticatorFlags {
4040
let attestedCredentialData: Bool
4141
let extensionDataIncluded: Bool
4242

43+
static func isFlagSet(on byte: UInt8, at position: Bit) -> Bool {
44+
(byte & (1 << position.rawValue)) != 0
45+
}
46+
}
47+
48+
extension AuthenticatorFlags {
4349
init(_ byte: UInt8) {
4450
userPresent = Self.isFlagSet(on: byte, at: .userPresent)
4551
userVerified = Self.isFlagSet(on: byte, at: .userVerified)
@@ -48,8 +54,4 @@ struct AuthenticatorFlags {
4854
attestedCredentialData = Self.isFlagSet(on: byte, at: .attestedCredentialDataIncluded)
4955
extensionDataIncluded = Self.isFlagSet(on: byte, at: .extensionDataIncluded)
5056
}
51-
52-
static func isFlagSet(on byte: UInt8, at position: Bit) -> Bool {
53-
(byte & (1 << position.rawValue)) != 0
54-
}
5557
}

Sources/WebAuthn/WebAuthnError.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,10 @@ public enum WebAuthnError: Error {
3636
case invalidAssertionCredentialType
3737

3838
// MARK: ParsedAuthenticatorAttestationResponse
39-
case invalidAttestationData
39+
case invalidAttestationObject
4040
case invalidAuthData
4141
case invalidFmt
42-
case invalidAttStmt
42+
case missingAttStmt
4343
case attestationFormatNotSupported
4444

4545
// MARK: ParsedCredentialCreationResponse
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import XCTest
2+
import SwiftCBOR
3+
@testable import WebAuthn
4+
5+
// swiftlint:disable line_length
6+
7+
// swiftlint:disable:next type_name
8+
final class ParsedAuthenticatorAttestationResponseTests: XCTestCase {
9+
let realClientDataJSON = "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiY21GdVpHOXRVM1J5YVc1blJuSnZiVk5sY25abGNnIiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwIiwiY3Jvc3NPcmlnaW4iOmZhbHNlLCJvdGhlcl9rZXlzX2Nhbl9iZV9hZGRlZF9oZXJlIjoiZG8gbm90IGNvbXBhcmUgY2xpZW50RGF0YUpTT04gYWdhaW5zdCBhIHRlbXBsYXRlLiBTZWUgaHR0cHM6Ly9nb28uZ2wveWFiUGV4In0"
10+
let realAttestationObject = "o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEcwRQIgNTRtpI_SOOZVzU1pN_4cX-osqUPiHMOW48qqq91DXfUCIQC-MHiaIxt2OdIxgqYnyUDHceevNOMfPibenabQGvXgjGhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAK3OAAI1vMYKZIsLJfHwVQMAIDo-5W3Kur7A7y9Lfw7ijhExfCz3_5coMEQNY_y6p-JrpQECAyYgASFYIJr_yLoYbYWgcf7aQcd7pcjUj-3o8biafWQH28WijQSvIlggPI2KqqRQ26KKuFaJ0yH7nouCBrzHu8qRONW-CPa9VDM"
11+
12+
func testInitFromRawResponseFailsWithInvalidClientDataJSON() throws {
13+
XCTAssertThrowsError(try parseResponse(
14+
clientDataJSON: "a", // this isn't base64 decodable, so parsing should fail
15+
attestationObject: ""
16+
)) { error in
17+
XCTAssertEqual(error as? WebAuthnError, .invalidClientDataJSON)
18+
}
19+
}
20+
21+
func testInitFromRawResponseFailsIfClientDataJSONDecodingFails() throws {
22+
XCTAssertThrowsError(try parseResponse(
23+
clientDataJSON: "abc", // this is base64 decodable, but will not result in a proper clientData json object
24+
attestationObject: ""
25+
)) { error in
26+
XCTAssertNotNil(error as? DecodingError)
27+
}
28+
}
29+
30+
func testInitFromRawResponseFailsIfAttestationObjectIsNotBase64() throws {
31+
XCTAssertThrowsError(try parseResponse(
32+
clientDataJSON: realClientDataJSON,
33+
attestationObject: "a"
34+
)) { error in
35+
XCTAssertEqual(error as? WebAuthnError, .invalidAttestationObject)
36+
}
37+
}
38+
39+
func testInitFromRawResponseFailsIfAuthDataIsInvalid() throws {
40+
let attestationObjectWithInvalidAuthData = "A363666D74667061636B65646761747453746D74A263616C67266373696758473045022035346DA48FD238E655CD4D6937FE1C5FEA2CA943E21CC396E3CAAAABDD435DF5022100BE30789A231B7639D23182A627C940C771E7AF34E31F3E26DE9DA6D01AF5E08C68617574684461746101"
41+
XCTAssertThrowsError(try parseResponse(
42+
clientDataJSON: realClientDataJSON,
43+
attestationObject: attestationObjectWithInvalidAuthData
44+
)) { error in
45+
XCTAssertEqual(error as? WebAuthnError, .invalidAuthData)
46+
}
47+
}
48+
49+
func testInitFromRawResponseFailsIfFmtIsInvalid() throws {
50+
let attestationObjectWithInvalidFmt = "o2NmbXQBZ2F0dFN0bXSiY2FsZyZjc2lnWEcwRQIgNTRtpI_SOOZVzU1pN_4cX-osqUPiHMOW48qqq91DXfUCIQC-MHiaIxt2OdIxgqYnyUDHceevNOMfPibenabQGvXgjGhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAK3OAAI1vMYKZIsLJfHwVQMAIDo-5W3Kur7A7y9Lfw7ijhExfCz3_5coMEQNY_y6p-JrpQECAyYgASFYIJr_yLoYbYWgcf7aQcd7pcjUj-3o8biafWQH28WijQSvIlggPI2KqqRQ26KKuFaJ0yH7nouCBrzHu8qRONW-CPa9VDM"
51+
XCTAssertThrowsError(try parseResponse(
52+
clientDataJSON: realClientDataJSON,
53+
attestationObject: attestationObjectWithInvalidFmt
54+
)) { error in
55+
XCTAssertEqual(error as? WebAuthnError, .invalidFmt)
56+
}
57+
}
58+
59+
func testInitFromRawResponseFailsIfAttStmtIsMissing() throws {
60+
let attestationObjectWithMissingAttStmt = "omNmbXRmcGFja2VkaGF1dGhEYXRhWKRJlg3liA6MaHQ0Fw9kdmBbj-SuuaKGMseZXPO6gx2XY0UAAAAArc4AAjW8xgpkiwsl8fBVAwAgOj7lbcq6vsDvL0t_DuKOETF8LPf_lygwRA1j_Lqn4mulAQIDJiABIVggmv_IuhhthaBx_tpBx3ulyNSP7ejxuJp9ZAfbxaKNBK8iWCA8jYqqpFDbooq4VonTIfuei4IGvMe7ypE41b4I9r1UMw"
61+
XCTAssertThrowsError(try parseResponse(
62+
clientDataJSON: realClientDataJSON,
63+
attestationObject: attestationObjectWithMissingAttStmt
64+
)) { error in
65+
XCTAssertEqual(error as? WebAuthnError, .missingAttStmt)
66+
}
67+
}
68+
69+
func testInitFromRawResponseSucceeds() throws {
70+
let expectedAttestationObject = AttestationObject(
71+
authenticatorData: AuthenticatorData(
72+
relyingPartyIDHash: "49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d9763".hexadecimal!,
73+
flags: AuthenticatorFlags(
74+
userPresent: true,
75+
userVerified: true,
76+
isBackupEligible: false,
77+
isCurrentlyBackedUp: false,
78+
attestedCredentialData: true,
79+
extensionDataIncluded: false
80+
),
81+
counter: 0,
82+
attestedData: AttestedCredentialData(
83+
aaguid: "adce000235bcc60a648b0b25f1f05503".hexadecimal!,
84+
credentialID: "3a3ee56dcababec0ef2f4b7f0ee28e11317c2cf7ff972830440d63fcbaa7e26b".hexadecimal!,
85+
publicKey: "a50102032620012158209affc8ba186d85a071feda41c77ba5c8d48fede8f1b89a7d6407dbc5a28d04af2258203c8d8aaaa450dba28ab85689d321fb9e8b8206bcc7bbca9138d5be08f6bd5433".hexadecimal!
86+
),
87+
extData: nil
88+
),
89+
rawAuthenticatorData: "49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97634500000000adce000235bcc60a648b0b25f1f0550300203a3ee56dcababec0ef2f4b7f0ee28e11317c2cf7ff972830440d63fcbaa7e26ba50102032620012158209affc8ba186d85a071feda41c77ba5c8d48fede8f1b89a7d6407dbc5a28d04af2258203c8d8aaaa450dba28ab85689d321fb9e8b8206bcc7bbca9138d5be08f6bd5433".hexadecimal!,
90+
format: .packed,
91+
attestationStatement: .map([
92+
.utf8String("sig"): .byteString("3045022035346DA48FD238E655CD4D6937FE1C5FEA2CA943E21CC396E3CAAAABDD435DF5022100BE30789A231B7639D23182A627C940C771E7AF34E31F3E26DE9DA6D01AF5E08C".hexadecimal!),
93+
.utf8String("alg"): .negativeInt(6)
94+
])
95+
)
96+
97+
let response = try parseResponse(clientDataJSON: realClientDataJSON, attestationObject: realAttestationObject)
98+
99+
XCTAssertEqual(response.clientData.challenge, "cmFuZG9tU3RyaW5nRnJvbVNlcnZlcg")
100+
XCTAssertEqual(response.clientData.origin, "http://localhost:8080")
101+
XCTAssertEqual(response.clientData.type, .create)
102+
103+
XCTAssertEqual(expectedAttestationObject, response.attestationObject)
104+
}
105+
106+
private func parseResponse(
107+
clientDataJSON: URLEncodedBase64,
108+
attestationObject: URLEncodedBase64
109+
) throws
110+
-> ParsedAuthenticatorAttestationResponse {
111+
try ParsedAuthenticatorAttestationResponse(from: .init(
112+
clientDataJSON: clientDataJSON,
113+
attestationObject: attestationObject
114+
))
115+
}
116+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import Foundation
2+
3+
extension String {
4+
/// Create `[UInt8]` from hexadecimal string representation
5+
var hexadecimal: [UInt8]? {
6+
let hex = self
7+
guard hex.count.isMultiple(of: 2) else {
8+
return nil
9+
}
10+
11+
let chars = hex.map { $0 }
12+
let bytes = stride(from: 0, to: chars.count, by: 2)
13+
.map { String(chars[$0]) + String(chars[$0 + 1]) }
14+
.compactMap { UInt8($0, radix: 16) }
15+
16+
guard hex.count / bytes.count == 2 else { return nil }
17+
18+
return bytes
19+
}
20+
}
21+
22+
extension Data {
23+
var hexadecimal: String {
24+
return map { String(format: "%02x", $0) }
25+
.joined()
26+
}
27+
}

0 commit comments

Comments
 (0)