mirror of
https://github.com/block/buzz.git
synced 2026-11-05 08:51:10 +00:00
test(push-gateway): pin App Attest transcript bytes with known-answer vectors
Add tests/vectors/app_attest_transcripts.json — deterministic known-answer vectors (exact transcript bytes + SHA-256) for all five NIP-PL App Attest operations: enroll, delegate, rotate_endpoint, revoke_delegation, revoke_installation — generated by the gateway's own transcript() encoder, plus in-crate tests asserting the encoder reproduces the fixture byte-for-byte. The fixture is shared ground truth for client-side canonical encoders (the Swift NIP-PL iOS client): a client that fails to reproduce these bytes exactly fails every attested call with invalid_attestation and no diagnostic (Swift JSONSerialization's \/ escaping is the known trap). Any future change to the Rust encoder now breaks a test instead of silently stranding clients. Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
This commit is contained in:
parent
7fbdacb03f
commit
a5098e990a
@@ -774,3 +774,146 @@ pub fn router_with_metrics(
|
||||
}
|
||||
(public, health)
|
||||
}
|
||||
|
||||
/// Known-answer vectors for the exact App Attest transcript bytes defined by
|
||||
/// NIP-PL ("Exact App Attest transcript construction"). The fixture file is
|
||||
/// shared ground truth with client-side canonical encoders (the Swift NIP-PL
|
||||
/// iOS client): a client encoder that fails to reproduce these bytes exactly
|
||||
/// fails every enroll/delegate/rotate/revoke call with `invalid_attestation`.
|
||||
#[cfg(test)]
|
||||
mod transcript_vector_tests {
|
||||
use super::*;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
const VECTORS_JSON: &str = include_str!("../tests/vectors/app_attest_transcripts.json");
|
||||
|
||||
// Deterministic fixture inputs mirrored in the vector file's `inputs`.
|
||||
const CHALLENGE_ID: uuid::Uuid =
|
||||
uuid::Uuid::from_u128(0x1111_1111_1111_4111_8111_1111_1111_1111);
|
||||
const INSTALLATION: uuid::Uuid =
|
||||
uuid::Uuid::from_u128(0x2222_2222_2222_4222_8222_2222_2222_2222);
|
||||
// base64url-no-pad of bytes 0x00..=0x1f.
|
||||
const CHALLENGE: &str = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8";
|
||||
// Standard base64 (padded) of 32 bytes of 0xAA.
|
||||
const KEY_ID: &str = "qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqo=";
|
||||
// 32-byte APNs token, lowercase hex.
|
||||
const ENDPOINT: &str = "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20";
|
||||
const RELAY_PUBKEY: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
||||
|
||||
fn assert_vector(name: &str, actual: &str) {
|
||||
let file: serde_json::Value = serde_json::from_str(VECTORS_JSON).unwrap();
|
||||
let vector = file["vectors"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find(|v| v["name"] == name)
|
||||
.unwrap_or_else(|| panic!("vector {name} missing from fixture"));
|
||||
assert_eq!(
|
||||
actual,
|
||||
vector["transcript"].as_str().unwrap(),
|
||||
"{name} bytes"
|
||||
);
|
||||
assert_eq!(
|
||||
hex::encode(Sha256::digest(actual.as_bytes())),
|
||||
vector["sha256"].as_str().unwrap(),
|
||||
"{name} sha256"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixture_encodings_match_their_raw_bytes() {
|
||||
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
||||
let challenge_bytes: Vec<u8> = (0u8..32).collect();
|
||||
assert_eq!(URL_SAFE_NO_PAD.encode(&challenge_bytes), CHALLENGE);
|
||||
assert_eq!(STANDARD.encode([0xAAu8; 32]), KEY_ID);
|
||||
assert_eq!(hex::decode(ENDPOINT).unwrap().len(), 32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enroll_transcript_vector() {
|
||||
let t = EnrollTranscript {
|
||||
v: 1,
|
||||
audience: "https://push.buzz.xyz/v1/installations",
|
||||
challenge_id: CHALLENGE_ID,
|
||||
challenge: CHALLENGE,
|
||||
key_id: KEY_ID,
|
||||
app_profile: AppProfile::BuzzIosProduction,
|
||||
endpoint: ENDPOINT,
|
||||
endpoint_epoch: 1,
|
||||
expires_at: 1_752_624_000,
|
||||
};
|
||||
assert_vector("enroll", &transcript("buzz.push.enroll.v1", &t).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delegate_transcript_vector() {
|
||||
let t = DelegateTranscript {
|
||||
v: 1,
|
||||
audience: "https://push.buzz.xyz/v1/delegations",
|
||||
challenge_id: CHALLENGE_ID,
|
||||
challenge: CHALLENGE,
|
||||
installation_handle: INSTALLATION,
|
||||
endpoint_epoch: 1,
|
||||
generation: 1,
|
||||
relay_pubkey: RELAY_PUBKEY,
|
||||
not_before: 1_752_620_000,
|
||||
expires_at: 1_752_624_000,
|
||||
};
|
||||
assert_vector(
|
||||
"delegate",
|
||||
&transcript("buzz.push.delegate.v1", &t).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rotate_endpoint_transcript_vector() {
|
||||
let t = RotateTranscript {
|
||||
v: 1,
|
||||
audience: "https://push.buzz.xyz/v1/installations/endpoint",
|
||||
challenge_id: CHALLENGE_ID,
|
||||
challenge: CHALLENGE,
|
||||
installation_handle: INSTALLATION,
|
||||
endpoint_epoch: 1,
|
||||
new_endpoint_epoch: 2,
|
||||
endpoint: ENDPOINT,
|
||||
};
|
||||
assert_vector(
|
||||
"rotate_endpoint",
|
||||
&transcript("buzz.push.rotate-endpoint.v1", &t).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revoke_delegation_transcript_vector() {
|
||||
let t = RevokeDelegationTranscript {
|
||||
v: 1,
|
||||
audience: "https://push.buzz.xyz/v1/delegations/revoke",
|
||||
challenge_id: CHALLENGE_ID,
|
||||
challenge: CHALLENGE,
|
||||
installation_handle: INSTALLATION,
|
||||
relay_pubkey: RELAY_PUBKEY,
|
||||
generation: 2,
|
||||
};
|
||||
assert_vector(
|
||||
"revoke_delegation",
|
||||
&transcript("buzz.push.revoke-delegation.v1", &t).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revoke_installation_transcript_vector() {
|
||||
let t = RevokeInstallationTranscript {
|
||||
v: 1,
|
||||
audience: "https://push.buzz.xyz/v1/installations/revoke",
|
||||
challenge_id: CHALLENGE_ID,
|
||||
challenge: CHALLENGE,
|
||||
installation_handle: INSTALLATION,
|
||||
endpoint_epoch: 1,
|
||||
new_endpoint_epoch: 2,
|
||||
};
|
||||
assert_vector(
|
||||
"revoke_installation",
|
||||
&transcript("buzz.push.revoke-installation.v1", &t).unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"description": "Known-answer vectors for the exact App Attest transcript bytes defined by NIP-PL ('Exact App Attest transcript construction'). Generated by the gateway's own transcript encoder (crates/buzz-push-gateway/src/http.rs transcript()). Client canonical encoders (Swift NIP-PL iOS client) MUST reproduce `transcript` byte-for-byte; `sha256` is the hex digest of those UTF-8 bytes (the App Attest clientDataHash input for assertion routes, and the exact clientData for enrollment).",
|
||||
"inputs": {
|
||||
"challenge_id": "11111111-1111-4111-8111-111111111111",
|
||||
"installation_handle": "22222222-2222-4222-8222-222222222222",
|
||||
"challenge": "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8",
|
||||
"challenge_note": "base64url-no-pad of bytes 0x00..0x1f",
|
||||
"key_id": "qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqo=",
|
||||
"key_id_note": "standard base64 (padded) of 32 bytes of 0xAA",
|
||||
"app_profile": "buzz-ios-production",
|
||||
"endpoint": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20",
|
||||
"relay_pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"not_before": 1752620000,
|
||||
"expires_at": 1752624000
|
||||
},
|
||||
"vectors": [
|
||||
{
|
||||
"name": "enroll",
|
||||
"domain": "buzz.push.enroll.v1",
|
||||
"transcript": "buzz.push.enroll.v1\n{\"v\":1,\"audience\":\"https://push.buzz.xyz/v1/installations\",\"challenge_id\":\"11111111-1111-4111-8111-111111111111\",\"challenge\":\"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8\",\"key_id\":\"qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqo=\",\"app_profile\":\"buzz-ios-production\",\"endpoint\":\"0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20\",\"endpoint_epoch\":1,\"expires_at\":1752624000}",
|
||||
"sha256": "f8f41fd671918aa0b7dc8eca704b26a5c2748270c2efc7db7348e36a4cccd189"
|
||||
},
|
||||
{
|
||||
"name": "delegate",
|
||||
"domain": "buzz.push.delegate.v1",
|
||||
"transcript": "buzz.push.delegate.v1\n{\"v\":1,\"audience\":\"https://push.buzz.xyz/v1/delegations\",\"challenge_id\":\"11111111-1111-4111-8111-111111111111\",\"challenge\":\"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8\",\"installation_handle\":\"22222222-2222-4222-8222-222222222222\",\"endpoint_epoch\":1,\"generation\":1,\"relay_pubkey\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"not_before\":1752620000,\"expires_at\":1752624000}",
|
||||
"sha256": "7466177cc2dc2a4f9a075fdbb461531692fc858778a171a5862b855cccfaa059"
|
||||
},
|
||||
{
|
||||
"name": "rotate_endpoint",
|
||||
"domain": "buzz.push.rotate-endpoint.v1",
|
||||
"transcript": "buzz.push.rotate-endpoint.v1\n{\"v\":1,\"audience\":\"https://push.buzz.xyz/v1/installations/endpoint\",\"challenge_id\":\"11111111-1111-4111-8111-111111111111\",\"challenge\":\"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8\",\"installation_handle\":\"22222222-2222-4222-8222-222222222222\",\"endpoint_epoch\":1,\"new_endpoint_epoch\":2,\"endpoint\":\"0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20\"}",
|
||||
"sha256": "601aba0c8d4021ddf97ce1e434b9c7ad1e051bf02a44929aaebd8c6bd724e7b3"
|
||||
},
|
||||
{
|
||||
"name": "revoke_delegation",
|
||||
"domain": "buzz.push.revoke-delegation.v1",
|
||||
"transcript": "buzz.push.revoke-delegation.v1\n{\"v\":1,\"audience\":\"https://push.buzz.xyz/v1/delegations/revoke\",\"challenge_id\":\"11111111-1111-4111-8111-111111111111\",\"challenge\":\"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8\",\"installation_handle\":\"22222222-2222-4222-8222-222222222222\",\"relay_pubkey\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"generation\":2}",
|
||||
"sha256": "d6bcd4b25235adcb519ef189820b08dd0386fc752fd4e4c77bc3ffb7a519a84a"
|
||||
},
|
||||
{
|
||||
"name": "revoke_installation",
|
||||
"domain": "buzz.push.revoke-installation.v1",
|
||||
"transcript": "buzz.push.revoke-installation.v1\n{\"v\":1,\"audience\":\"https://push.buzz.xyz/v1/installations/revoke\",\"challenge_id\":\"11111111-1111-4111-8111-111111111111\",\"challenge\":\"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8\",\"installation_handle\":\"22222222-2222-4222-8222-222222222222\",\"endpoint_epoch\":1,\"new_endpoint_epoch\":2}",
|
||||
"sha256": "0ba51827af6586a5e1230e9b770b99544fb342efb55db3ab1ce499cf24a893c8"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user