Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
9 changes: 7 additions & 2 deletions crates/attestation/src/apple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const ACL: &[u8] = &[
0x63, 0x30, 0x05, 0xa6, 0x03, 0x02, 0x01, 0x01,
];
const PRODUCTION_AAGUID: &[u8; 16] = b"appattest\0\0\0\0\0\0\0";
const AUTH_DATA_FLAGS: u8 = 0xc0;
const APP_ATTEST_ROOT_CA_BASE64: &str = "MIICITCCAaegAwIBAgIQC/O+DvHN0uD7jG5yH2IXmDAKBggqhkjOPQQDAzBSMSYwJAYDVQQDDB1BcHBsZSBBcHAgQXR0ZXN0YXRpb24gUm9vdCBDQTETMBEGA1UECgwKQXBwbGUgSW5jLjETMBEGA1UECAwKQ2FsaWZvcm5pYTAeFw0yMDAzMTgxODMyNTNaFw00NTAzMTUwMDAwMDBaMFIxJjAkBgNVBAMMHUFwcGxlIEFwcCBBdHRlc3RhdGlvbiBSb290IENBMRMwEQYDVQQKDApBcHBsZSBJbmMuMRMwEQYDVQQIDApDYWxpZm9ybmlhMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAERTHhmLW07ATaFQIEVwTtT4dyctdhNbJhFs/Ii2FdCgAHGbpphY3+d8qjuDngIN3WVhQUBHAoMeQ/cLiP1sOUtgjqK9auYen1mMEvRq9Sk3Jm5X8U62H+xTD3FE9TgS41o0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSskRBTM72+aEH/pwyp5frq5eWKoTAOBgNVHQ8BAf8EBAMCAQYwCgYIKoZIzj0EAwMDaAAwZQIwQgFGnByvsiVbpTKwSga0kP0e8EeDS4+sQmTvb7vn53O5+FRXgeLhpJ06ysC5PrOyAjEAp5U4xDgEgllF7En3VcE3iexZZtKeYnpqtijVoyFraWVIyd/dganmrduC1bmTBGwD";

/// Apple's pinned App Attestation Root CA in DER form.
Expand Down Expand Up @@ -316,7 +317,11 @@ fn attestation_auth_data(
data: &[u8],
rp_id_hash: [u8; 32],
) -> Result<AttestationAuth<'_>, AttestationError> {
if data.len() < 55 || data[..32] != rp_id_hash || data[32] != 0x40 || data[33..37] != [0; 4] {
if data.len() < 55
|| data[..32] != rp_id_hash
|| data[32] != AUTH_DATA_FLAGS
|| data[33..37] != [0; 4]
{
return Err(AttestationError::Credential);
}
if data[37..53] != *PRODUCTION_AAGUID {
Expand All @@ -339,7 +344,7 @@ fn attestation_auth_data(
}

fn assertion_auth_data(data: &[u8]) -> Result<AppleClaims, AttestationError> {
if data.len() < 37 || data[32] != 0x40 {
if data.len() < 37 || data[32] != AUTH_DATA_FLAGS {
return Err(AttestationError::Binding);
}
let cd_hash = apple_cd_hash(&data[37..], "Apple authenticator extensions")?;
Expand Down
82 changes: 69 additions & 13 deletions crates/attestation/tests/apple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ use serde_bytes::ByteBuf;
use sha2::{Digest as _, Sha256};

const APP_ID: &str = "2F53L9ZR3N.ai.hellas.app-attest-spike";
const FIXTURE_VALIDATION_TIME: u64 = 1_784_384_387;
const FIXTURE_VALIDATION_TIME: u64 = 1_790_996_283;
const REAL_CD_HASH: [u8; 32] = [
0xcb, 0x92, 0xa8, 0x90, 0x91, 0x54, 0x57, 0xb2, 0x26, 0xfe, 0xa9, 0xbe, 0x77, 0x8f, 0xc6, 0xa2,
0x99, 0x94, 0xa2, 0xc6, 0x0d, 0xfa, 0xa6, 0x03, 0x71, 0xe9, 0xe2, 0x64, 0xd8, 0x54, 0x13, 0x06,
0x36, 0x31, 0x40, 0xc0, 0x91, 0xe0, 0xfa, 0x85, 0xfe, 0xee, 0xb4, 0xce, 0x51, 0x5e, 0xdd, 0xca,
0xe8, 0xcf, 0x6d, 0xb4, 0x5b, 0x93, 0xb2, 0xca, 0x72, 0x23, 0x91, 0xa3, 0xa6, 0x4c, 0x10, 0x06,
];

struct Fixture {
Expand All @@ -31,16 +31,16 @@ struct Fixture {
}

fn fixture() -> Fixture {
fn field(name: &str) -> &'static str {
include_str!("fixtures/real-app-attest.json")
.lines()
let json = include_str!("fixtures/real-app-attest.json");
let field = |name: &str| {
json.lines()
.find_map(|line| {
let (key, value) = line.trim().split_once(" : ")?;
(key.trim_matches('"') == name)
.then(|| value.trim_end_matches(',').trim_matches('"'))
})
.unwrap()
}
};
Fixture {
attestation_object_base64: field("attestationObjectBase64"),
attestation_client_data_hash_hex: field("attestationClientDataHashHex"),
Expand Down Expand Up @@ -112,7 +112,7 @@ fn assertion(

let mut authenticator_data = Vec::new();
authenticator_data.extend_from_slice(&rp_id_hash);
authenticator_data.push(0x40);
authenticator_data.push(0xc0);
authenticator_data.extend_from_slice(&counter.to_be_bytes());
authenticator_data.extend_from_slice(&extension_bytes);
signed_assertion(signing_key, authenticator_data, client_data_hash)
Expand Down Expand Up @@ -180,7 +180,7 @@ fn real_assertion_verifies_with_app_rp_id_and_direct_cd_hash_allowlist() {
assert_eq!(claims.cd_hash, REAL_CD_HASH);
assert_eq!(appraise_apple(&claims, &policy), AppleVerdict::Accepted);

let legacy_hashed_cd_policy = ApplePolicy {
let hashed_cd_policy = ApplePolicy {
expected_rp_id_hash: apple_app_id_hash(APP_ID),
allowed_cd_hashes: vec![Sha256::digest(REAL_CD_HASH).into()],
};
Expand All @@ -189,7 +189,7 @@ fn real_assertion_verifies_with_app_rp_id_and_direct_cd_hash_allowlist() {
&assertion,
&client_data_hash,
&registered,
&legacy_hashed_cd_policy,
&hashed_cd_policy,
),
Err(AttestationError::Credential)
);
Expand All @@ -204,8 +204,8 @@ fn credential_identity_extracts_real_build_identity_without_rp_id_relation() {
assert_eq!(identity.public_key, registered_fixture(&fixture).public_key);
assert_eq!(identity.rp_id_hash, apple_app_id_hash(APP_ID));
assert_eq!(identity.cd_hash, REAL_CD_HASH);
let legacy_rp_id_hash: [u8; 32] = Sha256::digest(identity.cd_hash).into();
assert_ne!(apple_app_id_hash(APP_ID), legacy_rp_id_hash);
let hashed_cd_hash: [u8; 32] = Sha256::digest(identity.cd_hash).into();
assert_ne!(apple_app_id_hash(APP_ID), hashed_cd_hash);
}

#[test]
Expand Down Expand Up @@ -284,7 +284,7 @@ fn rejects_signed_assertion_that_omits_cd_hash_evidence() {
};
let mut authenticator_data = Vec::new();
authenticator_data.extend_from_slice(&rp_id_hash);
authenticator_data.push(0x40);
authenticator_data.push(0xc0);
authenticator_data.extend_from_slice(&1_u32.to_be_bytes());
let assertion = signed_assertion(&signing_key, authenticator_data, &client_data_hash);

Expand All @@ -294,6 +294,62 @@ fn rejects_signed_assertion_that_omits_cd_hash_evidence() {
);
}

#[test]
fn rejects_credential_with_invalid_authenticator_flags() {
let credential = fixture_credential(&fixture());
let mut object: BTreeMap<String, ciborium::Value> =
ciborium::from_reader(credential.attestation.as_slice()).unwrap();

for flags in [0x00, 0x40, 0x80, 0xc1] {
object.get_mut("authData").unwrap().as_bytes_mut().unwrap()[32] = flags;
let mut attestation = Vec::new();
ciborium::into_writer(&object, &mut attestation).unwrap();

assert_eq!(
apple_credential_identity(&attestation),
Err(AttestationError::Credential)
);
assert_eq!(
register_apple(
&AppleCredential {
attestation,
client_data_hash: credential.client_data_hash,
},
apple_app_id_hash(APP_ID),
apple_app_attest_root_ca(),
AnchorTime(FIXTURE_VALIDATION_TIME),
),
Err(AttestationError::Credential)
);
}
}

#[test]
fn rejects_signed_assertion_with_invalid_authenticator_flags() {
let fixture = fixture();
let signing_key = signing_key();
let registered = registered(&signing_key);
let client_data_hash = decode_hex_32(fixture.assertion_client_data_hash_hex);
let assertion = decode_base64(fixture.assertion_object_base64);
let mut object: BTreeMap<String, ByteBuf> =
ciborium::from_reader(assertion.as_slice()).unwrap();
let mut auth_data = object.remove("authenticatorData").unwrap().into_vec();
let policy = ApplePolicy {
expected_rp_id_hash: apple_app_id_hash(APP_ID),
allowed_cd_hashes: vec![REAL_CD_HASH],
};

for flags in [0x00, 0x40, 0x80, 0xc1] {
auth_data[32] = flags;
let assertion = signed_assertion(&signing_key, auth_data.clone(), &client_data_hash);

assert_eq!(
verify_apple_assertion(&assertion, &client_data_hash, &registered, &policy),
Err(AttestationError::Binding)
);
}
}

#[test]
fn repeated_verification_does_not_advance_counter_state() {
let signing_key = signing_key();
Expand Down
Loading
Loading