Skip to content

Commit

Permalink
Improve Code Quality (#421)
Browse files Browse the repository at this point in the history
* Rename DeserializeAndValidateBlob -> DeserializeAndValidateBlobAsync

* Make AuthenticatorAttestationResponse immutable

* Expose {PubArea,CertInfo}.Raw over a ReadOnlySpan<byte>

* Make MDSGetEndpointResponse immutable

* Make DevicePublicKeyAuthenticatorOutput immutable, and change properties that allocate to methods

* Make AttestedCredentialData immutable

* Add oid note

* Use Concat helper

* Rename ecparams -> ecParams

* Make AuthenticatorAssertionResponse immutable and improve nullability annotations

* Make AuthenticatorAssertionResponse.Signature a ReadOnlySpan

* Improve error message in AndroidKey

* Use Concat helper

* Breakout TrustAnchor logic from AuthenticatorAttestationResponse

* Pass {config,metadataService, cancellationToken} to DevicePublicKeyRegistrationAsync

* Move CryptoUtils -> /Extensions

* Introduce VerifyAttestationRequest to remove state from AttestationVerifier

* Move CoseKeyTypeFromOid helper to COSE
  • Loading branch information
iamcarbon authored Aug 17, 2023
1 parent bf75254 commit 86b6c6d
Show file tree
Hide file tree
Showing 31 changed files with 601 additions and 557 deletions.
14 changes: 13 additions & 1 deletion Src/Fido2.Models/COSETypes.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace Fido2NetLib.Objects;
using System;

namespace Fido2NetLib.Objects;

/// <summary>
/// CBOR Object Signing and Encryption RFC8152 https://tools.ietf.org/html/rfc8152
Expand Down Expand Up @@ -188,4 +190,14 @@ public enum EllipticCurve
/// </summary>
P256K = 8
}

public static KeyType GetKeyTypeFromOid(string oid)
{
return oid switch
{
"1.2.840.10045.2.1" => KeyType.EC2, // ecPublicKey
"1.2.840.113549.1.1.1" => KeyType.RSA,
_ => throw new Exception($"Unknown oid. Was {oid}")
};
}
}
22 changes: 11 additions & 11 deletions Src/Fido2/AttestationFormat/AndroidKey.cs
Original file line number Diff line number Diff line change
Expand Up @@ -130,22 +130,22 @@ public static bool IsPurposeSign(byte[] attExtBytes)
return (softwareEnforcedPurposeValue is 2 && teeEnforcedPurposeValue is 2);
}

public override (AttestationType, X509Certificate2[]) Verify()
public override (AttestationType, X509Certificate2[]) Verify(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)
if (_attStmt.Count is 0)
if (request.AttStmt.Count is 0)
throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, Fido2ErrorMessages.MissingAndroidKeyAttestationStatement);

if (!TryGetSig(out byte[]? sig))
if (!request.TryGetSig(out byte[]? sig))
throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, Fido2ErrorMessages.InvalidAndroidKeyAttestationSignature);

// 2. Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash
// using the attestation public key in attestnCert with the algorithm specified in alg
if (!(X5c is CborArray { Length: > 0 } x5cArray))
if (!(request.X5c is CborArray { Length: > 0 } x5cArray))
throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, Fido2ErrorMessages.MalformedX5c_AndroidKeyAttestation);

if (!TryGetAlg(out var alg))
if (!request.TryGetAlg(out var alg))
throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, Fido2ErrorMessages.InvalidAndroidKeyAttestationAlgorithm);

var trustPath = new X509Certificate2[x5cArray.Length];
Expand All @@ -172,21 +172,21 @@ public override (AttestationType, X509Certificate2[]) Verify()
X509Certificate2 androidKeyCert = trustPath[0];
ECDsa androidKeyPubKey = androidKeyCert.GetECDsaPublicKey()!; // attestation public key

byte[] ecsig;
byte[] ecSignature;
try
{
ecsig = CryptoUtils.SigFromEcDsaSig(sig, androidKeyPubKey.KeySize);
ecSignature = CryptoUtils.SigFromEcDsaSig(sig, androidKeyPubKey.KeySize);
}
catch (Exception ex)
{
throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Failed to decode android key attestation signature from ASN.1 encoded form", ex);
}

if (!androidKeyPubKey.VerifyData(Data, ecsig, CryptoUtils.HashAlgFromCOSEAlg(alg)))
throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Invalid android key attestation signature");
if (!androidKeyPubKey.VerifyData(request.Data, ecSignature, CryptoUtils.HashAlgFromCOSEAlg(alg)))
throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, Fido2ErrorMessages.InvalidAndroidKeyAttestationSignature);

// 3. Verify that the public key in the first certificate in x5c matches the credentialPublicKey in the attestedCredentialData in authenticatorData.
if (!AuthData.AttestedCredentialData!.CredentialPublicKey.Verify(Data, sig))
if (!request.AuthData.AttestedCredentialData!.CredentialPublicKey.Verify(request.Data, sig))
throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Incorrect credentialPublicKey in android key attestation");

// 4. Verify that the attestationChallenge field in the attestation certificate extension data is identical to clientDataHash
Expand All @@ -197,7 +197,7 @@ public override (AttestationType, X509Certificate2[]) Verify()
try
{
var attestationChallenge = GetAttestationChallenge(attExtBytes);
if (!_clientDataHash.AsSpan().SequenceEqual(attestationChallenge))
if (!request.ClientDataHash.SequenceEqual(attestationChallenge))
throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Mismatch between attestationChallenge and hashedClientDataJson verifying android key attestation certificate extension");
}
catch (Exception)
Expand Down
10 changes: 5 additions & 5 deletions Src/Fido2/AttestationFormat/AndroidSafetyNet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,22 @@ internal sealed class AndroidSafetyNet : AttestationVerifier
{
private const int _driftTolerance = 0;

public override (AttestationType, X509Certificate2[]) Verify()
public override (AttestationType, X509Certificate2[]) Verify(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)

// 2. Verify that response is a valid SafetyNet response of version ver
if (!TryGetVer(out string? ver))
if (!request.TryGetVer(out string? ver))
{
throw new Fido2VerificationException("Invalid version in SafetyNet data");
}

if (!(_attStmt["response"] is CborByteString { Length: > 0 }))
if (!(request.AttStmt["response"] is CborByteString { Length: > 0 }))
throw new Fido2VerificationException("Invalid response in SafetyNet data");

var response = (byte[])_attStmt["response"]!;
var response = (byte[])request.AttStmt["response"]!;
var responseJWT = Encoding.UTF8.GetString(response);

if (string.IsNullOrWhiteSpace(responseJWT))
Expand Down Expand Up @@ -153,7 +153,7 @@ public override (AttestationType, X509Certificate2[]) Verify()
}

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

if (!dataHash.SequenceEqual(nonceHash))
{
Expand Down
11 changes: 6 additions & 5 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 Fido2NetLib.Cbor;
using Fido2NetLib.Exceptions;
using Fido2NetLib.Objects;
Expand Down Expand Up @@ -39,10 +40,10 @@ public static byte[] GetAppleAttestationExtensionValue(X509ExtensionCollection e
}
}

public override (AttestationType, X509Certificate2[]) Verify()
public override (AttestationType, X509Certificate2[]) Verify(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 (!(X5c is CborArray { Length: >= 2 } x5cArray && x5cArray[0] is CborByteString { Length: > 0 }))
if (!(request.X5c is CborArray { Length: >= 2 } x5cArray && x5cArray[0] is CborByteString { Length: > 0 }))
{
throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, Fido2ErrorMessages.MalformedX5c_AppleAttestation);
}
Expand All @@ -61,7 +62,7 @@ public override (AttestationType, X509Certificate2[]) Verify()
var credCert = trustPath[0];

// 3. Concatenate authenticatorData and clientDataHash to form nonceToHash.
ReadOnlySpan<byte> nonceToHash = Data;
ReadOnlySpan<byte> nonceToHash = request.Data;

// 4. Perform SHA-256 hash of nonceToHash to produce nonce.
Span<byte> nonce = stackalloc byte[32];
Expand All @@ -75,13 +76,13 @@ public override (AttestationType, X509Certificate2[]) Verify()

// 6. Verify credential public key matches the Subject Public Key of credCert.
// First, obtain COSE algorithm being used from credential public key
var coseAlg = (COSE.Algorithm)(int)CredentialPublicKey[COSE.KeyCommonParameter.Alg];
var coseAlg = (COSE.Algorithm)(int)request.CredentialPublicKey[COSE.KeyCommonParameter.Alg];

// Next, build temporary CredentialPublicKey for comparison from credCert and COSE algorithm
var cpk = new CredentialPublicKey(credCert, coseAlg);

// Finally, compare byte sequence of CredentialPublicKey built from credCert with byte sequence of CredentialPublicKey from AttestedCredentialData from authData
if (!cpk.GetBytes().AsSpan().SequenceEqual(AuthData.AttestedCredentialData!.CredentialPublicKey.GetBytes()))
if (!cpk.GetBytes().AsSpan().SequenceEqual(request.AuthData.AttestedCredentialData!.CredentialPublicKey.GetBytes()))
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.
Expand Down
21 changes: 11 additions & 10 deletions Src/Fido2/AttestationFormat/AppleAppAttest.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 Fido2NetLib.Cbor;
using Fido2NetLib.Objects;

Expand Down Expand Up @@ -46,10 +47,10 @@ 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()
public override (AttestationType, X509Certificate2[]) Verify(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 (!(X5c is CborArray { Length: 2 } x5cArray && x5cArray[0] is CborByteString { Length: > 0 } && x5cArray[1] is CborByteString { Length: > 0 }))
if (!(request.X5c is CborArray { Length: 2 } x5cArray && x5cArray[0] is CborByteString { Length: > 0 } && x5cArray[1] is CborByteString { Length: > 0 }))
{
throw new Fido2VerificationException("Malformed x5c in Apple AppAttest attestation");
}
Expand All @@ -64,7 +65,7 @@ public override (AttestationType, X509Certificate2[]) Verify()
chain.ChainPolicy.ExtraStore.Add(intermediateCert);

X509Certificate2 credCert = new((byte[])x5cArray[0]);
if (AuthData.AttestedCredentialData!.AaGuid.Equals(devAaguid))
if (request.AuthData.AttestedCredentialData!.AaGuid.Equals(devAaguid))
{
// Allow expired leaf cert in development environment
chain.ChainPolicy.VerificationTime = credCert.NotBefore.AddSeconds(1);
Expand All @@ -79,13 +80,13 @@ public override (AttestationType, X509Certificate2[]) Verify()
// 3. Generate a new SHA256 hash of the composite item to create nonce.
// 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
Apple apple = new();
(var attType, var trustPath) = apple.Verify(_attStmt, _authenticatorData, _clientDataHash);
var apple = new Apple();
(var attType, var trustPath) = apple.Verify(request);

// 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);
var keyIdentifier = Convert.FromHexString(credCert.GetNameInfo(X509NameType.SimpleName, false));
ReadOnlySpan<byte> keyIdentifier = Convert.FromHexString(credCert.GetNameInfo(X509NameType.SimpleName, false));
if (!credCertPKHash.SequenceEqual(keyIdentifier))
{
throw new Fido2VerificationException("Public key hash does not match key identifier in Apple AppAttest attestation");
Expand All @@ -95,25 +96,25 @@ public override (AttestationType, X509Certificate2[]) Verify()
var appId = GetAppleAppIdFromCredCertExtValue(credCert.Extensions);
Span<byte> appIdHash = stackalloc byte[32];
SHA256.HashData(appId, appIdHash);
if (!appIdHash.SequenceEqual(AuthData.RpIdHash))
if (!appIdHash.SequenceEqual(request.AuthData.RpIdHash))
{
throw new Fido2VerificationException("App ID hash does not match RP ID hash in Apple AppAttest attestation");
}

// 7. Verify that the authenticator data's counter field equals 0.
if (AuthData.SignCount != 0)
if (request.AuthData.SignCount != 0)
{
throw new Fido2VerificationException("Sign count does not equal 0 in Apple AppAttest attestation");
}

// 8. Verify that the authenticator data's aaguid field is either appattestdevelop if operating in the development environment, or appattest followed by seven 0x00 bytes if operating in the production environment.
if (!AuthData.AttestedCredentialData.AaGuid.Equals(devAaguid) && !AuthData.AttestedCredentialData.AaGuid.Equals(prodAaguid))
if (!request.AuthData.AttestedCredentialData.AaGuid.Equals(devAaguid) && !request.AuthData.AttestedCredentialData.AaGuid.Equals(prodAaguid))
{
throw new Fido2VerificationException("Invalid aaguid encountered in Apple AppAttest attestation");
}

// 9. Verify that the authenticator data's credentialId field is the same as the key identifier.
if (!keyIdentifier.SequenceEqual(AuthData.AttestedCredentialData.CredentialID))
if (!keyIdentifier.SequenceEqual(request.AuthData.AttestedCredentialData.CredentialID))
{
throw new Fido2VerificationException("Mismatch between credentialId and keyIdentifier in Apple AppAttest attestation");
}
Expand Down
Loading

0 comments on commit 86b6c6d

Please sign in to comment.