Skip to content

Commit 7b92f4a

Browse files
authored
Support device public key and passkeys (#356)
* Start DPK and passkey support * Cleanup * Finish merge * Additional cleanup * Make BE and BS config options plumb them into attestation and assertion flows * Update DPK verification names to match current spec verbiage * Remove token binding * Set default to allow backed up credentials * Switch BE/BS config options from bool to enum Update BE/BS policy logic Add more assertion tests, move more error strings to error messages Get metadata into assertion flow for DPK
1 parent a33848c commit 7b92f4a

37 files changed

+2366
-224
lines changed

Demo/Controller.cs

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,12 @@ public JsonResult MakeCredentialOptions([FromForm] string username,
7171
if (!string.IsNullOrEmpty(authType))
7272
authenticatorSelection.AuthenticatorAttachment = authType.ToEnum<AuthenticatorAttachment>();
7373

74-
var exts = new AuthenticationExtensionsClientInputs()
75-
{
76-
Extensions = true,
77-
UserVerificationMethod = true,
74+
var exts = new AuthenticationExtensionsClientInputs()
75+
{
76+
Extensions = true,
77+
UserVerificationMethod = true,
78+
DevicePubKey = new AuthenticationExtensionsDevicePublicKeyInputs() { Attestation = attType },
79+
CredProps = true
7880
};
7981

8082
var options = _fido2.RequestNewCredential(user, existingKeys, authenticatorSelection, attType.ToEnum<AttestationConveyancePreference>(), exts);
@@ -117,19 +119,23 @@ public async Task<JsonResult> MakeCredential([FromBody] AuthenticatorAttestation
117119
// 3. Store the credentials in db
118120
DemoStorage.AddCredentialToUser(options.User, new StoredCredential
119121
{
122+
Type = success.Result.Type,
123+
Id = success.Result.Id,
120124
Descriptor = new PublicKeyCredentialDescriptor(success.Result.CredentialId),
121125
PublicKey = success.Result.PublicKey,
122126
UserHandle = success.Result.User.Id,
123-
SignatureCounter = success.Result.Counter,
127+
SignCount = success.Result.Counter,
124128
CredType = success.Result.CredType,
125129
RegDate = DateTime.Now,
126-
AaGuid = success.Result.AaGuid
130+
AaGuid = success.Result.AaGuid,
131+
Transports = success.Result.Transports,
132+
BE = success.Result.BE,
133+
BS = success.Result.BS,
134+
AttestationObject = success.Result.AttestationObject,
135+
AttestationClientDataJSON = success.Result.AttestationClientDataJSON,
136+
DevicePublicKeys = new List<byte[]>() { success.Result.DevicePublicKey }
127137
});
128138

129-
// Remove Certificates from success because System.Text.Json cannot serialize them properly. See https://github.com/passwordless-lib/fido2-net-lib/issues/328
130-
success.Result.AttestationCertificate = null;
131-
success.Result.AttestationCertificateChain = null;
132-
133139
// 4. return "ok" to the client
134140
return Json(success);
135141
}
@@ -157,8 +163,10 @@ public ActionResult AssertionOptionsPost([FromForm] string username, [FromForm]
157163
}
158164

159165
var exts = new AuthenticationExtensionsClientInputs()
160-
{
161-
UserVerificationMethod = true
166+
{
167+
Extensions = true,
168+
UserVerificationMethod = true,
169+
DevicePubKey = new AuthenticationExtensionsDevicePublicKeyInputs()
162170
};
163171

164172
// 3. Create options
@@ -206,11 +214,14 @@ public async Task<JsonResult> MakeAssertion([FromBody] AuthenticatorAssertionRaw
206214
};
207215

208216
// 5. Make the assertion
209-
var res = await _fido2.MakeAssertionAsync(clientResponse, options, creds.PublicKey, storedCounter, callback, cancellationToken: cancellationToken);
217+
var res = await _fido2.MakeAssertionAsync(clientResponse, options, creds.PublicKey, creds.DevicePublicKeys, storedCounter, callback, cancellationToken: cancellationToken);
210218

211219
// 6. Store the updated counter
212220
DemoStorage.UpdateCounter(res.CredentialId, res.Counter);
213221

222+
if (res.DevicePublicKey is not null)
223+
creds.DevicePublicKeys.Add(res.DevicePublicKey);
224+
214225
// 7. return OK to client
215226
return Json(res);
216227
}

Demo/Startup.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3-
3+
using Fido2NetLib;
44
using Microsoft.AspNetCore.Builder;
55
using Microsoft.AspNetCore.Hosting;
66
using Microsoft.AspNetCore.Http;
@@ -52,6 +52,8 @@ public void ConfigureServices(IServiceCollection services)
5252
options.Origins = Configuration.GetSection("fido2:origins").Get<HashSet<string>>();
5353
options.TimestampDriftTolerance = Configuration.GetValue<int>("fido2:timestampDriftTolerance");
5454
options.MDSCacheDirPath = Configuration["fido2:MDSCacheDirPath"];
55+
options.BackupEligibleCredentialPolicy = Configuration.GetValue<Fido2Configuration.CredentialBackupPolicy>("fido2:backupEligibleCredentialPolicy");
56+
options.BackedUpCredentialPolicy = Configuration.GetValue<Fido2Configuration.CredentialBackupPolicy>("fido2:backedUpCredentialPolicy");
5557
})
5658
.AddCachedMetadataService(config =>
5759
{

Demo/TestController.cs

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ namespace Fido2Demo;
1818
public class TestController : Controller
1919
{
2020
/* CONFORMANCE TESTING ENDPOINTS */
21-
private static readonly DevelopmentInMemoryStore DemoStorage = new ();
21+
private static readonly DevelopmentInMemoryStore _demoStorage = new ();
2222

2323
private readonly IFido2 _fido2;
2424
private readonly string _origin;
@@ -56,15 +56,15 @@ public JsonResult MakeCredentialOptionsTest([FromBody] TEST_MakeCredentialParams
5656
}
5757

5858
// 1. Get user from DB by username (in our example, auto create missing users)
59-
var user = DemoStorage.GetOrAddUser(opts.Username, () => new Fido2User
59+
var user = _demoStorage.GetOrAddUser(opts.Username, () => new Fido2User
6060
{
6161
DisplayName = opts.DisplayName,
6262
Name = opts.Username,
6363
Id = username // byte representation of userID is required
6464
});
6565

6666
// 2. Get user existing keys by username
67-
var existingKeys = DemoStorage.GetCredentialsByUser(user).Select(c => c.Descriptor).ToList();
67+
var existingKeys = _demoStorage.GetCredentialsByUser(user).Select(c => c.Descriptor).ToList();
6868

6969
//var exts = new AuthenticationExtensionsClientInputs() { Extensions = true, UserVerificationIndex = true, Location = true, UserVerificationMethod = true, BiometricAuthenticatorPerformanceBounds = new AuthenticatorBiometricPerfBounds { FAR = float.MaxValue, FRR = float.MaxValue } };
7070
var exts = new AuthenticationExtensionsClientInputs() { };
@@ -83,7 +83,7 @@ public JsonResult MakeCredentialOptionsTest([FromBody] TEST_MakeCredentialParams
8383

8484
[HttpPost]
8585
[Route("/attestation/result")]
86-
public async Task<JsonResult> MakeCredentialResultTest([FromBody] AuthenticatorAttestationRawResponse attestationResponse, CancellationToken cancellationToken)
86+
public async Task<JsonResult> MakeCredentialResultTestAsync([FromBody] AuthenticatorAttestationRawResponse attestationResponse, CancellationToken cancellationToken)
8787
{
8888

8989
// 1. get the options we sent the client
@@ -93,20 +93,20 @@ public async Task<JsonResult> MakeCredentialResultTest([FromBody] AuthenticatorA
9393
// 2. Create callback so that lib can verify credential id is unique to this user
9494
IsCredentialIdUniqueToUserAsyncDelegate callback = static async (args, cancellationToken) =>
9595
{
96-
var users = await DemoStorage.GetUsersByCredentialIdAsync(args.CredentialId, cancellationToken);
96+
var users = await _demoStorage.GetUsersByCredentialIdAsync(args.CredentialId, cancellationToken);
9797
return users.Count <= 0;
9898
};
9999

100100
// 2. Verify and make the credentials
101101
var success = await _fido2.MakeNewCredentialAsync(attestationResponse, options, callback, cancellationToken: cancellationToken);
102102

103103
// 3. Store the credentials in db
104-
DemoStorage.AddCredentialToUser(options.User, new StoredCredential
104+
_demoStorage.AddCredentialToUser(options.User, new StoredCredential
105105
{
106106
Descriptor = new PublicKeyCredentialDescriptor(success.Result.CredentialId),
107107
PublicKey = success.Result.PublicKey,
108108
UserHandle = success.Result.User.Id,
109-
SignatureCounter = success.Result.Counter
109+
SignCount = success.Result.Counter
110110
});
111111

112112
// 4. return "ok" to the client
@@ -119,12 +119,12 @@ public IActionResult AssertionOptionsTest([FromBody] TEST_AssertionClientParams
119119
{
120120
var username = assertionClientParams.Username;
121121
// 1. Get user from DB
122-
var user = DemoStorage.GetUser(username);
122+
var user = _demoStorage.GetUser(username);
123123
if (user == null)
124124
return NotFound("username was not registered");
125125

126126
// 2. Get registered credentials from database
127-
var existingCredentials = DemoStorage.GetCredentialsByUser(user).Select(c => c.Descriptor).ToList();
127+
var existingCredentials = _demoStorage.GetCredentialsByUser(user).Select(c => c.Descriptor).ToList();
128128

129129
var uv = assertionClientParams.UserVerification;
130130
if (null != assertionClientParams.authenticatorSelection)
@@ -154,30 +154,33 @@ public IActionResult AssertionOptionsTest([FromBody] TEST_AssertionClientParams
154154

155155
[HttpPost]
156156
[Route("/assertion/result")]
157-
public async Task<JsonResult> MakeAssertionTest([FromBody] AuthenticatorAssertionRawResponse clientResponse, CancellationToken cancellationToken)
157+
public async Task<JsonResult> MakeAssertionTestAsync([FromBody] AuthenticatorAssertionRawResponse clientResponse, CancellationToken cancellationToken)
158158
{
159159
// 1. Get the assertion options we sent the client
160160
var jsonOptions = HttpContext.Session.GetString("fido2.assertionOptions");
161161
var options = AssertionOptions.FromJson(jsonOptions);
162162

163163
// 2. Get registered credential from database
164-
var creds = DemoStorage.GetCredentialById(clientResponse.Id);
164+
var creds = _demoStorage.GetCredentialById(clientResponse.Id);
165165

166166
// 3. Get credential counter from database
167167
var storedCounter = creds.SignatureCounter;
168168

169169
// 4. Create callback to check if userhandle owns the credentialId
170170
IsUserHandleOwnerOfCredentialIdAsync callback = static async (args, cancellationToken) =>
171171
{
172-
var storedCreds = await DemoStorage.GetCredentialsByUserHandleAsync(args.UserHandle, cancellationToken);
172+
var storedCreds = await _demoStorage.GetCredentialsByUserHandleAsync(args.UserHandle, cancellationToken);
173173
return storedCreds.Exists(c => c.Descriptor.Id.SequenceEqual(args.CredentialId));
174174
};
175175

176176
// 5. Make the assertion
177-
var res = await _fido2.MakeAssertionAsync(clientResponse, options, creds.PublicKey, storedCounter, callback, cancellationToken: cancellationToken);
177+
var res = await _fido2.MakeAssertionAsync(clientResponse, options, creds.PublicKey, creds.DevicePublicKeys, storedCounter, callback, cancellationToken: cancellationToken);
178178

179179
// 6. Store the updated counter
180-
DemoStorage.UpdateCounter(res.CredentialId, res.Counter);
180+
_demoStorage.UpdateCounter(res.CredentialId, res.Counter);
181+
182+
if (res.DevicePublicKey is not null)
183+
creds.DevicePublicKeys.Add(res.DevicePublicKey);
181184

182185
var testRes = new
183186
{

Demo/appsettings.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
"fido2": {
33
"serverDomain": "localhost",
44
"origins": [ "https://localhost:44329" ],
5-
"timestampDriftTolerance": 300000
5+
"timestampDriftTolerance": 300000,
6+
"backupEligibleCredentialPolicy": "allowed",
7+
"backedUpCredentialPolicy": "allowed"
68
},
79
"Logging": {
810
"IncludeScopes": false,

Demo/wwwroot/js/custom.register.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,9 @@ async function registerNewCredential(newCredential) {
125125
extensions: newCredential.getClientExtensionResults(),
126126
response: {
127127
AttestationObject: coerceToBase64Url(attestationObject),
128-
clientDataJSON: coerceToBase64Url(clientDataJSON)
129-
}
128+
clientDataJson: coerceToBase64Url(clientDataJSON),
129+
transports: newCredential.response.getTransports(),
130+
},
130131
};
131132

132133
let response;

Src/Fido2.Models/AuthenticatorAssertionRawResponse.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,13 @@ public class AssertionResponse
4242
[JsonConverter(typeof(Base64UrlConverter))]
4343
[JsonPropertyName("clientDataJSON")]
4444
public byte[] ClientDataJson { get; set; }
45-
45+
#nullable enable
4646
[JsonPropertyName("userHandle")]
4747
[JsonConverter(typeof(Base64UrlConverter))]
48-
public byte[] UserHandle { get; set; }
48+
public byte[]? UserHandle { get; set; }
49+
50+
[JsonPropertyName("attestationObject")]
51+
[JsonConverter(typeof(Base64UrlConverter))]
52+
public byte[]? AttestationObject { get; set; }
4953
}
5054
}

Src/Fido2.Models/AuthenticatorAttestationRawResponse.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public sealed class AuthenticatorAttestationRawResponse
1515
public byte[] RawId { get; set; }
1616

1717
[JsonPropertyName("type")]
18-
public PublicKeyCredentialType? Type { get; set; }
18+
public PublicKeyCredentialType Type { get; set; } = PublicKeyCredentialType.PublicKey;
1919

2020
[JsonPropertyName("response")]
2121
public ResponseData Response { get; set; }

Src/Fido2.Models/Exceptions/Fido2ErrorCode.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,9 @@ public enum Fido2ErrorCode
3232
InvalidAuthenticatorResponseChallenge,
3333
NonUniqueCredentialId,
3434
AaGuidNotFound,
35-
UnimplementedAlgorithm
35+
UnimplementedAlgorithm,
36+
BackupEligibilityRequirementNotMet,
37+
BackupStateRequirementNotMet,
38+
CredentialAlgorithmRequirementNotMet,
39+
DevicePublicKeyAuthentication
3640
}

Src/Fido2.Models/Fido2Configuration.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Linq;
4+
using System.Runtime.Serialization;
45

56
namespace Fido2NetLib;
67

@@ -113,4 +114,35 @@ public ISet<string> FullyQualifiedOrigins
113114
AuthenticatorStatus.USER_KEY_PHYSICAL_COMPROMISE,
114115
AuthenticatorStatus.REVOKED
115116
};
117+
118+
/// <summary>
119+
/// Whether or not to accept a backup eligible credential
120+
/// </summary>
121+
public CredentialBackupPolicy BackupEligibleCredentialPolicy { get; set; } = CredentialBackupPolicy.Allowed;
122+
123+
/// <summary>
124+
/// Whether or not to accept a backed up credential
125+
/// </summary>
126+
public CredentialBackupPolicy BackedUpCredentialPolicy { get; set; } = CredentialBackupPolicy.Allowed;
127+
128+
public enum CredentialBackupPolicy
129+
{
130+
/// <summary>
131+
/// This value indicates that the Relying Party requires backup eligible or backed up credentials.
132+
/// </summary>
133+
[EnumMember(Value = "required")]
134+
Required,
135+
136+
/// <summary>
137+
/// This value indicates that the Relying Party allows backup eligible or backed up credentials.
138+
/// </summary>
139+
[EnumMember(Value = "allowed")]
140+
Allowed,
141+
142+
/// <summary>
143+
/// This value indicates that the Relying Party does not allow backup eligible or backed up credentials.
144+
/// </summary>
145+
[EnumMember(Value = "disallowed")]
146+
Disallowed
147+
}
116148
}

Src/Fido2.Models/Objects/AssertionVerificationResult.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,19 @@ public class AssertionVerificationResult : Fido2ResponseBase
88
public byte[] CredentialId { get; set; }
99

1010
public uint Counter { get; set; }
11+
12+
/// <summary>
13+
/// The latest value of the signature counter in the authenticator data from any ceremony using the public key credential source.
14+
/// </summary>
15+
public uint SignCount { get; set; }
16+
17+
/// <summary>
18+
/// The latest value of the BS flag in the authenticator data from any ceremony using the public key credential source.
19+
/// </summary>
20+
public bool BS { get; set; }
21+
22+
/// <summary>
23+
/// The public key portion of a hardware-bound device key pair
24+
/// </summary>
25+
public byte[] DevicePublicKey { get; set; }
1126
}

0 commit comments

Comments
 (0)