98 lines
5.1 KiB
JSON
98 lines
5.1 KiB
JSON
{
|
|
"_comment": "Cross-language test vectors for device-signature canonical-string + Ed25519 signing. Both Manager (Go) and client (Rust+TS) tests load this file, compute the canonical string from the raw inputs in their own implementation, and assert the computed string is byte-identical to expected_canonical. The shared test key lets each side independently produce + verify a signature; because Ed25519 sign is deterministic, both implementations must produce the SAME signature for the same digest. The test seed below is RFC 8032 test vector 1's secret — published, not real. Never use these keys for anything beyond unit tests.",
|
|
|
|
"test_keypair": {
|
|
"_comment": "32-byte Ed25519 seed, hex-encoded. Public key is derived deterministically. RFC 8032 test vector 1.",
|
|
"seed_hex": "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60",
|
|
"public_key_b64": "11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo=",
|
|
"public_key_hex": "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a"
|
|
},
|
|
|
|
"canonical_format_spec": {
|
|
"_comment": "EXACT byte layout that the canonical string must match. Any drift between Go and Rust/TS implementations here breaks signature verification end-to-end. Document mirror lives at: cc-haha/src/services/device/signRequest.ts (top-of-file comment) and heicode/middleware/device_signature.go (HeaderDeviceID block).",
|
|
"fields_in_order": [
|
|
"method (uppercase ASCII, e.g. POST)",
|
|
"path_with_query (RequestURI form: /v1/messages?stream=true)",
|
|
"timestamp_ms (decimal integer, no thousands separator, no sign)",
|
|
"nonce_hex (lowercase hex, 32 chars for 16 bytes)",
|
|
"device_fingerprint (lowercase hex, 64 chars for sha256)",
|
|
"sha256_hex(body_bytes) (lowercase hex, 64 chars)"
|
|
],
|
|
"separator": "\\n (single line-feed, 0x0A, between each field; NOT included after the last field)",
|
|
"digest_to_sign": "sha256(canonical_string)"
|
|
},
|
|
|
|
"cases": [
|
|
{
|
|
"name": "GET_empty_body",
|
|
"input": {
|
|
"method": "GET",
|
|
"path_with_query": "/v1/models",
|
|
"timestamp_ms": "1747680000000",
|
|
"nonce_hex": "0123456789abcdef0123456789abcdef",
|
|
"device_fingerprint": "a1b2c3d4e5f6789abcdef0123456789abcdef0123456789abcdef0123456789a",
|
|
"body_text": ""
|
|
},
|
|
"expected_body_sha256_hex": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
"expected_canonical": "GET\n/v1/models\n1747680000000\n0123456789abcdef0123456789abcdef\na1b2c3d4e5f6789abcdef0123456789abcdef0123456789abcdef0123456789a\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
|
},
|
|
{
|
|
"name": "POST_small_json_body",
|
|
"input": {
|
|
"method": "POST",
|
|
"path_with_query": "/v1/messages",
|
|
"timestamp_ms": "1747680001234",
|
|
"nonce_hex": "ffffffffffffffffffffffffffffffff",
|
|
"device_fingerprint": "0000000000000000000000000000000000000000000000000000000000000000",
|
|
"body_text": "{\"model\":\"claude-sonnet-4-6\",\"max_tokens\":1024}"
|
|
},
|
|
"expected_body_sha256_hex": "a86e9f8fe1ec25a48b78f4d1d3df88b3dee3c5816fbc8ce26d8b2bc44b46d4b8",
|
|
"expected_canonical_starts_with": "POST\n/v1/messages\n1747680001234\nffffffffffffffffffffffffffffffff\n0000000000000000000000000000000000000000000000000000000000000000\n",
|
|
"_note_about_body_hash": "expected_body_sha256_hex above is illustrative; tests MUST recompute SHA256 of body_text bytes (UTF-8) and compare directly rather than relying on the precomputed value."
|
|
},
|
|
{
|
|
"name": "POST_streaming_request",
|
|
"input": {
|
|
"method": "POST",
|
|
"path_with_query": "/v1/messages?stream=true",
|
|
"timestamp_ms": "1747680002000",
|
|
"nonce_hex": "abababababababababababababababab",
|
|
"device_fingerprint": "11111111111111111111111111111111deadbeefdeadbeefdeadbeefdeadbeef",
|
|
"body_text": "{\"model\":\"claude-sonnet-4-6\",\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}],\"max_tokens\":256,\"stream\":true}"
|
|
},
|
|
"expected_canonical_starts_with": "POST\n/v1/messages?stream=true\n1747680002000\n"
|
|
},
|
|
{
|
|
"name": "GET_query_with_special_chars",
|
|
"input": {
|
|
"method": "GET",
|
|
"path_with_query": "/v1/dashboard/billing/usage?date=2026-05-20&filter=cost+desc",
|
|
"timestamp_ms": "1747680003000",
|
|
"nonce_hex": "deadbeefcafebabe0001020304050607",
|
|
"device_fingerprint": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
|
"body_text": ""
|
|
},
|
|
"expected_canonical_starts_with": "GET\n/v1/dashboard/billing/usage?date=2026-05-20&filter=cost+desc\n"
|
|
}
|
|
],
|
|
|
|
"negative_cases": [
|
|
{
|
|
"name": "tampered_timestamp",
|
|
"_comment": "Sign canonical_1, then alter timestamp on the wire to canonical_2. Verify with pubkey + canonical_2 → must fail.",
|
|
"fixture": "GET_empty_body",
|
|
"tamper": { "timestamp_ms": "1747680000001" }
|
|
},
|
|
{
|
|
"name": "tampered_body",
|
|
"fixture": "POST_small_json_body",
|
|
"tamper": { "body_text": "{\"model\":\"claude-opus-4-7\",\"max_tokens\":1024}" }
|
|
},
|
|
{
|
|
"name": "wrong_method",
|
|
"fixture": "POST_small_json_body",
|
|
"tamper": { "method": "PUT" }
|
|
}
|
|
]
|
|
}
|