Skip to content

Commit

Permalink
Add support for apple verification format (#65)
Browse files Browse the repository at this point in the history
While this appears to no longer be in active use, Apple devices/software
from before Passkeys most likely use this format. Someone was kind
enough to provide some test vectors to the W3C repo that included this,
so adding preliminary support was possible.
  • Loading branch information
Firehed authored Dec 10, 2023
1 parent 1e07281 commit e263a35
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 2 deletions.
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -683,13 +683,13 @@ Due to an inability to generate responses with all formats, not all are supporte

| Format | Supported | Spec | Notes |
| --- | --- | --- | --- |
| `packed` | ✅⚠️ | 8.2 | Parses fine, attestation trust path support is limited. |
| `packed` | ✅⚠️[^limited-trust-path] | 8.2 | |
| `tpm` | ❌ | 8.3 | |
| `android-key` | ❌ | 8.4 | |
| `android-safetynet` | ❌ | 8.5 | |
| `fido-u2f` | ✅ | 8.6 | YubiKeys and similar U2F stateless devices. |
| `none` | ✅ | 8.7 | Used by Apple in Safari when using Passkeys (even when direct attestation is requested) |
| `apple` | | 8.8 | Apple does not appear to use this format, instead providing non-attested credentials. |
| `apple` | ✅⚠️ [^ext-vec], [^limited-trust-path] | 8.8 | Apple no longer appears to use this format, instead providing non-attested credentials (fmt=none). |
| `compound` | ❌ | 8.9 | This format only appears in the editor's draft of the spec and is not yet on the official registry. |

If you receive an `UnhandledMatchError` from the library pertaining to a format, please file an issue.
Expand All @@ -716,3 +716,6 @@ General quickstart guide:

Intro to passkeys:
- https://developer.apple.com/videos/play/wwdc2021/10106/

[^ext-vec]: Support is based on [unofficial test vectors](https://github.com/w3c/webauthn/issues/1633).
[^limited-trust-path]: Handling of attestation trust path is limited.
109 changes: 109 additions & 0 deletions src/Attestations/Apple.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php

declare(strict_types=1);

namespace Firehed\WebAuthn\Attestations;

use Exception;
use Firehed\CBOR\Decoder;
use Firehed\WebAuthn\AuthenticatorData;
use Firehed\WebAuthn\BinaryString;
use Firehed\WebAuthn\Certificate;
use Firehed\WebAuthn\PublicKey\EllipticCurve;
use OpenSSLCertificate;

class Apple implements AttestationStatementInterface
{
private const OID = '1.2.840.113635.100.8.2';

/**
* @param array{
* x5c: string[],
* } $data
*/
public function __construct(
private array $data,
) {
}

// 8.8
public function verify(AuthenticatorData $data, BinaryString $clientDataHash): VerificationResult
{
// ¶2
$nonceToHash = new BinaryString(
$data->getRaw()->unwrap() . $clientDataHash->unwrap()
);
// ¶3
$nonce = hash('sha256', $nonceToHash->unwrap(), binary: true);

// ¶4 - there's a whole lot of validation and data extraction that the
// spec glosses over.
$certs = array_map(self::parseDer(...), $this->data['x5c']);
assert(count($certs) >= 1);
$credCert = array_shift($certs);
$info = openssl_x509_parse($credCert);
if ($info === false) {
throw new Exception('Invalid certificate');
}
if (!array_key_exists('extensions', $info)) {
throw new Exception('No extensions in credential cert');
}
$certExt = $info['extensions'];
if (!array_key_exists(self::OID, $certExt)) {
throw new Exception('Expected OID not present in cert extensions');
}
$nonceInCert = $certExt[self::OID];
// This isn't clear in the spec, but the value arrives ASN.1-encoded.
// Manually verify some length assumptions instead of doing some
// full-on parsing.
if (strlen($nonceInCert) !== 38) {
throw new Exception("Malformed nonce in cert");
}
// 0x3024 SEQUENCE (constructed) legnth 36
// 0xA122 Element 1, length 34
// 0x0420 OCTET STRING length 32
if (!str_starts_with(needle: "\x30\x24\xA1\x22\x04\x20", haystack: $nonceInCert)) {
throw new Exception("Cert nonce has weird encoding");
}
// (finally, the actual verification procedure)
$decodedNonceInCert = substr($nonceInCert, 6);
if (!hash_equals($nonce, $decodedNonceInCert)) {
throw new Exception('Nonce mismatch of expected value');
}

// ¶5
$pubKey = openssl_pkey_get_public($credCert);
if ($pubKey === false) {
throw new Exception('Could not read pubkey of certificate');
}
$pkd = openssl_pkey_get_details($pubKey);
if ($pkd === false) {
throw new Exception('Could not extract public key info');
}
$credPK = $data->getAttestedCredentialData()->coseKey;
$credPKPem = $credPK->getPublicKey()->getPemFormatted();
// ¶6
if (trim($pkd['key']) !== trim($credPKPem)) {
throw new Exception('Credential public key does not match cert subject');
}
return new VerificationResult(
AttestationType::AnonymizationCA,
// trustPath: rest of x5c
);
}

private static function parseDer(string $certificate): OpenSSLCertificate
{
// Convert DER to PEM format
$certificate = "-----BEGIN CERTIFICATE-----\n"
. chunk_split(base64_encode($certificate), 64, "\n")
. "-----END CERTIFICATE-----";

// Read and parse the certificate
$certResource = openssl_x509_read($certificate);
if ($certResource === false) {
throw new Exception('Certiticate parsing error');
}
return $certResource;
}
}
1 change: 1 addition & 0 deletions src/Attestations/AttestationObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public function __construct(
assert(array_key_exists('authData', $decoded));

$stmt = match (Format::tryFrom($decoded['fmt'])) { // @phpstan-ignore-line
Format::Apple => new Apple($decoded['attStmt']),
Format::None => new None($decoded['attStmt']),
Format::Packed => new Packed($decoded['attStmt']),
Format::U2F => new FidoU2F($decoded['attStmt']),
Expand Down
1 change: 1 addition & 0 deletions tests/IntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* Performs general integration testing with known-good data covering various
* formats, attestation requirements, etc.
*
* @covers \Firehed\WebAuthn\Attestations\Apple
* @covers \Firehed\WebAuthn\Attestations\FidoU2F
* @covers \Firehed\WebAuthn\Attestations\None
* @covers \Firehed\WebAuthn\Attestations\Packed
Expand Down
4 changes: 4 additions & 0 deletions tests/integration/1633-ios/metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"id": "f25BX23OovR-Ti84WI6lQcp_qfc",
"origin": "https://mobilepki.org"
}
5 changes: 5 additions & 0 deletions tests/integration/1633-ios/reg-req.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"publicKey": {
"challenge": "1Z4m-O7Lmi762WffOu5jSXSnJfF93HHwWd52BhNhrUQ"
}
}
11 changes: 11 additions & 0 deletions tests/integration/1633-ios/reg-res.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"id": "f25BX23OovR-Ti84WI6lQcp_qfc",
"rawId": "f25BX23OovR-Ti84WI6lQcp_qfc",
"response": {
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiMVo0bS1PN0xtaTc2MldmZk91NWpTWFNuSmZGOTNISHdXZDUyQmhOaHJVUSIsIm9yaWdpbiI6Imh0dHBzOi8vbW9iaWxlcGtpLm9yZyJ9",
"attestationObject": "o2NmbXRlYXBwbGVnYXR0U3RtdKJjYWxnJmN4NWOCWQJGMIICQjCCAcmgAwIBAgIGAXmASIscMAoGCCqGSM49BAMCMEgxHDAaBgNVBAMME0FwcGxlIFdlYkF1dGhuIENBIDExEzARBgNVBAoMCkFwcGxlIEluYy4xEzARBgNVBAgMCkNhbGlmb3JuaWEwHhcNMjEwNTE3MTYyMTQ4WhcNMjEwNTIwMTYyMTQ4WjCBkTFJMEcGA1UEAwxAYzUxZGUwY2QwNjBkMTlmZGU5NWFlNmI1YWNlYjRmNGQ0NjA5YTUzYzBiNjJjMGQ2N2JiYWU1NTg5OGQ0NDIxMTEaMBgGA1UECwwRQUFBIENlcnRpZmljYXRpb24xEzARBgNVBAoMCkFwcGxlIEluYy4xEzARBgNVBAgMCkNhbGlmb3JuaWEwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASsbmkJXOuTCECtxpUV8hIpXddsOUFDlV6ecQqxOBdrY8-MjNLS0p5nz8v5bGI3kXR6C1FeJzzXx7nU7cOfprQ_o1UwUzAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB_wQEAwIE8DAzBgkqhkiG92NkCAIEJjAkoSIEIHcxH4L2XBi_OttASQZ8wqaTnb6PnGlh5mOwNfQGktYqMAoGCCqGSM49BAMCA2cAMGQCMHNr9RnVEWfKNtrG5ovjBic8WSEQE2T-CWmZ3nSO740N41kkQ6q5kx3iW-qThQ4_nwIwAyM4XCTtmH-AB_UmqN2AbPqvb0N57vVyQpey7Yxj9fKIJ6ALDOux0T1c9F-HteYrWQI4MIICNDCCAbqgAwIBAgIQViVTlcen-0Dr4ijYJghTtjAKBggqhkjOPQQDAzBLMR8wHQYDVQQDDBZBcHBsZSBXZWJBdXRobiBSb290IENBMRMwEQYDVQQKDApBcHBsZSBJbmMuMRMwEQYDVQQIDApDYWxpZm9ybmlhMB4XDTIwMDMxODE4MzgwMVoXDTMwMDMxMzAwMDAwMFowSDEcMBoGA1UEAwwTQXBwbGUgV2ViQXV0aG4gQ0EgMTETMBEGA1UECgwKQXBwbGUgSW5jLjETMBEGA1UECAwKQ2FsaWZvcm5pYTB2MBAGByqGSM49AgEGBSuBBAAiA2IABIMuhy8mFJGBAiW59fzWu2N4tfVfP8sEW8c1mTR1_VSQRN-b_hkhF2XGmh3aBQs41FCDQBpDT7JNES1Ww-HPv8uYkf7AaWCBvvlsvHfIjd2vRqWu4d1RW1r6q5O-nAsmkaNmMGQwEgYDVR0TAQH_BAgwBgEB_wIBADAfBgNVHSMEGDAWgBQm12TZxXjCWmfRp95rEtAbY_HG1zAdBgNVHQ4EFgQU666CxP-hrFtR1M8kYQUAvmO9d4gwDgYDVR0PAQH_BAQDAgEGMAoGCCqGSM49BAMDA2gAMGUCMQDdixo0gaX62du052V7hB4UTCe3W4dqQYbCsUdXUDNyJ-_lVEV-9kiVDGMuXEg-cMECMCyKYETcIB_P5ZvDTSkwwUh4Udlg7Wp18etKyr44zSW4l9DIBb7wx_eLB6VxxugOB2hhdXRoRGF0YViYKSqtX-Wo3JpWQpsrCGT2kSTRHZYWuoNy4MTSFTN75b1FAAAAAAAAAAAAAAAAAAAAAAAAAAAAFH9uQV9tzqL0fk4vOFiOpUHKf6n3pQECAyYgASFYIKxuaQlc65MIQK3GlRXyEild12w5QUOVXp5xCrE4F2tjIlggz4yM0tLSnmfPy_lsYjeRdHoLUV4nPNfHudTtw5-mtD8",
"transports": []
},
"authenticatorAttachment": "",
"type": "public-key"
}

0 comments on commit e263a35

Please sign in to comment.