Skip to content

Commit 3d823eb

Browse files
authored
Attestation refactor, implement Apple attestation format verifier (#205)
* WIP to make attestation work more like WebAuthn spec, reduce different ways of handling trust path * Initial implementation for Apple anonymous attestation * Additional Apple attestation work
1 parent acda23d commit 3d823eb

File tree

17 files changed

+723
-398
lines changed

17 files changed

+723
-398
lines changed

Src/Fido2/AttestationFormat/AndroidKey.cs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,10 @@
66
using Fido2NetLib.Objects;
77
using PeterO.Cbor;
88

9-
namespace Fido2NetLib.AttestationFormat
9+
namespace Fido2NetLib
1010
{
11-
internal class AndroidKey : AttestationFormat
11+
internal class AndroidKey : AttestationVerifier
1212
{
13-
public AndroidKey(CBORObject attStmt, byte[] authenticatorData, byte[] clientDataHash) : base(attStmt, authenticatorData, clientDataHash)
14-
{
15-
}
16-
1713
public static byte[] AttestationExtensionBytes(X509ExtensionCollection exts)
1814
{
1915
foreach (var ext in exts)
@@ -139,7 +135,7 @@ public static bool IsPurposeSign(byte[] attExtBytes)
139135
return (2 == softwareEnforcedPurposeValue && 2 == teeEnforcedPurposeValue);
140136
}
141137

142-
public override void Verify()
138+
public override (AttestationType, X509Certificate2[]) Verify()
143139
{
144140
// 1. Verify that attStmt is valid CBOR conforming to the syntax defined above and perform CBOR decoding on it to extract the contained fields
145141
// (handled in base class)
@@ -219,6 +215,12 @@ public override void Verify()
219215
// 5bii. The value in the AuthorizationList.purpose field is equal to KM_PURPOSE_SIGN (which == 2).
220216
if (false == IsPurposeSign(attExtBytes))
221217
throw new Fido2VerificationException("Found purpose field not set to KM_PURPOSE_SIGN in android key attestation certificate extension");
218+
219+
var trustPath = X5c.Values
220+
.Select(x => new X509Certificate2(x.GetByteString()))
221+
.ToArray();
222+
223+
return (AttestationType.Basic, trustPath);
222224
}
223225
}
224226
}

Src/Fido2/AttestationFormat/AndroidSafetyNet.cs

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,17 @@
66
using System.Security.Cryptography;
77
using System.Security.Cryptography.X509Certificates;
88
using System.Text;
9+
using Fido2NetLib.Objects;
910
using Microsoft.IdentityModel.Tokens;
1011
using Newtonsoft.Json.Linq;
1112
using PeterO.Cbor;
1213

13-
namespace Fido2NetLib.AttestationFormat
14+
namespace Fido2NetLib
1415
{
15-
internal class AndroidSafetyNet : AttestationFormat
16+
internal class AndroidSafetyNet : AttestationVerifier
1617
{
1718
private readonly int _driftTolerance;
1819

19-
public AndroidSafetyNet(CBORObject attStmt, byte[] authenticatorData, byte[] clientDataHash, int driftTolerance)
20-
: base(attStmt, authenticatorData, clientDataHash)
21-
{
22-
_driftTolerance = driftTolerance;
23-
}
24-
2520
private X509Certificate2 GetX509Certificate(string certString)
2621
{
2722
try
@@ -35,7 +30,7 @@ private X509Certificate2 GetX509Certificate(string certString)
3530
}
3631
}
3732

38-
public override void Verify()
33+
public override (AttestationType, X509Certificate2[]) Verify()
3934
{
4035
// 1. Verify that attStmt is valid CBOR conforming to the syntax defined above and perform
4136
// CBOR decoding on it to extract the contained fields
@@ -169,7 +164,8 @@ public override void Verify()
169164
}
170165

171166
// 4. Let attestationCert be the attestation certificate
172-
var subject = certs[0].GetNameInfo(X509NameType.DnsName, false);
167+
var attestationCert = certs[0];
168+
var subject = attestationCert.GetNameInfo(X509NameType.DnsName, false);
173169

174170
// 5. Verify that the attestation certificate is issued to the hostname "attest.android.com"
175171
if (false == ("attest.android.com").Equals(subject))
@@ -181,6 +177,8 @@ public override void Verify()
181177

182178
if (true != ctsProfileMatch)
183179
throw new Fido2VerificationException("SafetyNet response ctsProfileMatch false");
180+
181+
return (AttestationType.Basic, new X509Certificate2[] { attestationCert });
184182
}
185183
}
186184
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
using System;
2+
using System.Linq;
3+
using System.Security.Cryptography;
4+
using System.Security.Cryptography.X509Certificates;
5+
using Asn1;
6+
using Fido2NetLib.Objects;
7+
using PeterO.Cbor;
8+
9+
namespace Fido2NetLib
10+
{
11+
internal class Apple : AttestationVerifier
12+
{
13+
public static byte[] GetAppleAttestationExtensionValue(X509ExtensionCollection exts)
14+
{
15+
var appleExtension = exts.Cast<X509Extension>().FirstOrDefault(e => e.Oid.Value == "1.2.840.113635.100.8.2");
16+
17+
if (appleExtension == null || appleExtension.RawData == null || appleExtension.RawData.Length < 0x26)
18+
throw new Fido2VerificationException("Extension with OID 1.2.840.113635.100.8.2 not found on Apple attestation credCert");
19+
20+
try
21+
{
22+
var appleAttestationASN = AsnElt.Decode(appleExtension.RawData);
23+
appleAttestationASN.CheckConstructed();
24+
appleAttestationASN.CheckTag(AsnElt.SEQUENCE);
25+
appleAttestationASN.CheckNumSub(1);
26+
27+
var sequence = appleAttestationASN.GetSub(0);
28+
sequence.CheckConstructed();
29+
sequence.CheckNumSub(1);
30+
31+
var context = sequence.GetSub(0);
32+
context.CheckPrimitive();
33+
context.CheckTag(AsnElt.OCTET_STRING);
34+
35+
return context.GetOctetString();
36+
}
37+
38+
catch (Exception ex)
39+
{
40+
throw new Fido2VerificationException("Apple attestation extension has invalid data", ex);
41+
}
42+
}
43+
44+
public override (AttestationType, X509Certificate2[]) Verify()
45+
{
46+
// 1. Verify that attStmt is valid CBOR conforming to the syntax defined above and perform CBOR decoding on it to extract the contained fields.
47+
if (null == X5c || CBORType.Array != X5c.Type || X5c.Count < 2 ||
48+
null == X5c.Values || 0 == X5c.Values.Count ||
49+
CBORType.ByteString != X5c.Values.First().Type ||
50+
0 == X5c.Values.First().GetByteString().Length)
51+
throw new Fido2VerificationException("Malformed x5c in Apple attestation");
52+
53+
// 2. Verify x5c is a valid certificate chain starting from the credCert to the Apple WebAuthn root certificate.
54+
// TODO: Pull this in instead of hard coding?
55+
// https://www.apple.com/certificateauthority/Apple_WebAuthn_Root_CA.pem
56+
var appleWebAuthnRoots = new string[] {
57+
"MIICEjCCAZmgAwIBAgIQaB0BbHo84wIlpQGUKEdXcTAKBggqhkjOPQQDAzBLMR8w" +
58+
"HQYDVQQDDBZBcHBsZSBXZWJBdXRobiBSb290IENBMRMwEQYDVQQKDApBcHBsZSBJ" +
59+
"bmMuMRMwEQYDVQQIDApDYWxpZm9ybmlhMB4XDTIwMDMxODE4MjEzMloXDTQ1MDMx" +
60+
"NTAwMDAwMFowSzEfMB0GA1UEAwwWQXBwbGUgV2ViQXV0aG4gUm9vdCBDQTETMBEG" +
61+
"A1UECgwKQXBwbGUgSW5jLjETMBEGA1UECAwKQ2FsaWZvcm5pYTB2MBAGByqGSM49" +
62+
"AgEGBSuBBAAiA2IABCJCQ2pTVhzjl4Wo6IhHtMSAzO2cv+H9DQKev3//fG59G11k" +
63+
"xu9eI0/7o6V5uShBpe1u6l6mS19S1FEh6yGljnZAJ+2GNP1mi/YK2kSXIuTHjxA/" +
64+
"pcoRf7XkOtO4o1qlcaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUJtdk" +
65+
"2cV4wlpn0afeaxLQG2PxxtcwDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cA" +
66+
"MGQCMFrZ+9DsJ1PW9hfNdBywZDsWDbWFp28it1d/5w2RPkRX3Bbn/UbDTNLx7Jr3" +
67+
"jAGGiQIwHFj+dJZYUJR786osByBelJYsVZd2GbHQu209b5RCmGQ21gpSAk9QZW4B" +
68+
"1bWeT0vT"};
69+
70+
var trustPath = X5c.Values
71+
.Select(x => new X509Certificate2(x.GetByteString()))
72+
.ToArray();
73+
74+
var appleWebAuthnRootCerts = appleWebAuthnRoots
75+
.Select(x => new X509Certificate2(Convert.FromBase64String(x)))
76+
.ToArray();
77+
78+
if (!CryptoUtils.ValidateTrustChain(trustPath, appleWebAuthnRootCerts))
79+
throw new Fido2VerificationException("Invalid certificate chain in Apple attestation");
80+
81+
// credCert is the first certificate in the trust path
82+
var credCert = trustPath[0];
83+
84+
// 3. Concatenate authenticatorData and clientDataHash to form nonceToHash.
85+
var nonceToHash = Data;
86+
87+
// 4. Perform SHA-256 hash of nonceToHash to produce nonce.
88+
var nonce = CryptoUtils.GetHasher(HashAlgorithmName.SHA256).ComputeHash(nonceToHash);
89+
90+
// 5. Verify nonce matches the value of the extension with OID ( 1.2.840.113635.100.8.2 ) in credCert.
91+
var appleExtensionBytes = GetAppleAttestationExtensionValue(credCert.Extensions);
92+
93+
if (!nonce.SequenceEqual(appleExtensionBytes))
94+
throw new Fido2VerificationException("Mismatch between nonce and credCert attestation extension in Apple attestation");
95+
96+
// 6. Verify credential public key matches the Subject Public Key of credCert.
97+
// First, obtain COSE algorithm being used from credential public key
98+
var coseAlg = CredentialPublicKey[CBORObject.FromObject(COSE.KeyCommonParameter.Alg)].AsInt32();
99+
100+
// Next, build temporary CredentialPublicKey for comparison from credCert and COSE algorithm
101+
var cpk = new CredentialPublicKey(credCert, coseAlg);
102+
103+
// Finally, compare byte sequence of CredentialPublicKey built from credCert with byte sequence of CredentialPublicKey from AttestedCredentialData from authData
104+
if (!cpk.GetBytes().SequenceEqual(AuthData.AttestedCredentialData.CredentialPublicKey.GetBytes()))
105+
throw new Fido2VerificationException("Credential public key in Apple attestation does not match subject public key of credCert");
106+
107+
// 7. If successful, return implementation-specific values representing attestation type Anonymous CA and attestation trust path x5c.
108+
return (AttestationType.Basic, trustPath);
109+
}
110+
}
111+
}

Src/Fido2/AttestationFormat/AttestationFormat.cs

Lines changed: 8 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,17 @@
11
using PeterO.Cbor;
22
using System;
33
using System.Security.Cryptography.X509Certificates;
4-
using System.Linq;
54
using Fido2NetLib.Objects;
65
using Asn1;
76

8-
namespace Fido2NetLib.AttestationFormat
7+
namespace Fido2NetLib
98
{
10-
public abstract class AttestationFormat
9+
public abstract class AttestationVerifier
1110
{
1211
public CBORObject attStmt;
1312
public byte[] authenticatorData;
1413
public byte[] clientDataHash;
1514

16-
public AttestationFormat(CBORObject attStmt, byte[] authenticatorData, byte[] clientDataHash)
17-
{
18-
this.attStmt = attStmt;
19-
this.authenticatorData = authenticatorData;
20-
this.clientDataHash = clientDataHash;
21-
}
22-
2315
internal CBORObject Sig => attStmt["sig"];
2416
internal CBORObject X5c => attStmt["x5c"];
2517
internal CBORObject Alg => attStmt["alg"];
@@ -81,34 +73,14 @@ internal static int U2FTransportsFromAttnCert(X509ExtensionCollection exts)
8173
}
8274
return u2ftransports;
8375
}
84-
85-
internal bool ValidateTrustChain(X509Certificate2[] trustPath, X509Certificate2[] attestationRootCertificates)
76+
public virtual (AttestationType, X509Certificate2[]) Verify(CBORObject attStmt, byte[] authenticatorData, byte[] clientDataHash)
8677
{
87-
foreach (var attestationRootCert in attestationRootCertificates)
88-
{
89-
var chain = new X509Chain();
90-
chain.ChainPolicy.ExtraStore.Add(attestationRootCert);
91-
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
92-
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
93-
if (trustPath.Length > 1)
94-
{
95-
foreach (var cert in trustPath.Skip(1).Reverse())
96-
{
97-
chain.ChainPolicy.ExtraStore.Add(cert);
98-
}
99-
}
100-
var valid = chain.Build(trustPath[0]);
101-
102-
// because we are using AllowUnknownCertificateAuthority we have to verify that the root matches ourselves
103-
var chainRoot = chain.ChainElements[chain.ChainElements.Count - 1].Certificate;
104-
valid = valid && chainRoot.RawData.SequenceEqual(attestationRootCert.RawData);
105-
106-
if (true == valid)
107-
return true;
108-
}
109-
return false;
78+
this.attStmt = attStmt;
79+
this.authenticatorData = authenticatorData;
80+
this.clientDataHash = clientDataHash;
81+
return Verify();
11082
}
11183

112-
public abstract void Verify();
84+
public abstract (AttestationType, X509Certificate2[]) Verify();
11385
}
11486
}

Src/Fido2/AttestationFormat/FidoU2f.cs

Lines changed: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,13 @@
66
using Fido2NetLib.Objects;
77
using PeterO.Cbor;
88

9-
namespace Fido2NetLib.AttestationFormat
9+
namespace Fido2NetLib
1010
{
11-
internal class FidoU2f : AttestationFormat
11+
internal class FidoU2f : AttestationVerifier
1212
{
1313
private readonly IMetadataService _metadataService;
1414

15-
public FidoU2f(CBORObject attStmt, byte[] authenticatorData, byte[] clientDataHash, IMetadataService metadataService) : base(attStmt, authenticatorData, clientDataHash)
16-
{
17-
_metadataService = metadataService;
18-
}
19-
public override void Verify()
15+
public override (AttestationType, X509Certificate2[]) Verify()
2016
{
2117
// verify that aaguid is 16 empty bytes (note: required by fido2 conformance testing, could not find this in spec?)
2218
if (0 != AuthData.AttestedCredentialData.AaGuid.CompareTo(Guid.Empty))
@@ -101,25 +97,7 @@ public override void Verify()
10197
.Select(x => new X509Certificate2(x.GetByteString()))
10298
.ToArray();
10399

104-
var aaguid = AaguidFromAttnCertExts(attCert.Extensions);
105-
106-
if (null != _metadataService && null != aaguid)
107-
{
108-
var guidAaguid = AttestedCredentialData.FromBigEndian(aaguid);
109-
var entry = _metadataService.GetEntry(guidAaguid);
110-
111-
if (null != entry && null != entry.MetadataStatement)
112-
{
113-
var attestationRootCertificates = entry.MetadataStatement.AttestationRootCertificates
114-
.Select(x => new X509Certificate2(Convert.FromBase64String(x)))
115-
.ToArray();
116-
117-
if (false == ValidateTrustChain(trustPath, attestationRootCertificates))
118-
{
119-
throw new Fido2VerificationException("Invalid certificate chain in U2F attestation");
120-
}
121-
}
122-
}
100+
return (AttestationType.AttCa, trustPath);
123101
}
124102
}
125103
}
Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
1-
using PeterO.Cbor;
1+
using System.Security.Cryptography.X509Certificates;
2+
using Fido2NetLib.Objects;
3+
using PeterO.Cbor;
24

3-
namespace Fido2NetLib.AttestationFormat
5+
namespace Fido2NetLib
46
{
5-
internal class None : AttestationFormat
7+
public class None : AttestationVerifier
68
{
7-
public None(CBORObject attStmt, byte[] authenticatorData, byte[] clientDataHash)
8-
: base(attStmt, authenticatorData, clientDataHash)
9-
{
10-
}
11-
12-
public override void Verify()
9+
public override (AttestationType, X509Certificate2[]) Verify()
1310
{
1411
if (0 != attStmt.Keys.Count && 0 != attStmt.Values.Count)
1512
throw new Fido2VerificationException("Attestation format none should have no attestation statement");
13+
14+
return (AttestationType.None, null);
1615
}
1716
}
1817
}

0 commit comments

Comments
 (0)