-
-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathAttestationValidator.cs
More file actions
144 lines (120 loc) · 5.2 KB
/
Copy pathAttestationValidator.cs
File metadata and controls
144 lines (120 loc) · 5.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
using System.Security.Cryptography;
using System.Text;
using static NpgsqlRestClient.Fido2.RequestParsers;
namespace NpgsqlRestClient.Fido2;
public static class AttestationValidator
{
public static AttestationResult Validate(
byte[] attestationObject,
byte[] clientDataJson,
byte[] expectedChallenge,
string[] expectedOrigins,
string expectedRpId,
bool requireUserVerification = false)
{
// Step 1: Parse and validate clientDataJSON
ClientDataParsed clientData;
try
{
clientData = ParseClientData(clientDataJson);
}
catch
{
return AttestationResult.Fail(ValidationError.InvalidClientData);
}
if (clientData.Type == null || clientData.Challenge == null || clientData.Origin == null)
return AttestationResult.Fail(ValidationError.InvalidClientData);
// Step 2: Verify type is "webauthn.create"
if (clientData.Type != "webauthn.create")
return AttestationResult.Fail($"{ValidationError.InvalidType}: expected 'webauthn.create', got '{clientData.Type}'");
// Step 3: Verify challenge matches
var challengeBytes = Base64UrlDecode(clientData.Challenge);
if (challengeBytes == null || !CryptographicOperations.FixedTimeEquals(challengeBytes, expectedChallenge))
return AttestationResult.Fail(ValidationError.ChallengeMismatch);
// Step 4: Verify origin
if (!IsOriginAllowed(clientData.Origin, expectedOrigins))
return AttestationResult.Fail($"{ValidationError.OriginMismatch}: {clientData.Origin}");
// Step 5: Parse attestation object
var attestation = CborDecoder.DecodeAttestationObject(attestationObject);
if (attestation == null)
return AttestationResult.Fail(ValidationError.InvalidAttestationObject);
// Step 6: Parse authenticator data
var authData = AuthenticatorData.Parse(attestation.AuthData);
if (authData == null)
return AttestationResult.Fail(ValidationError.InvalidAuthenticatorData);
// Step 7: Verify RP ID hash
var expectedRpIdHash = SHA256.HashData(Encoding.UTF8.GetBytes(expectedRpId));
if (!CryptographicOperations.FixedTimeEquals(authData.RpIdHash, expectedRpIdHash))
return AttestationResult.Fail(ValidationError.RpIdHashMismatch);
// Step 8: Verify user present flag
if (!authData.UserPresent)
return AttestationResult.Fail(ValidationError.UserNotPresent);
// Step 9: Verify user verification if required
if (requireUserVerification && !authData.UserVerified)
return AttestationResult.Fail(ValidationError.UserVerificationRequired);
// Step 10: Verify attested credential data is present
if (authData.AttestedCredentialData == null)
return AttestationResult.Fail(ValidationError.MissingAttestedCredentialData);
var credData = authData.AttestedCredentialData;
// Step 11: Verify the algorithm is supported
if (!CredentialPublicKey.IsSupportedAlgorithm(credData.Algorithm))
return AttestationResult.Fail($"{ValidationError.UnsupportedAlgorithm}: {credData.Algorithm}");
// Attestation validation is successful
// Note: We're using "none" attestation format by default, so we don't verify the attestation statement.
// For higher security requirements, you could verify packed, tpm, android-key, or other attestation formats.
return AttestationResult.Success(
credentialId: credData.CredentialId,
publicKey: credData.PublicKeyBytes,
algorithm: credData.Algorithm,
signCount: authData.SignCount,
backupEligible: authData.BackupEligible,
backedUp: authData.BackedUp,
aaguid: credData.Aaguid);
}
private static bool IsOriginAllowed(string origin, string[] allowedOrigins)
{
if (allowedOrigins == null || allowedOrigins.Length == 0)
{
// If no origins specified, accept any (not recommended for production)
return true;
}
foreach (var allowed in allowedOrigins)
{
if (string.Equals(origin, allowed, StringComparison.OrdinalIgnoreCase))
return true;
}
return false;
}
public static byte[]? Base64UrlDecode(string input)
{
if (string.IsNullOrEmpty(input))
return null;
try
{
// Replace base64url characters with base64 characters
var output = input.Replace('-', '+').Replace('_', '/');
// Add padding if necessary
switch (output.Length % 4)
{
case 2:
output += "==";
break;
case 3:
output += "=";
break;
}
return Convert.FromBase64String(output);
}
catch
{
return null;
}
}
public static string Base64UrlEncode(byte[] input)
{
return Convert.ToBase64String(input)
.Replace('+', '-')
.Replace('/', '_')
.TrimEnd('=');
}
}