Skip to content

Commit e6567d3

Browse files
authored
Make AuthenticatorData immutable and strongly type throughout library (#412)
* Make AuthenticatorData immutable and strongly type throughout library * Improve nullability annotations * Add ParsedAttestationObject.FromCbor helper * Make AuthenticatorResponse immutable * Update Xunit * Update System.IdentityModel.Tokens.Jwt * Update Test.Sdk * Improve AuthenticatorResponse test coverage * Fix typos
1 parent 02772cc commit e6567d3

22 files changed

+322
-341
lines changed

Src/Fido2.Models/Objects/AttestationVerificationSuccess.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using System;
2-
using System.Security.Cryptography.X509Certificates;
32
using System.Text.Json.Serialization;
43

54
namespace Fido2NetLib.Objects;

Src/Fido2/AttestationFormat/AndroidKey.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ public override (AttestationType, X509Certificate2[]) Verify()
186186
throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Invalid android key attestation signature");
187187

188188
// 3. Verify that the public key in the first certificate in x5c matches the credentialPublicKey in the attestedCredentialData in authenticatorData.
189-
if (!AuthData.AttestedCredentialData.CredentialPublicKey.Verify(Data, sig))
189+
if (!AuthData.AttestedCredentialData!.CredentialPublicKey.Verify(Data, sig))
190190
throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Incorrect credentialPublicKey in android key attestation");
191191

192192
// 4. Verify that the attestationChallenge field in the attestation certificate extension data is identical to clientDataHash

Src/Fido2/AttestationFormat/Apple.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ public override (AttestationType, X509Certificate2[]) Verify()
8181
var cpk = new CredentialPublicKey(credCert, coseAlg);
8282

8383
// Finally, compare byte sequence of CredentialPublicKey built from credCert with byte sequence of CredentialPublicKey from AttestedCredentialData from authData
84-
if (!cpk.GetBytes().AsSpan().SequenceEqual(AuthData.AttestedCredentialData.CredentialPublicKey.GetBytes()))
84+
if (!cpk.GetBytes().AsSpan().SequenceEqual(AuthData.AttestedCredentialData!.CredentialPublicKey.GetBytes()))
8585
throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Credential public key in Apple attestation does not match subject public key of credCert");
8686

8787
// 7. If successful, return implementation-specific values representing attestation type Anonymous CA and attestation trust path x5c.

Src/Fido2/AttestationFormat/AppleAppAttest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public override (AttestationType, X509Certificate2[]) Verify()
6464
chain.ChainPolicy.ExtraStore.Add(intermediateCert);
6565

6666
X509Certificate2 credCert = new((byte[])x5cArray[0]);
67-
if (AuthData.AttestedCredentialData.AaGuid.Equals(devAaguid))
67+
if (AuthData.AttestedCredentialData!.AaGuid.Equals(devAaguid))
6868
{
6969
// Allow expired leaf cert in development environment
7070
chain.ChainPolicy.VerificationTime = credCert.NotBefore.AddSeconds(1);

Src/Fido2/AttestationFormat/AttestationVerifier.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ namespace Fido2NetLib;
1414
public abstract class AttestationVerifier
1515
{
1616
private protected CborMap _attStmt;
17-
private protected byte[] _authenticatorData;
17+
private protected AuthenticatorData _authenticatorData;
1818
private protected byte[] _clientDataHash;
1919

2020
#nullable enable
@@ -23,11 +23,11 @@ public abstract class AttestationVerifier
2323

2424
internal CborObject? EcdaaKeyId => _attStmt["ecdaaKeyId"];
2525

26-
internal AuthenticatorData AuthData => new (_authenticatorData);
26+
internal AuthenticatorData AuthData => _authenticatorData;
2727

28-
internal CborMap CredentialPublicKey => AuthData.AttestedCredentialData.CredentialPublicKey.GetCborObject();
28+
internal CborMap CredentialPublicKey => AuthData.AttestedCredentialData!.CredentialPublicKey.GetCborObject();
2929

30-
internal byte[] Data => DataHelper.Concat(_authenticatorData, _clientDataHash);
30+
internal byte[] Data => DataHelper.Concat(_authenticatorData.ToByteArray(), _clientDataHash);
3131

3232
internal bool TryGetVer([NotNullWhen(true)] out string? ver)
3333
{
@@ -127,7 +127,7 @@ internal static byte U2FTransportsFromAttnCert(X509ExtensionCollection exts)
127127

128128
#nullable disable
129129

130-
public virtual (AttestationType, X509Certificate2[]) Verify(CborMap attStmt, byte[] authenticatorData, byte[] clientDataHash)
130+
public virtual (AttestationType, X509Certificate2[]) Verify(CborMap attStmt, AuthenticatorData authenticatorData, byte[] clientDataHash)
131131
{
132132
_attStmt = attStmt;
133133
_authenticatorData = authenticatorData;

Src/Fido2/AttestationFormat/FidoU2f.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ internal sealed class FidoU2f : AttestationVerifier
1414
public override (AttestationType, X509Certificate2[]) Verify()
1515
{
1616
// verify that aaguid is 16 empty bytes (note: required by fido2 conformance testing, could not find this in spec?)
17-
if (AuthData.AttestedCredentialData.AaGuid.CompareTo(Guid.Empty) != 0)
17+
if (AuthData.AttestedCredentialData!.AaGuid.CompareTo(Guid.Empty) != 0)
1818
throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Aaguid was not empty parsing fido-u2f atttestation statement");
1919

2020
// https://www.w3.org/TR/webauthn/#fido-u2f-attestation

Src/Fido2/AttestationFormat/Packed.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ public override (AttestationType, X509Certificate2[]?) Verify()
108108
// 2c. If attestnCert contains an extension with OID 1.3.6.1.4.1.45724.1.1.4 (id-fido-gen-ce-aaguid) verify that the value of this extension matches the aaguid in authenticatorData
109109
if (aaguid != null)
110110
{
111-
if (AttestedCredentialData.FromBigEndian(aaguid).CompareTo(AuthData.AttestedCredentialData.AaGuid) != 0)
111+
if (AttestedCredentialData.FromBigEndian(aaguid).CompareTo(AuthData.AttestedCredentialData!.AaGuid) != 0)
112112
throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "aaguid present in packed attestation cert exts but does not match aaguid from authData");
113113
}
114114

@@ -137,7 +137,7 @@ public override (AttestationType, X509Certificate2[]?) Verify()
137137
else
138138
{
139139
// 4a. Validate that alg matches the algorithm of the credentialPublicKey in authenticatorData
140-
if (!AuthData.AttestedCredentialData.CredentialPublicKey.IsSameAlg(alg))
140+
if (!AuthData.AttestedCredentialData!.CredentialPublicKey.IsSameAlg(alg))
141141
throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Algorithm mismatch between credential public key and authenticator data in self attestation statement");
142142

143143
// 4b. Verify that sig is a valid signature over the concatenation of authenticatorData and

Src/Fido2/AttestationFormat/Tpm.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ public override (AttestationType, X509Certificate2[]) Verify()
197197
// 5c. If aikCert contains an extension with OID 1.3.6.1.4.1.45724.1.1.4 (id-fido-gen-ce-aaguid) verify that the value of this extension matches the aaguid in authenticatorData
198198
if (AaguidFromAttnCertExts(aikCert.Extensions) is byte[] aaguid &&
199199
(!aaguid.AsSpan().SequenceEqual(Guid.Empty.ToByteArray())) &&
200-
(AttestedCredentialData.FromBigEndian(aaguid).CompareTo(AuthData.AttestedCredentialData.AaGuid) != 0))
200+
(AttestedCredentialData.FromBigEndian(aaguid).CompareTo(AuthData.AttestedCredentialData!.AaGuid) != 0))
201201
{
202202
throw new Fido2VerificationException($"aaguid malformed, expected {AuthData.AttestedCredentialData.AaGuid}, got {new Guid(aaguid)}");
203203
}

Src/Fido2/AuthenticatorAssertionResponse.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ private AuthenticatorAssertionResponse(byte[] clientDataJson) : base(clientDataJ
2626

2727
public AuthenticatorAssertionRawResponse Raw { get; init; }
2828

29-
public byte[] AuthenticatorData { get; init; }
29+
public AuthenticatorData AuthenticatorData { get; init; }
3030

3131
public byte[] Signature { get; init; }
3232

@@ -40,7 +40,7 @@ public static AuthenticatorAssertionResponse Parse(AuthenticatorAssertionRawResp
4040
var response = new AuthenticatorAssertionResponse(rawResponse.Response.ClientDataJson)
4141
{
4242
Raw = rawResponse, // accessed in Verify()
43-
AuthenticatorData = rawResponse.Response.AuthenticatorData,
43+
AuthenticatorData = AuthenticatorData.Parse(rawResponse.Response.AuthenticatorData),
4444
Signature = rawResponse.Response.Signature,
4545
UserHandle = rawResponse.Response.UserHandle,
4646
AttestationObject = rawResponse.Response.AttestationObject
@@ -103,7 +103,7 @@ public async Task<AssertionVerificationResult> VerifyAsync(
103103

104104
// 7. Let cData, authData and sig denote the value of credential’s response's clientDataJSON, authenticatorData, and signature respectively.
105105
//var cData = Raw.Response.ClientDataJson;
106-
var authData = new AuthenticatorData(AuthenticatorData);
106+
var authData = AuthenticatorData;
107107
//var sig = Raw.Response.Signature;
108108

109109
// 8. Let JSONtext be the result of running UTF-8 decode on the value of cData.
@@ -245,7 +245,7 @@ public async Task<AssertionVerificationResult> VerifyAsync(
245245
private static byte[] DevicePublicKeyAuthentication(
246246
List<byte[]> storedDevicePublicKeys,
247247
AuthenticationExtensionsClientOutputs clientExtensionResults,
248-
byte[] authData,
248+
AuthenticatorData authData,
249249
byte[] hash
250250
)
251251
{
@@ -257,7 +257,7 @@ byte[] hash
257257
DevicePublicKeyAuthenticatorOutput devicePublicKeyAuthenticatorOutput = new(attObjForDevicePublicKey.AuthenticatorOutput);
258258

259259
// 3. Verify that signature is a valid signature over the assertion signature input (i.e. authData and hash) by the device public key dpk.
260-
if (!devicePublicKeyAuthenticatorOutput.DevicePublicKey.Verify(DataHelper.Concat(authData, hash), attObjForDevicePublicKey.Signature))
260+
if (!devicePublicKeyAuthenticatorOutput.DevicePublicKey.Verify(DataHelper.Concat(authData.ToByteArray(), hash), attObjForDevicePublicKey.Signature))
261261
throw new Fido2VerificationException(Fido2ErrorCode.InvalidSignature, Fido2ErrorMessages.InvalidSignature);
262262

263263
// 4. If the Relying Party's user account mapped to the credential.id in play (i.e., for the user being
@@ -310,7 +310,7 @@ byte[] hash
310310
try
311311
{
312312
// This is a known device public key with a valid signature and valid attestation and thus a known device. Terminate these verification steps.
313-
_ = verifier.Verify(devicePublicKeyAuthenticatorOutput.AttStmt, devicePublicKeyAuthenticatorOutput.AuthData, devicePublicKeyAuthenticatorOutput.Hash);
313+
_ = verifier.Verify(devicePublicKeyAuthenticatorOutput.AttStmt, AuthenticatorData.Parse(devicePublicKeyAuthenticatorOutput.AuthData), devicePublicKeyAuthenticatorOutput.Hash);
314314
}
315315
catch (Exception ex)
316316
{
@@ -357,7 +357,7 @@ byte[] hash
357357
try
358358
{
359359
// This is a known device public key with a valid signature and valid attestation and thus a known device. Terminate these verification steps.
360-
_ = verifier.Verify(devicePublicKeyAuthenticatorOutput.AttStmt, devicePublicKeyAuthenticatorOutput.AuthData, devicePublicKeyAuthenticatorOutput.Hash);
360+
_ = verifier.Verify(devicePublicKeyAuthenticatorOutput.AttStmt, AuthenticatorData.Parse(devicePublicKeyAuthenticatorOutput.AuthData), devicePublicKeyAuthenticatorOutput.Hash);
361361
return devicePublicKeyAuthenticatorOutput.GetBytes();
362362
}
363363
catch (Exception ex)
@@ -394,7 +394,7 @@ byte[] hash
394394
try
395395
{
396396
// This is a known device public key with a valid signature and valid attestation and thus a known device. Terminate these verification steps.
397-
_ = verifier.Verify(devicePublicKeyAuthenticatorOutput.AttStmt, devicePublicKeyAuthenticatorOutput.AuthData, devicePublicKeyAuthenticatorOutput.Hash);
397+
_ = verifier.Verify(devicePublicKeyAuthenticatorOutput.AttStmt, AuthenticatorData.Parse(devicePublicKeyAuthenticatorOutput.AuthData), devicePublicKeyAuthenticatorOutput.Hash);
398398
return devicePublicKeyAuthenticatorOutput.GetBytes();
399399
}
400400
catch

Src/Fido2/AuthenticatorAttestationResponse.cs

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
using System;
44
using System.Linq;
5-
using System.Net.Http;
65
using System.Security.Cryptography;
76
using System.Security.Cryptography.X509Certificates;
87
using System.Text;
@@ -26,7 +25,8 @@ public sealed class AuthenticatorAttestationResponse : AuthenticatorResponse
2625
private IMetadataService _metadataService;
2726
private CancellationToken _cancellationToken;
2827
private Fido2Configuration _config;
29-
private AuthenticatorAttestationResponse(byte[] clientDataJson)
28+
29+
private AuthenticatorAttestationResponse(byte[] clientDataJson)
3030
: base(clientDataJson)
3131
{
3232
}
@@ -54,22 +54,12 @@ public static AuthenticatorAttestationResponse Parse(AuthenticatorAttestationRaw
5454
throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestationObject, Fido2ErrorMessages.InvalidAttestationObject, ex);
5555
}
5656

57-
if (!(
58-
cborAttestation["fmt"] is { Type: CborType.TextString } &&
59-
cborAttestation["attStmt"] is { Type: CborType.Map } &&
60-
cborAttestation["authData"] is { Type: CborType.ByteString }))
61-
{
62-
throw new Fido2VerificationException(Fido2ErrorCode.MalformedAttestationObject, Fido2ErrorMessages.MalformedAttestationObject);
63-
}
57+
var attestationObject = ParsedAttestationObject.FromCbor(cborAttestation);
6458

6559
return new AuthenticatorAttestationResponse(rawResponse.Response.ClientDataJson)
6660
{
6761
Raw = rawResponse,
68-
AttestationObject = new ParsedAttestationObject(
69-
fmt : (string)cborAttestation["fmt"],
70-
attStmt : (CborMap)cborAttestation["attStmt"],
71-
authData : (byte[])cborAttestation["authData"]
72-
)
62+
AttestationObject = attestationObject
7363
};
7464
}
7565

@@ -103,7 +93,7 @@ public async Task<AttestationVerificationSuccess> VerifyAsync(
10393
if (Raw.Type != PublicKeyCredentialType.PublicKey)
10494
throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestationResponse, "AttestationResponse type must be 'public-key'");
10595

106-
var authData = new AuthenticatorData(AttestationObject.AuthData);
96+
var authData = AttestationObject.AuthData;
10797

10898
// 10. Let hash be the result of computing a hash over response.clientDataJSON using SHA-256.
10999
byte[] clientDataHash = SHA256.HashData(Raw.Response.ClientDataJson);
@@ -235,7 +225,7 @@ public async Task<AttestationVerificationSuccess> VerifyAsync(
235225
/// <see cref="https://w3c.github.io/webauthn/#sctn-device-publickey-extension-verification-create"/>
236226
private async Task<byte[]> DevicePublicKeyRegistrationAsync(
237227
AuthenticationExtensionsClientOutputs clientExtensionResults,
238-
byte[] authData,
228+
AuthenticatorData authData,
239229
byte[] hash
240230
)
241231
{
@@ -247,7 +237,7 @@ byte[] hash
247237
DevicePublicKeyAuthenticatorOutput devicePublicKeyAuthenticatorOutput = new(attObjForDevicePublicKey.AuthenticatorOutput);
248238

249239
// 3. Verify that signature is a valid signature over the assertion signature input (i.e. authData and hash) by the device public key dpk.
250-
if (!devicePublicKeyAuthenticatorOutput.DevicePublicKey.Verify(DataHelper.Concat(authData, hash), attObjForDevicePublicKey.Signature))
240+
if (!devicePublicKeyAuthenticatorOutput.DevicePublicKey.Verify(DataHelper.Concat(authData.ToByteArray(), hash), attObjForDevicePublicKey.Signature))
251241
throw new Fido2VerificationException(Fido2ErrorCode.InvalidSignature, Fido2ErrorMessages.InvalidSignature);
252242

253243
// 4. Optionally, if attestation was requested and the Relying Party wishes to verify it, verify that attStmt is a correct attestation statement, conveying a valid attestation signature,
@@ -256,7 +246,7 @@ byte[] hash
256246
var verifier = AttestationVerifier.Create(devicePublicKeyAuthenticatorOutput.Fmt);
257247

258248
// https://w3c.github.io/webauthn/#sctn-device-publickey-attestation-calculations
259-
(var attType, var trustPath) = verifier.Verify(devicePublicKeyAuthenticatorOutput.AttStmt, devicePublicKeyAuthenticatorOutput.AuthData, devicePublicKeyAuthenticatorOutput.Hash);
249+
(var attType, var trustPath) = verifier.Verify(devicePublicKeyAuthenticatorOutput.AttStmt, AuthenticatorData.Parse(devicePublicKeyAuthenticatorOutput.AuthData), devicePublicKeyAuthenticatorOutput.Hash);
260250

261251
// 5. Complete the steps from § 7.1 Registering a New Credential and, if those steps are successful,
262252
// store the aaguid, dpk, scope, fmt, attStmt values indexed to the credential.id in the user account.
@@ -335,7 +325,7 @@ static bool ContainsAttestationType(MetadataBLOBPayloadEntry entry, MetadataAtte
335325
/// </summary>
336326
public sealed class ParsedAttestationObject
337327
{
338-
public ParsedAttestationObject(string fmt, CborMap attStmt, byte[] authData)
328+
public ParsedAttestationObject(string fmt, CborMap attStmt, AuthenticatorData authData)
339329
{
340330
Fmt = fmt;
341331
AttStmt = attStmt;
@@ -346,6 +336,23 @@ public ParsedAttestationObject(string fmt, CborMap attStmt, byte[] authData)
346336

347337
public CborMap AttStmt { get; }
348338

349-
public byte[] AuthData { get; }
339+
public AuthenticatorData AuthData { get; }
340+
341+
internal static ParsedAttestationObject FromCbor(CborObject cbor)
342+
{
343+
if (!(
344+
cbor["fmt"] is { Type: CborType.TextString } fmt &&
345+
cbor["attStmt"] is { Type: CborType.Map } attStmt &&
346+
cbor["authData"] is { Type: CborType.ByteString } authData))
347+
{
348+
throw new Fido2VerificationException(Fido2ErrorCode.MalformedAttestationObject, Fido2ErrorMessages.MalformedAttestationObject);
349+
}
350+
351+
return new ParsedAttestationObject(
352+
fmt : (string)fmt,
353+
attStmt : (CborMap)attStmt,
354+
authData : AuthenticatorData.Parse((byte[])authData)
355+
);
356+
}
350357
}
351358
}

0 commit comments

Comments
 (0)