1use sha2::{Digest, Sha256};
4
5pub const DOMAIN: &str = "heddle-req-sig-v1";
6pub const HEADER_ALGORITHM: &str = "x-heddle-sig-alg";
7pub const HEADER_SIGNATURE_BIN: &str = "x-heddle-sig-bin";
8pub const HEADER_TIMESTAMP: &str = "x-heddle-sig-ts";
9pub const HEADER_NONCE_BIN: &str = "x-heddle-sig-nonce-bin";
10pub const HEADER_IDENTITY: &str = "x-heddle-sig-identity";
11pub const HEADER_WEBAUTHN_CLIENT_DATA_BIN: &str = "x-heddle-sig-webauthn-client-data-bin";
12pub const HEADER_WEBAUTHN_AUTH_DATA_BIN: &str = "x-heddle-sig-webauthn-auth-data-bin";
13pub const HEADER_WEBAUTHN_USER_HANDLE_BIN: &str = "x-heddle-sig-webauthn-user-handle-bin";
14pub const HEADER_REQUIRED: &str = "x-heddle-sig-required";
15pub const HEADER_ACTION_URL: &str = "x-heddle-sig-action-url";
16
17pub fn unary_bytes(
19 signing_identity: &str,
20 route: &str,
21 timestamp_millis: i64,
22 nonce: &[u8],
23 deterministic_request: &[u8],
24) -> Vec<u8> {
25 canonical(
26 "unary",
27 &[
28 ("identity", signing_identity.as_bytes().to_vec()),
29 ("route", route.as_bytes().to_vec()),
30 ("timestamp_ms", timestamp_millis.to_string().into_bytes()),
31 ("nonce", hex::encode(nonce).into_bytes()),
32 (
33 "request_sha256",
34 hex::encode(Sha256::digest(deterministic_request)).into_bytes(),
35 ),
36 ],
37 )
38}
39
40pub fn stream_open_bytes(
42 signing_identity: &str,
43 stream_id: &str,
44 route: &str,
45 repository: &str,
46 resume_cursor: &str,
47 capability_context: &[u8],
48) -> Vec<u8> {
49 canonical(
50 "stream-open",
51 &[
52 ("identity", signing_identity.as_bytes().to_vec()),
53 ("stream_id", stream_id.as_bytes().to_vec()),
54 ("route", route.as_bytes().to_vec()),
55 ("repository", repository.as_bytes().to_vec()),
56 ("resume_cursor", resume_cursor.as_bytes().to_vec()),
57 (
58 "capability_sha256",
59 hex::encode(Sha256::digest(capability_context)).into_bytes(),
60 ),
61 ],
62 )
63}
64
65pub fn retry_key_hash(route: &str, client_operation_id: &str, request: &[u8]) -> [u8; 32] {
67 Sha256::digest(canonical(
68 "retry-key",
69 &[
70 ("route", route.as_bytes().to_vec()),
71 (
72 "client_operation_id",
73 client_operation_id.as_bytes().to_vec(),
74 ),
75 (
76 "request_sha256",
77 hex::encode(Sha256::digest(request)).into_bytes(),
78 ),
79 ],
80 ))
81 .into()
82}
83
84fn canonical(kind: &str, fields: &[(&str, Vec<u8>)]) -> Vec<u8> {
85 let mut result = format!("{DOMAIN}\nkind={}:{}", kind.len(), kind).into_bytes();
86 for (name, value) in fields {
87 result.extend_from_slice(format!("\n{name}={}:", value.len()).as_bytes());
88 result.extend_from_slice(value);
89 }
90 result
91}
92
93#[cfg(test)]
94mod tests {
95 use serde::Deserialize;
96
97 use super::*;
98
99 #[derive(Deserialize)]
100 struct UnaryVector {
101 identity: String,
102 route: String,
103 timestamp_millis: i64,
104 nonce_hex: String,
105 request_hex: String,
106 canonical_hex: String,
107 }
108
109 #[test]
110 fn canonical_fields_are_length_delimited() {
111 let first = unary_bytes("ab", "/c", 1, &[0], &[1]);
112 let second = unary_bytes("a", "b/c", 1, &[0], &[1]);
113 assert_ne!(first, second);
114 assert!(first.starts_with(b"heddle-req-sig-v1\nkind=5:unary"));
115 }
116
117 #[test]
118 fn unary_signature_matches_cross_language_vector() {
119 let vector: UnaryVector =
120 serde_json::from_str(include_str!("../tests/fixtures/unary-signing-v1.json"))
121 .expect("valid fixture");
122 let actual = unary_bytes(
123 &vector.identity,
124 &vector.route,
125 vector.timestamp_millis,
126 &hex::decode(vector.nonce_hex).expect("nonce hex"),
127 &hex::decode(vector.request_hex).expect("request hex"),
128 );
129 assert_eq!(hex::encode(actual), vector.canonical_hex);
130 }
131}