ASP.NET Core 5.0 - FIDO2 Credential Create

This article will describe the registration or attestation ceremony I implement in the Users Without Passwords Project (UWPP). I will assume you have downloaded the ASP.NET Core 5.0 - Users Without Passwords Project.

Users Without Passwords Project and Article Series

I have developed two separate projects in the Users Without Passwords Project (UWPP) solution. The Users Without Passwords v4 project supports Bootstrap v4 and the new Users Without Passwords project supports Bootstrap v5. The new version is published at Fido.KenHaggerty.Com. You can register a new user with Windows Hello or a FIDO2 security key. Details, screenshots, and related articles can be found at ASP.NET Core 5.0 - Users Without Passwords Project. The details page includes the version change log.

The registration or attestation ceremony requires a valid decoded response from the authenticating device before the user and a related credential are created. The main purpose of the authenticator's response is to provide a Credential Id and a PublicKey which are used to configure an authentication request and validate the response from a log in or assertion ceremony.

The UWPP configures the CredentialCreationOptions before the user calls the navigator. credentials. create() function from JavaScript. The serialized options must have a field name of publicKey. See ASP.NET Core 5.0 - FIDO2 Challenge Options. The UWPP detects platform authenticators like Windows Hello with the PublicKeyCredential. isUserVerifying PlatformAuthenticatorAvailable JavaScript function. If available, the Create With Platform button is displayed.

if (typeof PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable === "function") {
    PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
        .then(function (available) {
            if (available) {
                platformButton.classList.remove('d-none');
                platformButton.addEventListener('click', function () {
                    createCredential(true);
                });
            }
        })
        .catch(function (err) {
            console.error(err);
        });
}

The createCredential() JavaScript function implements an usePlatform parameter. If usePlatform, the createOptions. authenticatorSelection. authenticatorAttachment property is updated from the default value of "cross-platform" to "platform". The UWPP implements a small setTimeout() function because I found Firefox freezes the DOM before the waiting-div is displayed. The UWPP extracts the ExtensionResults and Transports with credential functions.

function createCredential(usePlatform) {

    let deviceName = document.querySelector('#DeviceName').value;
    if (deviceName == '') {
        showMessageModal('Device Name is required.', 'alert-danger');
        return;
    }

    // Update byte string to expected Uint8Array
    try {
        if (typeof (createOptions.challenge) != Uint8Array)
            createOptions.challenge = getUint8Array(createOptions.challenge);

        if (typeof (createOptions.user.id) != Uint8Array)
            createOptions.user.id = getUint8Array(createOptions.user.id);

    } catch (e) {
        showMessageModal(e, 'alert-warning');
        return;
    }

    document.querySelector('.request-div').classList.add('d-none');
    document.querySelector('.waiting-div').classList.remove('d-none');

    let attachment = createOptions.authenticatorSelection.authenticatorAttachment; //'cross-platform';
    if (usePlatform) {
        createOptions.authenticatorSelection.authenticatorAttachment = 'platform';
        attachment = 'platform';
    }

    // Firefox v91.0 freezes the DOM before the waiting-div is displayed
    setTimeout(function () {
        // 7.1.2 Call navigator.credentials.create() and pass options as the publicKey option.
        navigator.credentials.create({ publicKey: createOptions })
            .then((credential) => {

                if (authAbortSignal.aborted) {
                    showMessageModal('Abort by user, authAbortSignal.aborted.', 'alert-danger', false, '', '', true, './register',
                        '_self', 'Try Again', 'btn-primary', false);
                    return;
                }

                let credentialJson = credentialToJSON(credential);

                credentialJson.attachment = attachment;
                credentialJson.name = deviceName;
                credentialJson.extensionResults = credential.getClientExtensionResults();
                // Firefox v90.0 does not support credential.response.getTransports()
                credentialJson.response.transports = [];
                if (typeof credential.response.getTransports === "function")
                    credentialJson.response.transports = credential.response.getTransports();

                return fetch(window.location.pathname + '?handler=callback',
                {
                    method: 'post',
                    credentials: 'same-origin',
                    headers: {
                        'Content-Type': 'application/json;charset=UTF-8',
                        'RequestVerificationToken':
                            document.querySelector('input[type="hidden"][name="__RequestVerificationToken"]').value
                    },
                    body: JSON.stringify(credentialJson),
                    keepalive: false,
                    signal: authAbortSignal
                })
                .then(function (response) {
                    if (response.ok) return response.text();
                    else throw Error('Response Not OK');
                })
                .then(function (text) {
                    try { return JSON.parse(text); }
                    catch (err) { throw Error('Method Not Found'); }
                })
                .then(function (responseJSON) {
                    if (responseJSON.Status == 'Success')
                        showMessageModal("Registration completed.", 'alert-success', false, '', '', true, './login',
                            '_self', 'Login Now', 'btn-primary', false);
                    else
                        showMessageModal(responseJSON.Error, 'alert-danger', false, '', '', true, './register',
                            '_self', 'Try Again', 'btn-primary', false);
                })
                .catch(function (error) {
                    showMessageModal(error, 'alert-danger', false, '', '', true, './register',
                        '_self', 'Try Again', 'btn-primary', false);
                })

            })
            .catch(function (err) {
                if (err == 'AbortError: Request has been aborted.') err = 'Abort by user completed.';
                // No acceptable authenticator or user refused consent. Handle appropriately.
                showMessageModal(err, 'alert-danger', false, '', '', true, './register', '_self',
                    'Try Again', 'btn-primary', false);
            });

    }, 100);
}

The UWPP implements the recursive credentialToJSON function in site.js to serialize the credential.

function credentialToJSON(pubKeyCred) {
    if (pubKeyCred instanceof Array) {
        let arr = [];
        for (let i of pubKeyCred)
            arr.push(credentialToJSON(i));
        return arr;
    }
    if (pubKeyCred instanceof ArrayBuffer)
        return btoa(String.fromCharCode.apply(null, new Uint8Array(pubKeyCred)));
    if (pubKeyCred instanceof Object) {
        let obj = {};
        for (let key in pubKeyCred)
            obj[key] = credentialToJSON(pubKeyCred[key]);
        return obj;
    }
    return pubKeyCred;
}

The UWPP implements a Challenge entity to log challenges with all available data. The Verified property allows the UWPP to log failed challenges. The authenticator's response is posted to the server for decoding, verifying and storage.

RegisterChallenge.cshtml.cs > OnPostCallbackAsync:
public async Task<JsonResult> OnPostCallbackAsync([FromBody] AttestationResponseModel attestationResponse)
{
    string path = HttpContext.Request.Path.ToString().ToLower();
    if (path.Length > 256) path = path.Substring(0, 256);
    var uaString = HttpContext.Request.Headers["User-Agent"].FirstOrDefault();
    if (string.IsNullOrEmpty(uaString)) uaString = "NullOrEmpty";
    else if (uaString.Length > 512) uaString = uaString.Substring(0, 512);
    string anonymizedIp = HttpContext.Connection.RemoteIpAddress.AnonymizeIP();

    bool isFirefox = uaString.Contains("Firefox");

    if (TempData.Peek("ChallengeGuid") == null || TempData.Peek("LoginName") == null)
    {
        if (!await _credentialService.AddNewChallengeAsync(string.Empty, false, 0, string.Empty, 0,
            string.Empty, true, string.Empty, string.Empty, string.Empty, string.Empty, 0, string.Empty,
            string.Empty, "TempData not found.", path, anonymizedIp, uaString))
            return new JsonResult(new { Status = "Failed", Error = "An error occurred creating a failed Challenge." });
        return new JsonResult(new { Status = "Failed", Error = "TempData not found." });
    }

    // Recreate UserHandle
    var userHandle = Convert.ToBase64String(ChallengeGuid.ToByteArray());

    // 7.1.3 Let response be credential.response. If response is not an instance of AuthenticatorAttestationResponse,
    // abort the ceremony with a user-visible error.
    if (attestationResponse == null)
    {
        if (!await _credentialService.AddNewChallengeAsync(ChallengeGuid.ToString(), false, 0, string.Empty, 0,
            string.Empty, true, string.Empty, string.Empty, string.Empty, userHandle, 0, string.Empty, string.Empty,
            "7.1.3 AttestationResponse = null", path, anonymizedIp, uaString))
            return new JsonResult(new { Status = "Failed", Error = "An error occurred creating a failed Challenge." });
        return new JsonResult(new { Status = "Failed", Error = "7.1.3 AttestationResponse = null" });
    }

    var currentOrigin = HttpContext.Request.Scheme + Uri.SchemeDelimiter + HttpContext.Request.Host;

    var currentHost = HttpContext.Request.Host.ToString();
    var relyingPartyId = currentHost.Split(":")[0];

    var userVerification = (int)_challengeOptions.AuthenticatorSelection.UserVerification;
    var pubKeyCredParamsAlgs = _challengeOptions.PubKeyCredParamsAlgs;
    var attestation = _challengeOptions.Attestation;

    // 7.1.5 to 7.1.21
    var result = AttestationCrypto.DecodeAttestationResponse(attestationResponse, ChallengeGuid,
        currentOrigin, relyingPartyId, userVerification, pubKeyCredParamsAlgs, attestation, isFirefox);
    if (!result.Verified)
    {
        if (!await _credentialService.AddNewChallengeAsync(ChallengeGuid.ToString(), false, 0, result.AaGuidString, 0,
            attestationResponse.Response.ClientDataJson, true, string.Empty,
            attestationResponse.Response.AttestationObject, string.Empty, userHandle, result.SignatureCounter,
            result.SanExtensionJson, result.ClientExtensionResultsJson, result.Error, path, uaString, anonymizedIp))
            return new JsonResult(new { Status = "Failed", Error = "An error occurred creating a failed Challenge." });
        return new JsonResult(new { Status = "Failed", Error = result.Error });
    }

    // 7.1.22 Check that the credentialId is not yet registered to any other user. If registration is requested for a
    // credential that is already registered to a different user, the Relying Party SHOULD fail this registration ceremony.
    if (await _credentialService.GetIdByCredentialIdAsync(result.CredentialId) > 0)
    {
        if (!await _credentialService.AddNewChallengeAsync(ChallengeGuid.ToString(), false, 0, result.AaGuidString, 0,
            attestationResponse.Response.ClientDataJson, true, string.Empty, 
            attestationResponse.Response.AttestationObject, string.Empty, userHandle, result.SignatureCounter,
            result.SanExtensionJson, result.ClientExtensionResultsJson, "7.1.22 Duplicate Credential Id.", path, uaString,
            anonymizedIp))
            return new JsonResult(new { Status = "Failed", Error = "An error occurred creating a failed Challenge." });
        return new JsonResult(new { Status = "Failed", Error = "7.1.22 Duplicate Credential Id." });
    }

    // 7.1.23 If the attestation statement attStmt verified successfully and is found to be trustworthy, then register the
    // new credential with the account that was denoted in options.user:
    // Associate the user’s account with the credentialId and credentialPublicKey in authData.attestedCredentialData,
    // as appropriate for the Relying Party's system. Associate the credentialId with a new stored signature counter
    // value initialized to the value of authData.signCount.

    // Create and store user
    var administratorRole = false;
    if (await _userService.GetAppUserCountAsync() == 0)
        administratorRole = true;

    var appUser = new AppUser()
    {
        LoginName = LoginName,
        LoginNameUppercase = LoginName.ToUpper(),
        AdministratorRole = administratorRole,
        UserHandle = userHandle,
        SecurityStamp = Guid.NewGuid().ToString(),
        CreatedDate = DateTimeOffset.UtcNow
    };

    if (!await _userService.AddAppUserAsync(appUser).ConfigureAwait(false))
    {
        if (!await _credentialService.AddNewChallengeAsync(ChallengeGuid.ToString(), false, appUser.Id,
            result.AaGuidString, 0, attestationResponse.Response.ClientDataJson, true, string.Empty,
            attestationResponse.Response.AttestationObject, string.Empty, userHandle, result.SignatureCounter,
            result.SanExtensionJson, result.ClientExtensionResultsJson, "7.1.23 An error occurred creating an AppUser.",
            path, uaString, anonymizedIp))
            return new JsonResult(new { Status = "Failed", Error = "An error occurred creating a failed Challenge." });
        return new JsonResult(new { Status = "Failed", Error = "7.1.23 An error occurred creating an AppUser." });
    }

    if (appUser.Id == 0)
    {
        if (!await _credentialService.AddNewChallengeAsync(ChallengeGuid.ToString(), false, appUser.Id,
            result.AaGuidString, 0, attestationResponse.Response.ClientDataJson, true, string.Empty,
            attestationResponse.Response.AttestationObject, string.Empty, userHandle, result.SignatureCounter,
            result.SanExtensionJson, result.ClientExtensionResultsJson, "7.1.23 New AppUser Id = 0.", path, uaString,
            anonymizedIp))
            return new JsonResult(new { Status = "Failed", Error = "An error occurred creating a failed Challenge." });
        return new JsonResult(new { Status = "Failed", Error = "7.1.23 New AppUser Id = 0." });
    }

    var credential = new Credential(appUser.Id, result.CredentialId, result.PublicKeyJson,
        attestationResponse.Response.ClientDataJson, attestationResponse.Response.AttestationObject,
        attestationResponse.Name, true, result.AaGuidString, result.SanExtensionJson,
        result.ClientExtensionResultsJson, userHandle, result.SignatureCounter, result.Transports,
        attestationResponse.Attachment, ChallengeGuid.ToString(), anonymizedIp, uaString);

    if (!await _credentialService.AddCredentialAsync(credential))
    {
        if (!await _credentialService.AddNewChallengeAsync(ChallengeGuid.ToString(), false, appUser.Id,
            result.AaGuidString, credential.Id, attestationResponse.Response.ClientDataJson, true, string.Empty,
            attestationResponse.Response.AttestationObject, string.Empty, userHandle, result.SignatureCounter,
            result.SanExtensionJson, result.ClientExtensionResultsJson, "7.1.23 An error occurred creating a Credential.",
            path, uaString, anonymizedIp))
            return new JsonResult(new { Status = "Failed", Error = "An error occurred creating a failed Challenge." });
        return new JsonResult(new { Status = "Failed", Error = "7.1.23 An error occurred creating a Credential." });
    }

    if (credential.Id == 0)
    {
        if (!await _credentialService.AddNewChallengeAsync(ChallengeGuid.ToString(), false, appUser.Id, result.AaGuidString, credential.Id,
            attestationResponse.Response.ClientDataJson, true, string.Empty,
            attestationResponse.Response.AttestationObject, string.Empty, userHandle, result.SignatureCounter,
            result.SanExtensionJson, result.ClientExtensionResultsJson, "7.1.23 New Credential Id = 0.", path, uaString,
            anonymizedIp))
            return new JsonResult(new { Status = "Failed", Error = "An error occurred creating a failed Challenge." });
        return new JsonResult(new { Status = "Failed", Error = "7.1.23 New Credential Id = 0." });
    }

    var challengeCompleted = new Challenge(ChallengeGuid.ToString(), true, appUser.Id, result.AaGuidString,
        credential.Id, attestationResponse.Response.ClientDataJson, true, string.Empty,
        attestationResponse.Response.AttestationObject, string.Empty, userHandle, result.SignatureCounter,
        result.SanExtensionJson, result.ClientExtensionResultsJson, string.Empty, path, uaString, anonymizedIp);

    if (!await _credentialService.AddChallengeAsync(challengeCompleted))
    {
        if (!await _credentialService.AddNewChallengeAsync(ChallengeGuid.ToString(), false, appUser.Id, result.AaGuidString,
            credential.Id, attestationResponse.Response.ClientDataJson, true, string.Empty,
            attestationResponse.Response.AttestationObject, string.Empty, userHandle, result.SignatureCounter,
            result.SanExtensionJson, result.ClientExtensionResultsJson,
            "An error occurred creating a completed Challenge.", path, uaString, anonymizedIp))
            return new JsonResult(new { Status = "Failed", Error = "An error occurred creating a failed Challenge." });
        return new JsonResult(new { Status = "Failed", Error = "An error occurred creating a ChallengeCompleted." });
    }

    if (challengeCompleted.Id == 0)
    {
        if (!await _credentialService.AddNewChallengeAsync(ChallengeGuid.ToString(), false, appUser.Id,
            result.AaGuidString, credential.Id, attestationResponse.Response.ClientDataJson, true, string.Empty,
            attestationResponse.Response.AttestationObject, string.Empty, userHandle, result.SignatureCounter,
            result.SanExtensionJson, result.ClientExtensionResultsJson, "New completed Challenge Id = 0.", path,
            uaString, anonymizedIp))
            return new JsonResult(new { Status = "Failed", Error = "An error occurred creating a failed Challenge." });
        return new JsonResult(new { Status = "Failed", Error = "New ChallengeCompleted Id = 0." });
    }

    return new JsonResult(new { Status = "Success" });
}

The AttestationResponseModel contains the authenticator response's ClientDataJSON and AttestationObject properties.

AttestationResponseModel.cs:
public class AttestationResponseModel
{
    public string Id { get; set; }
    public AuthenticatorAttestationResponse Response { get; set; }
    public string Name { get; set; }
    public string Attachment { get; set; }
    public object ExtensionResults { get; set; }
}
public class AuthenticatorAttestationResponse
{
    public string AttestationObject { get; set; }
    public string ClientDataJson { get; set; }
    public List<string> Transports { get; set; }
}

The ClientDataJSON and AttestationObject are decoded to bytes with the Microsoft. AspNetCore. WebUtilities. Base64UrlTextEncoder. Decode function. The ClientDataJson is used to verify the type (create or get), the challenge and Relying Party's origin. The UWPP implements the PeterO.Cbor NuGet package to cast the AttestationObject as a CBOR object. The CBOR authData property is converted to a byte array. The UWPP implements an AuthenticatorUtils class with many enums and functions from GitHub - passwordless-lib / fido2-net-lib. The AuthenticatorUtils. GetSizedByteArray function slices the authData byte array to extract and validate properties. The most important properties are the CredentialId and the PublicKey which are used to configure an authentication request and validate the response from a log in or assertion ceremony.

AttestationCrypto.cs:
/// <summary>
/// Gets a <see cref="AuthenticationResult"/> with <see cref="AuthenticationResult.Verified"/> indicating success
/// and <see cref="AuthenticationResult.Error"/> indicating cause of failure.
/// </summary>
/// <param name="attestationResponse"></param>
/// <param name="challegeGuid"></param>
/// <param name="currentOrigin"></param>
/// <param name="relyingPartyId"></param>
/// <param name="userVerification"></param>
/// <param name="pubKeyCredParamsAlgs"></param>
/// <param name="attestation"></param>
/// <param name="isFirefox"></param>
/// <returns><see cref="AuthenticationResult"/> with <see cref="AuthenticationResult.Verified"/> indicating success.</returns>
/// <remarks>
/// All known <see cref="AuthenticationResult"/> properties are populated when the result is returned.
/// </remarks>
public static AuthenticationResult DecodeAttestationResponse(AttestationResponseModel attestationResponse,
    Guid challegeGuid, string currentOrigin, string relyingPartyId, int userVerification,
    IList<int> pubKeyCredParamsAlgs, string attestation, bool isFirefox)
{
    // 7.1.4 Let clientExtensionResults be the result of calling credential.getClientExtensionResults().
    // Assigned to ExtensionResults in JavaScript. 
    var clientExtensionResultsJson = JsonSerializer.Serialize(attestationResponse.ExtensionResults);
    // 7.1.23 It is RECOMMENDED to also:
    // Associate the credentialId with the transport hints returned by calling credential.response.getTransports().
    // This value SHOULD NOT be modified before or after storing it. It is RECOMMENDED to use this value to
    // populate the transports of the allowCredentials option in future get() calls to help the client know how to
    // find a suitable authenticator.
    // Assigned to Response.Transports in JavaScript.
    var transports = string.Join(", ", attestationResponse.Response.Transports);

    // Decode ClientDataJSON and AttestationObject
    var clientDataBytes = Base64UrlTextEncoder.Decode(attestationResponse.Response.ClientDataJson);
    var attestationObjectBytes = Base64UrlTextEncoder.Decode(attestationResponse.Response.AttestationObject);
    // 7.1.5 Let JSONtext be the result of running UTF-8 decode on the value of response.clientDataJSON.
    var clientDataAscii = Encoding.ASCII.GetString(clientDataBytes);
    // 7.1.6 Let C, the client data claimed as collected during the credential creation, be the result of running
    // an implementation-specific JSON parser on JSONtext.
    var clientData = JsonSerializer.Deserialize<ClientData>(clientDataAscii);
    // 7.1.7 Verify that the value of C.type is webauthn.create.
    if (clientData.Type != "webauthn.create")
        return new AuthenticationResult() {
            Error = $"7.1.7 Invalid client data type, expected: webauthn.create, detected: {clientData.Type}.",
            ClientExtensionResultsJson = clientExtensionResultsJson,
            Transports = transports
        };
    // 7.1.8 Verify that the value of C.challenge equals the base64url encoding of options.challenge.
    var tempDataChallenge = challegeGuid.ToByteArray(); // 16 bytes
    var clientChallenge = Base64UrlTextEncoder.Decode(clientData.Challenge);
    if (!clientChallenge.SequenceEqual(tempDataChallenge))
        return new AuthenticationResult() { 
            Error = $"7.1.8 Invalid challenge, expected: {challegeGuid}, detected: {clientData.Challenge}.",
            ClientExtensionResultsJson = clientExtensionResultsJson,
            Transports = transports
        };
    // 7.1.9 Verify that the value of C.origin matches the Relying Party's origin.
    if (clientData.Origin != currentOrigin)
        return new AuthenticationResult() {
            Error = $"7.1.9 Invalid origin, expected: {currentOrigin}, detected: {clientData.Origin}.",
            ClientExtensionResultsJson = clientExtensionResultsJson,
            Transports = transports
        };
            
    // 7.1.10
    // Verify that the value of clientData.tokenBinding.status matches the state of Token Binding for the TLS
    // connection over which the assertion was obtained. If Token Binding was used on that TLS connection,
    // also verify that clientData.tokenBinding.id matches the base64url encoding of the Token Binding ID for
    // the connection. Not used very often at the moment due to the lack of support by major web browsers.
    // https://research.kudelskisecurity.com/2020/02/12/fido2-deep-dive-attestations-trust-model-and-security/

    // 7.1.11 Let hash be the result of computing a hash over response.clientDataJSON using SHA-256.
    var sha256Hasher = new SHA256Managed();
    var clientDataHash = sha256Hasher.ComputeHash(clientDataBytes);
    // 7.1.12 Perform CBOR decoding on the attestationObject field of the AuthenticatorAttestationResponse
    // structure to obtain the attestation statement format fmt, the authenticator data authData, and the attestation
    // statement attStmt.
    var cbor = CBORObject.DecodeFromBytes(attestationObjectBytes, CBOREncodeOptions.Default);

    var authData = cbor["authData"].GetByteString();
    var offset = 0;

    // 7.1.13 Verify that the rpIdHash in authData is the SHA-256 hash of the RP ID expected by the Relying Party.
    var rpIdHash = AuthenticatorUtils.GetSizedByteArray(authData, ref offset, 32);
    var relyingPartyIdHash = sha256Hasher.ComputeHash(Encoding.ASCII.GetBytes(relyingPartyId));
    if (!rpIdHash.SequenceEqual(relyingPartyIdHash))
        return new AuthenticationResult() { 
            Error = $"7.1.13 Invalid RP ID, expected: {Base64UrlTextEncoder.Encode(relyingPartyIdHash)}, " +
            $"detected: {Base64UrlTextEncoder.Encode(rpIdHash.ToArray())}.",
            ClientExtensionResultsJson = clientExtensionResultsJson,
            Transports = transports
        };

    var flagsByte = AuthenticatorUtils.GetSizedByteArray(authData, ref offset, 1);
    var flags = new BitArray(flagsByte);

    var userPresent = flags[0]; // (UP)
    // Bit 1 reserved for future use (RFU1)
    var userVerified = flags[2]; // (UV)
    // Bits 3-5 reserved for future use (RFU2)
    var attestedCredentialData = flags[6]; // (AT) "Indicates whether the authenticator added attested credential data"
    var extensionDataIncluded = flags[7]; // (ED)

    if (!attestedCredentialData)
        return new AuthenticationResult() { 
            Error = "Attestation should have attestedCredentialData, expected: true, detected: false.",
            ClientExtensionResultsJson = clientExtensionResultsJson,
            Transports = transports
        };

    if (extensionDataIncluded)
        return new AuthenticationResult() { 
            Error = "Extension Data is not supported but extensionDataIncluded flag is set, expected: false, detected: true.",
            ClientExtensionResultsJson = clientExtensionResultsJson,
            Transports = transports
        };

    // Signature counter (4 bytes, big-endian unint32)
    var counterBytes = AuthenticatorUtils.GetSizedByteArray(authData, ref offset, 4);
    Array.Reverse(counterBytes);
    var counter = BitConverter.ToUInt32(counterBytes);

    // AAGUID (16 bytes, big-endian GUID)
    // 00000000-0000-0000-0000-000000000000 when Attestation Option = none
    var aaguidBytes = AuthenticatorUtils.GetSizedByteArray(authData, ref offset, 16);
    var aaguid = AuthenticatorUtils.GuidFromBigEndian(aaguidBytes);

    // CredentialId Length (2 bytes, big-endian uint16)
    var credentialIdLengthBytes = AuthenticatorUtils.GetSizedByteArray(authData, ref offset, 2);
    Array.Reverse(credentialIdLengthBytes);
    var credentialIdLength = BitConverter.ToUInt16(credentialIdLengthBytes);
    // Credential ID (CredentialId Length bytes)
    var credentialId = AuthenticatorUtils.GetSizedByteArray(authData, ref offset, credentialIdLength);

    // Create PublicKey
    var publicKeyBytes = AuthenticatorUtils.GetSizedByteArray(authData, ref offset, (ushort)(authData.Length - offset));
    var cborAuthDataPublicKey = CBORObject.DecodeFromBytes(publicKeyBytes);

    var publicKeyJson = cborAuthDataPublicKey.ToJSONString();

    // 7.1.14 Verify that the User Present bit of the flags in authData is set.
    if (!userPresent)
        return new AuthenticationResult()
        {
            Error = "7.1.14 User not present, expected: true, detected: false.",
            ClientExtensionResultsJson = clientExtensionResultsJson,
            Transports = transports,
            AaGuidString = aaguid.ToString()
        };

    // 7.1.15 If user verification is required for this registration,
    // verify that the User Verified bit of the flags in authData is set.
    if (userVerification == 0 && !userVerified)
        return new AuthenticationResult()
        {
            Error = "7.1.15 User verification is required but the authData's UserVerified flag is not set.",
            AaGuidString = aaguid.ToString()
        };

    // 7.1.16 Verify that the "alg" parameter in the credential public key in authData matches the alg attribute of one
    // of the items in options.pubKeyCredParams.
    var authDataPublicKeyAlgorithm = cborAuthDataPublicKey[3].ToObject<int>();
    if (!pubKeyCredParamsAlgs.Contains(authDataPublicKeyAlgorithm))
        return new AuthenticationResult()
        {
            Error = $"7.1.16 AuthData PublicKey algorithm not found in pubKeyCredParams, " +
            $"AuthData: {authDataPublicKeyAlgorithm}, " +
            $"PubKeyCredParams Algorithms: {string.Join(", ", pubKeyCredParamsAlgs)}.",
            ClientExtensionResultsJson = clientExtensionResultsJson,
            Transports = transports,
            AaGuidString = aaguid.ToString()
        };

    // 7.1.17 Verify that the values of the client extension outputs in clientExtensionResults
    // See 7.1.4 Current evaluation = ValueKind = Object {}

    // 7.1.18 Determine the attestation statement format by performing a USASCII case-sensitive match on fmt
    // against the set of supported WebAuthn Attestation Statement Format Identifier values.
    // If Attestation: none then fmt = none.
    // If Attestation: direct then fmt = packed or tpm (Trusted Platform Module).
    var fmt = cbor["fmt"].AsString();

    // 7.1.21 Assess the attestation trustworthiness using the outputs of the verification procedure in step 19, as follows:            
    // a If no attestation was provided, verify that None attestation is acceptable under Relying Party policy.
    if (fmt.Equals("none"))
    {
        // Firefox v90.0 generates credentials using TPM with Windows Hello - but does not send TPM attestation
        // https://github.com/w3c/webauthn/issues/1620
        // https://bugzilla.mozilla.org/show_bug.cgi?id=1716859
        if (attestation == "none" || isFirefox)
            return new AuthenticationResult
            {
                AaGuidString = aaguid.ToString(),
                CredentialId = Convert.ToBase64String(credentialId.ToArray()),
                PublicKeyJson = publicKeyJson,
                SignatureCounter = counter,
                ClientExtensionResultsJson = clientExtensionResultsJson,
                Transports = transports,
                SanExtensionJson = string.Empty,
                Verified = true
            };
        else
            return new AuthenticationResult()
            {
                Error = "AttStat fmt = none, expected: packed or tpm.",
                AaGuidString = aaguid.ToString(),
                CredentialId = Convert.ToBase64String(credentialId.ToArray()),
                PublicKeyJson = publicKeyJson,
                SignatureCounter = counter,
                ClientExtensionResultsJson = clientExtensionResultsJson,
                Transports = transports,
                SanExtensionJson = string.Empty
            };
    }

    // When the Attestation option in CredentialCreationOptions is set to "direct",
    // the fmt should return a "packed" or "tpm" (Trusted Platform Module) value.
    // The "packed" and "tpm" fmt validations are described in the FIDO2 Attestation Trust article.
}
Ken Haggerty
Created 07/17/21
Updated 08/16/21 04:01 GMT

Log In or Reset Quota to read more.

Successfully completed. Thank you for contributing.
Processing...
Something went wrong. Please try again.
Contribute to enjoy content without advertisments.
You can contribute without registering.

Comments(0)

Loading...
Loading...

Not accepting new comments.

Submit your comment. Comments are moderated.

User Image.
DisplayedName - Member Since ?