Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update AttestationVerifier api to Async #458

Merged
merged 4 commits into from
Dec 22, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions Src/Fido2/AttestationFormat/AndroidKey.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Linq;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;

using Fido2NetLib.Cbor;
using Fido2NetLib.Exceptions;
Expand Down Expand Up @@ -131,7 +132,7 @@ public static bool IsPurposeSign(byte[] attExtBytes)
return (softwareEnforcedPurposeValue is 2 && teeEnforcedPurposeValue is 2);
}

public override (AttestationType, X509Certificate2[]) Verify(VerifyAttestationRequest request)
public override ValueTask<VerifyAttestationResult> VerifyAsync(VerifyAttestationRequest request)
{
// 1. Verify that attStmt is valid CBOR conforming to the syntax defined above and perform CBOR decoding on it to extract the contained fields
// (handled in base class)
Expand Down Expand Up @@ -220,6 +221,6 @@ public override (AttestationType, X509Certificate2[]) Verify(VerifyAttestationRe
if (!IsPurposeSign(attExtBytes))
throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Found purpose field not set to KM_PURPOSE_SIGN in android key attestation certificate extension");

return (AttestationType.Basic, trustPath);
return new(new VerifyAttestationResult(AttestationType.Basic, trustPath));
}
}
8 changes: 4 additions & 4 deletions Src/Fido2/AttestationFormat/AndroidSafetyNet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

using Fido2NetLib.Cbor;
using Fido2NetLib.Exceptions;
Expand All @@ -20,7 +21,7 @@ internal sealed class AndroidSafetyNet : AttestationVerifier
{
private const int _driftTolerance = 0;

public override (AttestationType, X509Certificate2[]) Verify(VerifyAttestationRequest request)
public override ValueTask<VerifyAttestationResult> VerifyAsync(VerifyAttestationRequest request)
{
// 1. Verify that attStmt is valid CBOR conforming to the syntax defined above and perform
// CBOR decoding on it to extract the contained fields
Expand Down Expand Up @@ -157,8 +158,7 @@ public override (AttestationType, X509Certificate2[]) Verify(VerifyAttestationRe
throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Nonce value not base64string in SafetyNet attestation", ex);
}

Span<byte> dataHash = stackalloc byte[32];
SHA256.HashData(request.Data, dataHash);
byte[] dataHash = SHA256.HashData(request.Data);

if (!dataHash.SequenceEqual(nonceHash))
{
Expand All @@ -180,6 +180,6 @@ public override (AttestationType, X509Certificate2[]) Verify(VerifyAttestationRe
if (true != ctsProfileMatch)
throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "SafetyNet response ctsProfileMatch false");

return (AttestationType.Basic, new X509Certificate2[] { attestationCert });
return new(new VerifyAttestationResult(AttestationType.Basic, new X509Certificate2[] { attestationCert }));
}
}
5 changes: 3 additions & 2 deletions Src/Fido2/AttestationFormat/Apple.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Linq;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;

using Fido2NetLib.Cbor;
using Fido2NetLib.Exceptions;
Expand Down Expand Up @@ -40,7 +41,7 @@ public static byte[] GetAppleAttestationExtensionValue(X509ExtensionCollection e
}
}

public override (AttestationType, X509Certificate2[]) Verify(VerifyAttestationRequest request)
public override ValueTask<VerifyAttestationResult> VerifyAsync(VerifyAttestationRequest request)
{
// 1. Verify that attStmt is valid CBOR conforming to the syntax defined above and perform CBOR decoding on it to extract the contained fields.
if (!(request.X5c is CborArray { Length: >= 2 } x5cArray && x5cArray[0] is CborByteString { Length: > 0 }))
Expand Down Expand Up @@ -86,6 +87,6 @@ public override (AttestationType, X509Certificate2[]) Verify(VerifyAttestationRe
throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Credential public key in Apple attestation does not match subject public key of credCert");

// 7. If successful, return implementation-specific values representing attestation type Anonymous CA and attestation trust path x5c.
return (AttestationType.Basic, trustPath);
return new(new VerifyAttestationResult(AttestationType.Basic, trustPath));
}
}
20 changes: 9 additions & 11 deletions Src/Fido2/AttestationFormat/AppleAppAttest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
using System.Linq;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;

using Fido2NetLib.Cbor;
using Fido2NetLib.Objects;

namespace Fido2NetLib;

Expand Down Expand Up @@ -47,7 +47,7 @@ public static byte[] GetAppleAppIdFromCredCertExtValue(X509ExtensionCollection e
// 61707061-7474-6573-7400-000000000000
public static readonly Guid prodAaguid = new("61707061-7474-6573-7400-000000000000");

public override (AttestationType, X509Certificate2[]) Verify(VerifyAttestationRequest request)
public override async ValueTask<VerifyAttestationResult> VerifyAsync(VerifyAttestationRequest request)
{
// 1. Verify that the x5c array contains the intermediate and leaf certificates for App Attest, starting from the credential certificate in the first data buffer in the array (credcert).
if (!(request.X5c is CborArray { Length: 2 } x5cArray && x5cArray[0] is CborByteString { Length: > 0 } && x5cArray[1] is CborByteString { Length: > 0 }))
Expand Down Expand Up @@ -81,22 +81,20 @@ public override (AttestationType, X509Certificate2[]) Verify(VerifyAttestationRe
// 4. Obtain the value of the credCert extension with OID 1.2.840.113635.100.8.2, which is a DER - encoded ASN.1 sequence.Decode the sequence and extract the single octet string that it contains. Verify that the string equals nonce.
// Steps 2 - 4 done in the "apple" format verifier
var apple = new Apple();
(var attType, var trustPath) = apple.Verify(request);
(var attType, var trustPath) = await apple.VerifyAsync(request).ConfigureAwait(false);

// 5. Create the SHA256 hash of the public key in credCert, and verify that it matches the key identifier from your app.
Span<byte> credCertPKHash = stackalloc byte[32];
SHA256.HashData(credCert.GetPublicKey(), credCertPKHash);
ReadOnlySpan<byte> keyIdentifier = Convert.FromHexString(credCert.GetNameInfo(X509NameType.SimpleName, false));
if (!credCertPKHash.SequenceEqual(keyIdentifier))
byte[] credCertPKHash = SHA256.HashData(credCert.GetPublicKey());
byte[] keyIdentifier = Convert.FromHexString(credCert.GetNameInfo(X509NameType.SimpleName, false));
if (!credCertPKHash.AsSpan().SequenceEqual(keyIdentifier))
{
throw new Fido2VerificationException("Public key hash does not match key identifier in Apple AppAttest attestation");
}

// 6. Compute the SHA256 hash of your app's App ID, and verify that it’s the same as the authenticator data's RP ID hash.
var appId = GetAppleAppIdFromCredCertExtValue(credCert.Extensions);
Span<byte> appIdHash = stackalloc byte[32];
SHA256.HashData(appId, appIdHash);
if (!appIdHash.SequenceEqual(request.AuthData.RpIdHash))
byte[] appIdHash = SHA256.HashData(appId);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure to understand this change. Why not continue to hash into a stackalloc byte[32] directly instead of allocating a byte array?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ref structs (including Spans) aren't allowed inside of an async block.

if (!appIdHash.AsSpan().SequenceEqual(request.AuthData.RpIdHash))
{
throw new Fido2VerificationException("App ID hash does not match RP ID hash in Apple AppAttest attestation");
}
Expand All @@ -119,6 +117,6 @@ public override (AttestationType, X509Certificate2[]) Verify(VerifyAttestationRe
throw new Fido2VerificationException("Mismatch between credentialId and keyIdentifier in Apple AppAttest attestation");
}

return (attType, trustPath);
return new VerifyAttestationResult(attType, trustPath);
}
}
7 changes: 4 additions & 3 deletions Src/Fido2/AttestationFormat/AttestationVerifier.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Formats.Asn1;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;

using Fido2NetLib.Cbor;
using Fido2NetLib.Exceptions;
Expand All @@ -10,12 +11,12 @@ namespace Fido2NetLib;

public abstract class AttestationVerifier
{
public (AttestationType, X509Certificate2[]) Verify(CborMap attStmt, AuthenticatorData authenticatorData, byte[] clientDataHash)
public ValueTask<VerifyAttestationResult> VerifyAsync(CborMap attStmt, AuthenticatorData authenticatorData, byte[] clientDataHash)
{
return Verify(new VerifyAttestationRequest(attStmt, authenticatorData, clientDataHash));
return VerifyAsync(new VerifyAttestationRequest(attStmt, authenticatorData, clientDataHash));
}

public abstract (AttestationType, X509Certificate2[]) Verify(VerifyAttestationRequest request);
public abstract ValueTask<VerifyAttestationResult> VerifyAsync(VerifyAttestationRequest request);

public static AttestationVerifier Create(string formatIdentifier)
{
Expand Down
5 changes: 3 additions & 2 deletions Src/Fido2/AttestationFormat/FidoU2f.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;

using Fido2NetLib.Cbor;
using Fido2NetLib.Exceptions;
Expand All @@ -10,7 +11,7 @@ namespace Fido2NetLib;

internal sealed class FidoU2f : AttestationVerifier
{
public override (AttestationType, X509Certificate2[]) Verify(VerifyAttestationRequest request)
public override ValueTask<VerifyAttestationResult> VerifyAsync(VerifyAttestationRequest request)
{
// verify that aaguid is 16 empty bytes (note: required by fido2 conformance testing, could not find this in spec?)
if (request.AuthData.AttestedCredentialData!.AaGuid.CompareTo(Guid.Empty) != 0)
Expand Down Expand Up @@ -86,6 +87,6 @@ public override (AttestationType, X509Certificate2[]) Verify(VerifyAttestationRe

var trustPath = new X509Certificate2[1] { attCert };

return (AttestationType.AttCa, trustPath);
return new(new VerifyAttestationResult(AttestationType.AttCa, trustPath));
}
}
6 changes: 3 additions & 3 deletions Src/Fido2/AttestationFormat/None.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;

using Fido2NetLib.Exceptions;
using Fido2NetLib.Objects;
Expand All @@ -7,11 +7,11 @@ namespace Fido2NetLib;

public sealed class None : AttestationVerifier
{
public override (AttestationType, X509Certificate2[]) Verify(VerifyAttestationRequest request)
public override ValueTask<VerifyAttestationResult> VerifyAsync(VerifyAttestationRequest request)
{
if (request.AttStmt.Count != 0)
throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Attestation format none should have no attestation statement");

return (AttestationType.None, null!);
return new(new VerifyAttestationResult(AttestationType.None, null!));
}
}
7 changes: 4 additions & 3 deletions Src/Fido2/AttestationFormat/Packed.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;

using Fido2NetLib.Cbor;
using Fido2NetLib.Exceptions;
Expand Down Expand Up @@ -36,7 +37,7 @@ public static bool IsValidPackedAttnCertSubject(string attnCertSubj)
&& subjectMap.TryGetValue("CN", out var cn) && cn.Length > 0;
}

public override (AttestationType, X509Certificate2[]) Verify(VerifyAttestationRequest request)
public override ValueTask<VerifyAttestationResult> VerifyAsync(VerifyAttestationRequest request)
{
// 1. Verify that attStmt is valid CBOR conforming to the syntax defined above and
// perform CBOR decoding on it to extract the contained fields.
Expand Down Expand Up @@ -117,7 +118,7 @@ public override (AttestationType, X509Certificate2[]) Verify(VerifyAttestationRe

// 2d. Optionally, inspect x5c and consult externally provided knowledge to determine whether attStmt conveys a Basic or AttCA attestation

return (AttestationType.AttCa, trustPath);
return new(new VerifyAttestationResult(AttestationType.AttCa, trustPath));
}

// 3. If ecdaaKeyId is present, then the attestation type is ECDAA
Expand Down Expand Up @@ -145,7 +146,7 @@ public override (AttestationType, X509Certificate2[]) Verify(VerifyAttestationRe
if (!request.AuthData.AttestedCredentialData.CredentialPublicKey.Verify(request.Data, sig))
throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Failed to validate signature");

return (AttestationType.Self, null!);
return new(new VerifyAttestationResult(AttestationType.Self, null!));
}
}
}
5 changes: 3 additions & 2 deletions Src/Fido2/AttestationFormat/Tpm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Linq;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;

using Fido2NetLib.Cbor;
using Fido2NetLib.Exceptions;
Expand Down Expand Up @@ -43,7 +44,7 @@ internal sealed class Tpm : AttestationVerifier
"id:474F4F47", // 'GOOG' Google
};

public override (AttestationType, X509Certificate2[]) Verify(VerifyAttestationRequest request)
public override ValueTask<VerifyAttestationResult> VerifyAsync(VerifyAttestationRequest request)
{
// 1. Verify that attStmt is valid CBOR conforming to the syntax defined above and perform CBOR decoding on it to extract the contained fields.
// (handled in base class)
Expand Down Expand Up @@ -202,7 +203,7 @@ public override (AttestationType, X509Certificate2[]) Verify(VerifyAttestationRe
throw new Fido2VerificationException($"aaguid malformed, expected {request.AuthData.AttestedCredentialData.AaGuid}, got {new Guid(aaguid)}");
}

return (AttestationType.AttCa, trustPath);
return new(new VerifyAttestationResult(AttestationType.AttCa, trustPath));
}
// If ecdaaKeyId is present, then the attestation type is ECDAA
else if (request.EcdaaKeyId != null)
Expand Down
23 changes: 23 additions & 0 deletions Src/Fido2/AttestationFormat/VerifyAttestationResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System.Security.Cryptography.X509Certificates;

using Fido2NetLib.Objects;

namespace Fido2NetLib;

public sealed class VerifyAttestationResult
{
public VerifyAttestationResult(AttestationType type, X509Certificate2[] certificates)
{
Type = type;
Certificates = certificates;
}

public AttestationType Type { get; }

public X509Certificate2[] Certificates { get; }

public void Deconstruct(out AttestationType type, out X509Certificate2[] certificates)
{
(type, certificates) = (Type, Certificates);
}
}
12 changes: 6 additions & 6 deletions Src/Fido2/AuthenticatorAssertionResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ public async Task<VerifyAssertionResult> VerifyAsync(
byte[]? devicePublicKeyResult = null;
if (Raw.Extensions?.DevicePubKey is not null)
{
devicePublicKeyResult = DevicePublicKeyAuthentication(storedDevicePublicKeys, Raw.Extensions, AuthenticatorData, hash);
devicePublicKeyResult = await DevicePublicKeyAuthenticationAsync(storedDevicePublicKeys, Raw.Extensions, AuthenticatorData, hash).ConfigureAwait(false);
}

// Pretty sure these conditions are not able to be met due to the AuthenticatorData constructor implementation
Expand Down Expand Up @@ -200,7 +200,7 @@ public async Task<VerifyAssertionResult> VerifyAsync(
var verifier = AttestationVerifier.Create(fmt);

// 4. Verify that attStmt is a correct attestation statement, conveying a valid attestation signature, by using the attestation statement format fmt’s verification procedure given attStmt, authData and hash.
(var attType, var trustPath) = verifier.Verify(attStmt, AuthenticatorData, hash);
(var attType, var trustPath) = await verifier.VerifyAsync(attStmt, AuthenticatorData, hash).ConfigureAwait(false);

// 5. If validation is successful, obtain a list of acceptable trust anchors (attestation root certificates or ECDAA-Issuer public keys)
// for that attestation type and attestation statement format fmt, from a trusted source or from policy.
Expand Down Expand Up @@ -237,7 +237,7 @@ public async Task<VerifyAssertionResult> VerifyAsync(
/// <param name="authData"></param>
/// <param name="hash"></param>
/// </summary>
private static byte[]? DevicePublicKeyAuthentication(
private static async ValueTask<byte[]?> DevicePublicKeyAuthenticationAsync(
List<byte[]> storedDevicePublicKeys,
AuthenticationExtensionsClientOutputs clientExtensionResults,
AuthenticatorData authData,
Expand Down Expand Up @@ -307,7 +307,7 @@ public async Task<VerifyAssertionResult> VerifyAsync(
try
{
// This is a known device public key with a valid signature and valid attestation and thus a known device. Terminate these verification steps.
_ = verifier.Verify(devicePublicKeyAuthenticatorOutput.AttStmt, devicePublicKeyAuthenticatorOutput.GetAuthenticatorData(), devicePublicKeyAuthenticatorOutput.GetHash());
_ = await verifier.VerifyAsync(devicePublicKeyAuthenticatorOutput.AttStmt, devicePublicKeyAuthenticatorOutput.GetAuthenticatorData(), devicePublicKeyAuthenticatorOutput.GetHash()).ConfigureAwait(false);
}
catch (Exception ex)
{
Expand Down Expand Up @@ -354,7 +354,7 @@ public async Task<VerifyAssertionResult> VerifyAsync(
try
{
// This is a known device public key with a valid signature and valid attestation and thus a known device. Terminate these verification steps.
_ = verifier.Verify(devicePublicKeyAuthenticatorOutput.AttStmt, devicePublicKeyAuthenticatorOutput.GetAuthenticatorData(), devicePublicKeyAuthenticatorOutput.GetHash());
_ = await verifier.VerifyAsync(devicePublicKeyAuthenticatorOutput.AttStmt, devicePublicKeyAuthenticatorOutput.GetAuthenticatorData(), devicePublicKeyAuthenticatorOutput.GetHash()).ConfigureAwait(false);
return devicePublicKeyAuthenticatorOutput.Encode();
}
catch (Exception ex)
Expand Down Expand Up @@ -391,7 +391,7 @@ public async Task<VerifyAssertionResult> VerifyAsync(
try
{
// This is a known device public key with a valid signature and valid attestation and thus a known device. Terminate these verification steps.
_ = verifier.Verify(devicePublicKeyAuthenticatorOutput.AttStmt, devicePublicKeyAuthenticatorOutput.GetAuthenticatorData(), devicePublicKeyAuthenticatorOutput.GetHash());
_ = await verifier.VerifyAsync(devicePublicKeyAuthenticatorOutput.AttStmt, devicePublicKeyAuthenticatorOutput.GetAuthenticatorData(), devicePublicKeyAuthenticatorOutput.GetHash()).ConfigureAwait(false);
return devicePublicKeyAuthenticatorOutput.Encode();
}
catch
Expand Down
Loading