1use sha2::{Digest, Sha256};
4
5use crate::heddle::api::v1alpha1::{EndpointDescriptor, RelayAdmissionClaims};
6use prost::Message;
7
8pub const DOMAIN: &str = "heddle-req-sig-v1";
9pub const PROVIDER_PLAN_DOMAIN: &str = "heddle-provider-plan-v1";
10pub const HEADER_ALGORITHM: &str = "x-heddle-sig-alg";
11pub const HEADER_SIGNATURE_BIN: &str = "x-heddle-sig-bin";
12pub const HEADER_TIMESTAMP: &str = "x-heddle-sig-ts";
13pub const HEADER_NONCE_BIN: &str = "x-heddle-sig-nonce-bin";
14pub const HEADER_IDENTITY: &str = "x-heddle-sig-identity";
15pub const HEADER_WEBAUTHN_CLIENT_DATA_BIN: &str = "x-heddle-sig-webauthn-client-data-bin";
16pub const HEADER_WEBAUTHN_AUTH_DATA_BIN: &str = "x-heddle-sig-webauthn-auth-data-bin";
17pub const HEADER_WEBAUTHN_USER_HANDLE_BIN: &str = "x-heddle-sig-webauthn-user-handle-bin";
18pub const HEADER_REQUIRED: &str = "x-heddle-sig-required";
19pub const HEADER_ACTION_URL: &str = "x-heddle-sig-action-url";
20
21pub fn unary_bytes(
23 signing_identity: &str,
24 route: &str,
25 timestamp_millis: i64,
26 nonce: &[u8],
27 deterministic_request: &[u8],
28) -> Vec<u8> {
29 canonical(
30 "unary",
31 &[
32 ("identity", signing_identity.as_bytes().to_vec()),
33 ("route", route.as_bytes().to_vec()),
34 ("timestamp_ms", timestamp_millis.to_string().into_bytes()),
35 ("nonce", hex::encode(nonce).into_bytes()),
36 (
37 "request_sha256",
38 hex::encode(Sha256::digest(deterministic_request)).into_bytes(),
39 ),
40 ],
41 )
42}
43
44pub fn stream_open_bytes(
46 signing_identity: &str,
47 stream_id: &str,
48 route: &str,
49 repository: &str,
50 resume_cursor: &str,
51 capability_context: &[u8],
52) -> Vec<u8> {
53 canonical(
54 "stream-open",
55 &[
56 ("identity", signing_identity.as_bytes().to_vec()),
57 ("stream_id", stream_id.as_bytes().to_vec()),
58 ("route", route.as_bytes().to_vec()),
59 ("repository", repository.as_bytes().to_vec()),
60 ("resume_cursor", resume_cursor.as_bytes().to_vec()),
61 (
62 "capability_sha256",
63 hex::encode(Sha256::digest(capability_context)).into_bytes(),
64 ),
65 ],
66 )
67}
68
69pub fn provider_plan_bytes(
76 signing_identity: &str,
77 stream_id: &str,
78 repository: &str,
79 client_endpoint_id: &str,
80 plan_nonce: &[u8],
81 grant_batch_digest: &[u8],
82) -> Vec<u8> {
83 provider_plan_canonical(
84 "exact-batch",
85 &[
86 ("identity", signing_identity.as_bytes().to_vec()),
87 ("stream_id", stream_id.as_bytes().to_vec()),
88 ("repository", repository.as_bytes().to_vec()),
89 ("client_endpoint_id", client_endpoint_id.as_bytes().to_vec()),
90 ("plan_nonce", hex::encode(plan_nonce).into_bytes()),
91 (
92 "grant_batch_digest",
93 hex::encode(grant_batch_digest).into_bytes(),
94 ),
95 ],
96 )
97}
98
99pub fn retry_key_hash(route: &str, client_operation_id: &str, request: &[u8]) -> [u8; 32] {
101 Sha256::digest(canonical(
102 "retry-key",
103 &[
104 ("route", route.as_bytes().to_vec()),
105 (
106 "client_operation_id",
107 client_operation_id.as_bytes().to_vec(),
108 ),
109 (
110 "request_sha256",
111 hex::encode(Sha256::digest(request)).into_bytes(),
112 ),
113 ],
114 ))
115 .into()
116}
117
118pub fn endpoint_descriptor_bytes(descriptor: &EndpointDescriptor) -> Vec<u8> {
120 bootstrap_bytes("endpoint-descriptor", descriptor)
121}
122
123pub fn relay_admission_bytes(claims: &RelayAdmissionClaims) -> Vec<u8> {
125 bootstrap_bytes("relay-admission", claims)
126}
127
128fn bootstrap_bytes(kind: &str, message: &impl Message) -> Vec<u8> {
129 canonical(kind, &[("protobuf", message.encode_to_vec())])
130}
131
132fn canonical(kind: &str, fields: &[(&str, Vec<u8>)]) -> Vec<u8> {
133 let mut result = format!("{DOMAIN}\nkind={}:{}", kind.len(), kind).into_bytes();
134 for (name, value) in fields {
135 result.extend_from_slice(format!("\n{name}={}:", value.len()).as_bytes());
136 result.extend_from_slice(value);
137 }
138 result
139}
140
141fn provider_plan_canonical(kind: &str, fields: &[(&str, Vec<u8>)]) -> Vec<u8> {
142 let mut result = format!("{PROVIDER_PLAN_DOMAIN}\nkind={}:{}", kind.len(), kind).into_bytes();
143 for (name, value) in fields {
144 result.extend_from_slice(format!("\n{name}={}:", value.len()).as_bytes());
145 result.extend_from_slice(value);
146 }
147 result
148}
149
150#[cfg(test)]
151mod tests {
152 use serde::Deserialize;
153
154 use super::*;
155
156 #[derive(Deserialize)]
157 struct UnaryVector {
158 identity: String,
159 route: String,
160 timestamp_millis: i64,
161 nonce_hex: String,
162 request_hex: String,
163 canonical_hex: String,
164 }
165
166 #[test]
167 fn canonical_fields_are_length_delimited() {
168 let first = unary_bytes("ab", "/c", 1, &[0], &[1]);
169 let second = unary_bytes("a", "b/c", 1, &[0], &[1]);
170 assert_ne!(first, second);
171 assert!(first.starts_with(b"heddle-req-sig-v1\nkind=5:unary"));
172 }
173
174 #[test]
175 fn provider_plan_signature_changes_with_every_authorization_binding() {
176 let endpoint = "11".repeat(32);
177 let baseline = provider_plan_bytes(
178 "principal:alice",
179 "pull:one",
180 "acme/widgets",
181 &endpoint,
182 &[7; 16],
183 &[9; 32],
184 );
185 let different_digest = provider_plan_bytes(
186 "principal:alice",
187 "pull:one",
188 "acme/widgets",
189 &endpoint,
190 &[7; 16],
191 &[8; 32],
192 );
193 let different_nonce = provider_plan_bytes(
194 "principal:alice",
195 "pull:one",
196 "acme/widgets",
197 &endpoint,
198 &[6; 16],
199 &[9; 32],
200 );
201
202 assert!(baseline.starts_with(b"heddle-provider-plan-v1\nkind=11:exact-batch"));
203 assert_ne!(baseline, different_digest);
204 assert_ne!(baseline, different_nonce);
205 }
206
207 #[test]
208 fn unary_signature_matches_cross_language_vector() {
209 let vector: UnaryVector =
210 serde_json::from_str(include_str!("../tests/fixtures/unary-signing-v1.json"))
211 .expect("valid fixture");
212 let actual = unary_bytes(
213 &vector.identity,
214 &vector.route,
215 vector.timestamp_millis,
216 &hex::decode(vector.nonce_hex).expect("nonce hex"),
217 &hex::decode(vector.request_hex).expect("request hex"),
218 );
219 assert_eq!(hex::encode(actual), vector.canonical_hex);
220 }
221}