Skip to content

Commit 721e667

Browse files
authored
Enforce UV in attestation if required for registration (#276)
1 parent 3b4478c commit 721e667

File tree

4 files changed

+93
-16
lines changed

4 files changed

+93
-16
lines changed

Src/Fido2/AuthenticatorAttestationResponse.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,14 +112,14 @@ public async Task<AttestationVerificationSuccess> VerifyAsync(CredentialCreateOp
112112
throw new Fido2VerificationException("User Present flag not set in authenticator data");
113113

114114
// 11. If user verification is required for this registration, verify that the User Verified bit of the flags in authData is set.
115-
// see authData.UserVerified
116-
// TODO: Make this a configurable option and add check to require
115+
if (originalOptions.AuthenticatorSelection?.UserVerification is UserVerificationRequirement.Required && !authData.UserVerified)
116+
throw new Fido2VerificationException("User Verified flag not set in authenticator data and user verification was required");
117117

118118
// 12. Verify that the values of the client extension outputs in clientExtensionResults and the authenticator extension outputs in the extensions in authData are as expected,
119119
// considering the client extension input values that were given as the extensions option in the create() call. In particular, any extension identifier values
120120
// in the clientExtensionResults and the extensions in authData MUST be also be present as extension identifier values in the extensions member of options, i.e.,
121121
// no extensions are present that were not requested. In the general case, the meaning of "are as expected" is specific to the Relying Party and which extensions are in use.
122-
122+
123123
// TODO?: Implement sort of like this: ClientExtensions.Keys.Any(x => options.extensions.contains(x);
124124

125125
if (!authData.HasAttestedCredentialData)

Test/Attestation/Apple.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ public void TestApplePublicKeyMismatch()
245245
{
246246
AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform,
247247
RequireResidentKey = true,
248-
UserVerification = UserVerificationRequirement.Required,
248+
UserVerification = UserVerificationRequirement.Discouraged,
249249
},
250250
Challenge = _challenge,
251251
ErrorMessage = "",
@@ -273,7 +273,7 @@ public void TestApplePublicKeyMismatch()
273273
{
274274
ServerDomain = "6cc3c9e7967a.ngrok.io",
275275
ServerName = "6cc3c9e7967a.ngrok.io",
276-
Origin = "https://www.passwordless.dev",
276+
Origins = new HashSet<string> { "https://www.passwordless.dev" },
277277
});
278278

279279
var credentialMakeResult = lib.MakeNewCredentialAsync(attestationResponse, origChallenge, callback);

Test/AuthenticatorResponse.cs

Lines changed: 87 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ public async Task TestAuthenticatorOrigins(string origin, string expectedOrigin)
8888
{
8989
AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform,
9090
RequireResidentKey = true,
91-
UserVerification = UserVerificationRequirement.Required,
91+
UserVerification = UserVerificationRequirement.Discouraged,
9292
},
9393
Challenge = challenge,
9494
ErrorMessage = "",
@@ -192,7 +192,7 @@ public void TestAuthenticatorOriginsFail(string origin, string expectedOrigin)
192192
{
193193
AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform,
194194
RequireResidentKey = true,
195-
UserVerification = UserVerificationRequirement.Required,
195+
UserVerification = UserVerificationRequirement.Discouraged,
196196
},
197197
Challenge = challenge,
198198
ErrorMessage = "",
@@ -388,7 +388,7 @@ public void TestAuthenticatorAttestationResponseInvalidType()
388388
{
389389
AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform,
390390
RequireResidentKey = true,
391-
UserVerification = UserVerificationRequirement.Required,
391+
UserVerification = UserVerificationRequirement.Discouraged,
392392
},
393393
Challenge = challenge,
394394
ErrorMessage = "",
@@ -459,7 +459,7 @@ public void TestAuthenticatorAttestationResponseInvalidRawId(byte[] value)
459459
{
460460
AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform,
461461
RequireResidentKey = true,
462-
UserVerification = UserVerificationRequirement.Required,
462+
UserVerification = UserVerificationRequirement.Discouraged,
463463
},
464464
Challenge = challenge,
465465
ErrorMessage = "",
@@ -528,7 +528,7 @@ public void TestAuthenticatorAttestationResponseInvalidRawType()
528528
{
529529
AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform,
530530
RequireResidentKey = true,
531-
UserVerification = UserVerificationRequirement.Required,
531+
UserVerification = UserVerificationRequirement.Discouraged,
532532
},
533533
Challenge = challenge,
534534
ErrorMessage = "",
@@ -605,7 +605,7 @@ public void TestAuthenticatorAttestationResponseRpidMismatch()
605605
{
606606
AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform,
607607
RequireResidentKey = true,
608-
UserVerification = UserVerificationRequirement.Required,
608+
UserVerification = UserVerificationRequirement.Discouraged,
609609
},
610610
Challenge = challenge,
611611
ErrorMessage = "",
@@ -683,7 +683,7 @@ public void TestAuthenticatorAttestationResponseNotUserPresent()
683683
{
684684
AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform,
685685
RequireResidentKey = true,
686-
UserVerification = UserVerificationRequirement.Required,
686+
UserVerification = UserVerificationRequirement.Discouraged,
687687
},
688688
Challenge = challenge,
689689
ErrorMessage = "",
@@ -760,7 +760,7 @@ public void TestAuthenticatorAttestationResponseNoAttestedCredentialData()
760760
{
761761
AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform,
762762
RequireResidentKey = true,
763-
UserVerification = UserVerificationRequirement.Required,
763+
UserVerification = UserVerificationRequirement.Discouraged,
764764
},
765765
Challenge = challenge,
766766
ErrorMessage = "",
@@ -838,7 +838,7 @@ public void TestAuthenticatorAttestationResponseUnknownAttestationType()
838838
{
839839
AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform,
840840
RequireResidentKey = true,
841-
UserVerification = UserVerificationRequirement.Required,
841+
UserVerification = UserVerificationRequirement.Discouraged,
842842
},
843843
Challenge = challenge,
844844
ErrorMessage = "",
@@ -915,7 +915,7 @@ public void TestAuthenticatorAttestationResponseNotUniqueCredId()
915915
{
916916
AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform,
917917
RequireResidentKey = true,
918-
UserVerification = UserVerificationRequirement.Required,
918+
UserVerification = UserVerificationRequirement.Discouraged,
919919
},
920920
Challenge = challenge,
921921
ErrorMessage = "",
@@ -950,6 +950,83 @@ public void TestAuthenticatorAttestationResponseNotUniqueCredId()
950950
Assert.Equal("CredentialId is not unique to this user", ex.Result.Message);
951951
}
952952

953+
[Fact]
954+
public void TestAuthenticatorAttestationResponseUVRequired()
955+
{
956+
var challenge = RandomGenerator.Default.GenerateBytes(128);
957+
var rp = "https://www.passwordless.dev";
958+
var acd = new AttestedCredentialData(("00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-40-FE-6A-32-63-BE-37-D1-01-B1-2E-57-CA-96-6C-00-22-93-E4-19-C8-CD-01-06-23-0B-C6-92-E8-CC-77-12-21-F1-DB-11-5D-41-0F-82-6B-DB-98-AC-64-2E-B1-AE-B5-A8-03-D1-DB-C1-47-EF-37-1C-FD-B1-CE-B0-48-CB-2C-A5-01-02-03-26-20-01-21-58-20-A6-D1-09-38-5A-C7-8E-5B-F0-3D-1C-2E-08-74-BE-6D-BB-A4-0B-4F-2A-5F-2F-11-82-45-65-65-53-4F-67-28-22-58-20-43-E1-08-2A-F3-13-5B-40-60-93-79-AC-47-42-58-AA-B3-97-B8-86-1D-E4-41-B4-4E-83-08-5D-1C-6B-E0-D0").Split('-').Select(c => Convert.ToByte(c, 16)).ToArray());
959+
var authData = new AuthenticatorData(
960+
SHA256.HashData(Encoding.UTF8.GetBytes(rp)),
961+
AuthenticatorFlags.AT | AuthenticatorFlags.UP,
962+
0,
963+
acd
964+
).ToByteArray();
965+
var clientDataJson = JsonSerializer.SerializeToUtf8Bytes(new
966+
{
967+
type = "webauthn.create",
968+
challenge = challenge,
969+
origin = rp,
970+
});
971+
972+
var rawResponse = new AuthenticatorAttestationRawResponse
973+
{
974+
Type = PublicKeyCredentialType.PublicKey,
975+
Id = new byte[] { 0xf1, 0xd0 },
976+
RawId = new byte[] { 0xf1, 0xd0 },
977+
Response = new AuthenticatorAttestationRawResponse.ResponseData()
978+
{
979+
AttestationObject = new CborMap {
980+
{ "fmt", "none" },
981+
{ "attStmt", new CborMap() },
982+
{ "authData", authData }
983+
}.Encode(),
984+
ClientDataJson = clientDataJson
985+
},
986+
};
987+
988+
var origChallenge = new CredentialCreateOptions
989+
{
990+
Attestation = AttestationConveyancePreference.Direct,
991+
AuthenticatorSelection = new AuthenticatorSelection
992+
{
993+
AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform,
994+
RequireResidentKey = true,
995+
UserVerification = UserVerificationRequirement.Required,
996+
},
997+
Challenge = challenge,
998+
ErrorMessage = "",
999+
PubKeyCredParams = new List<PubKeyCredParam>()
1000+
{
1001+
new PubKeyCredParam(COSE.Algorithm.ES256)
1002+
},
1003+
Rp = new PublicKeyCredentialRpEntity(rp, rp, ""),
1004+
Status = "ok",
1005+
User = new Fido2User
1006+
{
1007+
Name = "testuser",
1008+
Id = Encoding.UTF8.GetBytes("testuser"),
1009+
DisplayName = "Test User",
1010+
},
1011+
Timeout = 60000,
1012+
};
1013+
1014+
IsCredentialIdUniqueToUserAsyncDelegate callback = (args) =>
1015+
{
1016+
return Task.FromResult(true);
1017+
};
1018+
1019+
var lib = new Fido2(new Fido2Configuration()
1020+
{
1021+
ServerDomain = rp,
1022+
ServerName = rp,
1023+
Origins = new HashSet<string> { rp },
1024+
});
1025+
1026+
var ex = Assert.ThrowsAsync<Fido2VerificationException>(() => lib.MakeNewCredentialAsync(rawResponse, origChallenge, callback));
1027+
Assert.Equal("User Verified flag not set in authenticator data and user verification was required", ex.Result.Message);
1028+
}
1029+
9531030
[Fact]
9541031
public void TestAuthenticatorAssertionRawResponse()
9551032
{

Test/Fido2Tests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ public Attestation()
223223
{
224224
AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform,
225225
RequireResidentKey = true,
226-
UserVerification = UserVerificationRequirement.Required,
226+
UserVerification = UserVerificationRequirement.Discouraged,
227227
},
228228
Challenge = _challenge,
229229
ErrorMessage = "",

0 commit comments

Comments
 (0)