Răsfoiți Sursa

Server side credential verification

Lukas Angerer 2 ani în urmă
părinte
comite
dd1bb1609f
5 a modificat fișierele cu 123 adăugiri și 7 ștergeri
  1. 90 3
      CredentialManager.cs
  2. 4 4
      OptionsCache.cs
  3. 3 0
      OptionsWithName.cs
  4. 10 0
      Program.cs
  5. 16 0
      data/my username.json

+ 90 - 3
CredentialManager.cs

@@ -1,5 +1,6 @@
 using System.Text;
 using System.Text.Json;
+using System.Text.Json.Nodes;
 using Fido2NetLib;
 using Fido2NetLib.Objects;
 
@@ -47,7 +48,7 @@ public class CredentialManager
         };
         
         var options = _fido2.RequestNewCredential(user, new List<PublicKeyCredentialDescriptor>(), authenticatorSelection, AttestationConveyancePreference.None, extensions);
-        _optionsCache.Set(options);
+        _optionsCache.Set(options.User.Name, options);
         
         return options;
     }
@@ -57,12 +58,12 @@ public class CredentialManager
         // 2. Create callback so that lib can verify credential id is unique to this user
         static Task<bool> Callback(IsCredentialIdUniqueToUserParams args, CancellationToken cancellationToken)
         {
-            return Task.FromResult(!File.Exists($"./data/{args.User.Name}"));
+            return Task.FromResult(!File.Exists($"./data/{args.User.Name}.json"));
         }
 
         var loginDisplay = Encoding.UTF8.GetString(Base64UrlConverter.Decode(login));
         var loginName = NameTransform.ToFileName(loginDisplay);
-        var options = _optionsCache.Get(loginName);
+        var options = _optionsCache.Get<CredentialCreateOptions>(loginName);
 
         var success = await _fido2.MakeNewCredentialAsync(attestationResponse, options, Callback);
 
@@ -74,4 +75,90 @@ public class CredentialManager
         
         return success;
     }
+
+    public async Task<AssertionOptions> BuildAssertionOptions(string login)
+    {
+        var loginDisplay = Encoding.UTF8.GetString(Base64UrlConverter.Decode(login));
+        var loginName = NameTransform.ToFileName(loginDisplay);
+        byte[] credentialId;
+
+        if (File.Exists($"./data/{loginName}.json"))
+        {
+            await using var fileStream = File.OpenRead($"./data/{loginName}.json");
+            var accountInfo =
+                (JsonObject)(await JsonSerializer.DeserializeAsync(fileStream, typeof(JsonObject), _jsonOptions))!;
+            credentialId = Convert.FromBase64String(accountInfo["CredentialId"]?.GetValue<string>());
+        }
+        else
+        {
+            throw new ArgumentException("Username was not registered");
+        }
+
+        var extensions = new AuthenticationExtensionsClientInputs()
+        {
+            Extensions = true,
+            UserVerificationMethod = false,
+        };
+
+        var descriptor = new PublicKeyCredentialDescriptor()
+        {
+            Id = credentialId,
+            Type = PublicKeyCredentialType.PublicKey,
+        };
+
+        var options = _fido2.GetAssertionOptions(
+            new[] { descriptor },
+            UserVerificationRequirement.Discouraged,
+            extensions
+        );
+
+        _optionsCache.Set(Convert.ToBase64String(credentialId), new OptionsWithName<AssertionOptions>(loginName, options));
+        return options;
+    }
+
+    public async Task<AssertionVerificationResult> VerifyCredential(AuthenticatorAssertionRawResponse assertionResponse)
+    {
+        // 1. Get the assertion options we sent the client
+        var optionsWithName = _optionsCache.Get<OptionsWithName<AssertionOptions>>(Convert.ToBase64String(assertionResponse.Id));
+        AttestationVerificationSuccess assertionVerification;
+
+        if (File.Exists($"./data/{optionsWithName.Name}.json"))
+        {
+            await using var fileStream = File.OpenRead($"./data/{optionsWithName.Name}.json");
+            assertionVerification =
+                (AttestationVerificationSuccess)(await JsonSerializer.DeserializeAsync(fileStream,
+                    typeof(AttestationVerificationSuccess), _jsonOptions))!;
+        }
+        else
+        {
+            throw new ArgumentException("Username was not registered");
+        }
+        
+
+        // 3. Get credential counter from database
+        var storedCounter = assertionVerification.Counter;
+
+        // 4. Create callback to check if the user handle owns the credentialId
+        Task<bool> Callback(IsUserHandleOwnerOfCredentialIdParams args, CancellationToken cancellationToken)
+        {
+            return Task.FromResult(assertionVerification.CredentialId.SequenceEqual(args.CredentialId));
+        }
+
+        // 5. Make the assertion
+        var result = await _fido2.MakeAssertionAsync(assertionResponse, optionsWithName.Options, assertionVerification.PublicKey, storedCounter, Callback);
+
+        if (result.Status == "ok")
+        {
+            assertionVerification.Counter = result.Counter;
+            await using var fileStream = File.OpenWrite($"./data/{optionsWithName.Name}.json");
+            await JsonSerializer.SerializeAsync(fileStream, assertionVerification, _jsonOptions);
+        }
+        
+        // TODO: ???
+        // if (result.DevicePublicKey is not null)
+        //     creds.DevicePublicKeys.Add(result.DevicePublicKey);
+
+        // 7. return OK to client
+        return result;
+    }
 }

+ 4 - 4
OptionsCache.cs

@@ -12,13 +12,13 @@ public class OptionsCache
         _cache = cache;
     }
 
-    public CredentialCreateOptions Get(string key)
+    public T Get<T>(string key)
     {
-        return _cache.Get<CredentialCreateOptions>(key) ?? throw new Exception("Credential options not found - probably due to creation timeout");
+        return _cache.Get<T>(key) ?? throw new Exception("Credential options not found - probably due to creation timeout");
     }
 
-    public void Set(CredentialCreateOptions options)
+    public void Set<T>(string key, T options)
     {
-        _cache.Set(options.User.Name, options, TimeSpan.FromMinutes(5));
+        _cache.Set(key, options, TimeSpan.FromMinutes(5));
     }
 }

+ 3 - 0
OptionsWithName.cs

@@ -0,0 +1,3 @@
+namespace Passwordless;
+
+public record OptionsWithName<T>(string Name, T Options);

+ 10 - 0
Program.cs

@@ -41,4 +41,14 @@ app.MapPost("/registerCredential", async ([FromQuery] string login, [FromBody] A
     .WithName("RegisterCredential")
     .WithOpenApi();
 
+app.MapGet("/buildAssertionOptions", async ([FromQuery] string login, CredentialManager credMan) => 
+    await credMan.BuildAssertionOptions(login))
+    .WithName("BuildAssertionOptions")
+    .WithOpenApi();
+
+app.MapPost("/verifyCredential", async ([FromBody] AuthenticatorAssertionRawResponse assertionResponse, CredentialManager credMan) => 
+    await credMan.VerifyCredential(assertionResponse))
+    .WithName("VerifyCredential")
+    .WithOpenApi();
+
 app.Run();

+ 16 - 0
data/my username.json

@@ -0,0 +1,16 @@
+{
+  "PublicKey": "pQECAyYgASFYIJitjEDcX9HF9TH14j2yCJozjhoGaRSq8VP72yX0m0ADIlgg7IY4GIyurlr9hMphly3vBoXp8B8ElfxbF7aYHsW1SoI",
+  "User": {
+    "name": "my username",
+    "id": "TXkgVXNlck5hbWU",
+    "displayName": "My UserName"
+  },
+  "CredType": "none",
+  "Aaguid": "00000000-0000-0000-0000-000000000000",
+  "AttestationCertificate": null,
+  "AttestationCertificateChain": [],
+  "CredentialId": "7M3KnPu2OYqBI83iLPkHyaiuOTX4AsR9qg6fx9hOJdrdfTdk0a3Jd09CjVDJV/qxPTPJFlS02P49Gpp2yewx+g==",
+  "Counter": 17,
+  "status": null,
+  "errorMessage": null
+}