|
| 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 | +} |
0 commit comments