Skip to content

Commit 956ea25

Browse files
authored
Merge pull request #44 from dimitribouniol/dimitri/credential-id-length
AuthenticatorData credentialID length check as per spec
2 parents e0b5fdc + 0963f7b commit 956ea25

File tree

3 files changed

+57
-8
lines changed

3 files changed

+57
-8
lines changed

Sources/WebAuthn/Ceremonies/Shared/AuthenticatorData.swift

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,24 +78,31 @@ extension AuthenticatorData {
7878

7979
}
8080

81-
/// Returns: Attested credentials data and the length
81+
/// Parse and return the attested credential data and its length.
82+
///
83+
/// This is assumed to take place after the first 37 bytes of `data`, which is always of fixed size.
84+
/// - SeeAlso: [WebAuthn Level 3 Editor's Draft §6.5.1. Attested Credential Data]( https://w3c.github.io/webauthn/#sctn-attested-credential-data)
8285
private static func parseAttestedData(_ data: Data) throws -> (AttestedCredentialData, Int) {
83-
// We've parsed the first 37 bytes so far, the next bytes now should be the attested credential data
84-
// See https://w3c.github.io/webauthn/#sctn-attested-credential-data
86+
/// **aaguid** (16): The AAGUID of the authenticator.
8587
let aaguidLength = 16
8688
let aaguid = data[37..<(37 + aaguidLength)] // To byte at index 52
8789

90+
/// **credentialIdLength** (2): Byte length L of credentialId, 16-bit unsigned big-endian integer. Value MUST be ≤ 1023.
8891
let idLengthBytes = data[53..<55] // Length is 2 bytes
8992
let idLengthData = Data(idLengthBytes)
9093
let idLength: UInt16 = idLengthData.toInteger(endian: .big)
94+
95+
guard idLength <= 1023
96+
else { throw WebAuthnError.credentialIDTooLong }
97+
9198
let credentialIDEndIndex = Int(idLength) + 55
99+
guard data.count >= credentialIDEndIndex
100+
else { throw WebAuthnError.credentialIDTooShort }
92101

93-
guard data.count >= credentialIDEndIndex else {
94-
throw WebAuthnError.credentialIDTooShort
95-
}
102+
/// **credentialId** (L): Credential ID
96103
let credentialID = data[55..<credentialIDEndIndex]
97104

98-
/// **credentialPublicKey** (variable): The credential public key encoded in COSE_Key format, as defined in Section 7 of [RFC9052], using the CTAP2 canonical CBOR encoding form.
105+
/// **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.
99106
/// Assuming valid CBOR, verify the public key's length by decoding the next CBOR item.
100107
let inputStream = ByteInputStream(data[credentialIDEndIndex...])
101108
let decoder = CBORDecoder(stream: inputStream)
@@ -108,7 +115,7 @@ extension AuthenticatorData {
108115
publicKey: Array(publicKeyBytes)
109116
)
110117

111-
// 2 is the bytes storing the size of the credential ID
118+
/// `2` is the size of **credentialIdLength**
112119
let length = data.aaguid.count + 2 + data.credentialID.count + data.publicKey.count
113120

114121
return (data, length)

Sources/WebAuthn/WebAuthnError.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ public enum WebAuthnError: Error, Equatable {
4949
case attestedCredentialFlagNotSet
5050
case extensionDataMissing
5151
case leftOverBytesInAuthenticatorData
52+
case credentialIDTooLong
5253
case credentialIDTooShort
5354

5455
// MARK: CredentialPublicKey

Tests/WebAuthnTests/WebAuthnManagerRegistrationTests.swift

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,47 @@ final class WebAuthnManagerRegistrationTests: XCTestCase {
291291
expect: WebAuthnError.credentialRawIDTooLong
292292
)
293293
}
294+
295+
func testFinishAuthenticationFailsIfCredentialIDTooLong() async throws {
296+
/// This should succeed as it's on the border of being acceptable
297+
_ = try await finishRegistration(
298+
attestationObject: TestAttestationObjectBuilder()
299+
.validMock()
300+
.authData(
301+
TestAuthDataBuilder()
302+
.validMock()
303+
.attestedCredData(
304+
aaguid: Array(repeating: 0, count: 16),
305+
credentialIDLength: [0b000_00011, 0b1111_1111],
306+
credentialID: Array(repeating: 0, count: 1023),
307+
credentialPublicKey: TestCredentialPublicKeyBuilder().validMock().buildAsByteArray()
308+
)
309+
)
310+
.build()
311+
.cborEncoded
312+
)
313+
314+
/// While this one should throw
315+
try await assertThrowsError(
316+
await finishRegistration(
317+
attestationObject: TestAttestationObjectBuilder()
318+
.validMock()
319+
.authData(
320+
TestAuthDataBuilder()
321+
.validMock()
322+
.attestedCredData(
323+
aaguid: Array(repeating: 0, count: 16),
324+
credentialIDLength: [0b000_00100, 0b0000_0000],
325+
credentialID: Array(repeating: 0, count: 1024),
326+
credentialPublicKey: TestCredentialPublicKeyBuilder().validMock().buildAsByteArray()
327+
)
328+
)
329+
.build()
330+
.cborEncoded
331+
),
332+
expect: WebAuthnError.credentialIDTooLong
333+
)
334+
}
294335

295336
func testFinishRegistrationSucceeds() async throws {
296337
let credentialID: [UInt8] = [0, 1, 0, 1, 0, 1]

0 commit comments

Comments
 (0)