1use {
2 super::error::SignedHttpError,
3 base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _},
4 ed25519_dalek::{Signature, Signer as _, SigningKey, VerifyingKey},
5 serde::{Deserialize, Serialize},
6 sha2::{Digest as _, Sha256},
7 std::{
8 collections::HashMap,
9 fs,
10 path::{Path, PathBuf},
11 },
12};
13
14pub const HEADER_SIGNATURE_VERSION: &str = "X-Nexus-Sig-V";
15pub const HEADER_LEADER_ID: &str = "X-Nexus-Leader-Id";
16pub const HEADER_LEADER_KEY_ID: &str = "X-Nexus-Leader-Key-Id";
17pub const HEADER_INPUT_HASH: &str = "X-Nexus-Input-Hash";
18pub const HEADER_LEADER_SIGNATURE: &str = "X-Nexus-Leader-Signature";
19pub const HEADER_NONCE: &str = "X-Nexus-Nonce";
20pub const HEADER_TOOL_SIGNATURE: &str = "X-Nexus-Tool-Signature";
21pub const SIGNATURE_VERSION_V2: &str = "2";
22
23const SHA256_LEN: usize = 32;
24const ED25519_SIGNATURE_LEN: usize = 64;
25const TOOL_RESPONSE_DOMAIN: &[u8] = b"nexus.signed_http.v2.tool_response";
26
27#[derive(Clone, Debug, PartialEq, Eq)]
28pub struct EncodedRequestHeaders {
29 pub leader_id: String,
30 pub leader_key_id: u64,
31 pub input_hash: String,
32 pub leader_signature: String,
33 pub nonce: String,
34}
35
36impl EncodedRequestHeaders {
37 pub fn to_pairs(&self) -> [(&'static str, String); 6] {
38 [
39 (HEADER_SIGNATURE_VERSION, SIGNATURE_VERSION_V2.to_string()),
40 (HEADER_LEADER_ID, self.leader_id.clone()),
41 (HEADER_LEADER_KEY_ID, self.leader_key_id.to_string()),
42 (HEADER_INPUT_HASH, self.input_hash.clone()),
43 (HEADER_LEADER_SIGNATURE, self.leader_signature.clone()),
44 (HEADER_NONCE, self.nonce.clone()),
45 ]
46 }
47}
48
49#[derive(Clone, Debug, PartialEq, Eq)]
50pub struct EncodedResponseHeaders {
51 pub tool_signature: String,
52}
53
54impl EncodedResponseHeaders {
55 pub fn to_pairs(&self) -> [(&'static str, String); 2] {
56 [
57 (HEADER_SIGNATURE_VERSION, SIGNATURE_VERSION_V2.to_string()),
58 (HEADER_TOOL_SIGNATURE, self.tool_signature.clone()),
59 ]
60 }
61}
62
63#[derive(Clone, Copy, Debug, Default)]
64pub struct RequestHeadersRef<'a> {
65 pub signature_version: Option<&'a str>,
66 pub leader_id: Option<&'a str>,
67 pub leader_key_id: Option<&'a str>,
68 pub input_hash: Option<&'a str>,
69 pub leader_signature: Option<&'a str>,
70 pub nonce: Option<&'a str>,
71}
72
73impl<'a> RequestHeadersRef<'a> {
74 pub fn from_getter(mut get: impl FnMut(&str) -> Option<&'a str>) -> Self {
75 Self {
76 signature_version: get(HEADER_SIGNATURE_VERSION),
77 leader_id: get(HEADER_LEADER_ID),
78 leader_key_id: get(HEADER_LEADER_KEY_ID),
79 input_hash: get(HEADER_INPUT_HASH),
80 leader_signature: get(HEADER_LEADER_SIGNATURE),
81 nonce: get(HEADER_NONCE),
82 }
83 }
84}
85
86#[derive(Clone, Copy, Debug, Default)]
87pub struct ResponseHeadersRef<'a> {
88 pub signature_version: Option<&'a str>,
89 pub tool_signature: Option<&'a str>,
90}
91
92impl<'a> ResponseHeadersRef<'a> {
93 pub fn from_getter(mut get: impl FnMut(&str) -> Option<&'a str>) -> Self {
94 Self {
95 signature_version: get(HEADER_SIGNATURE_VERSION),
96 tool_signature: get(HEADER_TOOL_SIGNATURE),
97 }
98 }
99}
100
101#[derive(Clone, Debug, PartialEq, Eq)]
102pub struct AuthenticatedRequest {
103 pub leader_id: String,
104 pub leader_key_id: u64,
105 pub input_hash: [u8; SHA256_LEN],
106 pub leader_signature: [u8; ED25519_SIGNATURE_LEN],
107 pub nonce: [u8; SHA256_LEN],
108}
109
110pub trait LeaderKeyResolver: Send + Sync {
111 fn leader_public_key(&self, leader_id: &str, leader_key_id: u64) -> Option<[u8; 32]>;
112}
113
114pub fn sign_request(
115 leader_id: impl Into<String>,
116 leader_key_id: u64,
117 input_hash: [u8; SHA256_LEN],
118 nonce: [u8; SHA256_LEN],
119 signing_key: &SigningKey,
120) -> EncodedRequestHeaders {
121 let signature = signing_key.sign(&input_hash).to_bytes();
122 EncodedRequestHeaders {
123 leader_id: leader_id.into(),
124 leader_key_id,
125 input_hash: URL_SAFE_NO_PAD.encode(input_hash),
126 leader_signature: URL_SAFE_NO_PAD.encode(signature),
127 nonce: URL_SAFE_NO_PAD.encode(nonce),
128 }
129}
130
131pub fn authenticate_request(
132 headers: RequestHeadersRef<'_>,
133 keys: &dyn LeaderKeyResolver,
134) -> Result<AuthenticatedRequest, SignedHttpError> {
135 require_version(headers.signature_version)?;
136 let leader_id = required(headers.leader_id, HEADER_LEADER_ID)?.to_string();
137 let leader_key_id_raw = required(headers.leader_key_id, HEADER_LEADER_KEY_ID)?;
138 let leader_key_id = leader_key_id_raw
139 .parse()
140 .map_err(|_| SignedHttpError::InvalidInteger {
141 header: HEADER_LEADER_KEY_ID,
142 value: leader_key_id_raw.to_string(),
143 })?;
144 let input_hash = decode_array::<SHA256_LEN>(headers.input_hash, HEADER_INPUT_HASH)?;
145 let leader_signature =
146 decode_array::<ED25519_SIGNATURE_LEN>(headers.leader_signature, HEADER_LEADER_SIGNATURE)?;
147 let nonce = decode_array::<SHA256_LEN>(headers.nonce, HEADER_NONCE)?;
148 let public_key = keys
149 .leader_public_key(&leader_id, leader_key_id)
150 .ok_or_else(|| SignedHttpError::UnknownLeaderKey {
151 leader_id: leader_id.clone(),
152 leader_key_id,
153 })?;
154 verify_signature(
155 public_key,
156 &input_hash,
157 leader_signature,
158 format!("leader {leader_id} key {leader_key_id}"),
159 )?;
160
161 Ok(AuthenticatedRequest {
162 leader_id,
163 leader_key_id,
164 input_hash,
165 leader_signature,
166 nonce,
167 })
168}
169
170pub fn sign_response(
171 leader_signature: &[u8; ED25519_SIGNATURE_LEN],
172 nonce: &[u8; SHA256_LEN],
173 result_bytes: &[u8],
174 signing_key: &SigningKey,
175) -> EncodedResponseHeaders {
176 let message = tool_signature_message(leader_signature, nonce, result_bytes);
177 EncodedResponseHeaders {
178 tool_signature: URL_SAFE_NO_PAD.encode(signing_key.sign(&message).to_bytes()),
179 }
180}
181
182pub fn verify_response(
183 headers: ResponseHeadersRef<'_>,
184 leader_signature: &[u8; ED25519_SIGNATURE_LEN],
185 nonce: &[u8; SHA256_LEN],
186 result_bytes: &[u8],
187 tool_public_key: [u8; 32],
188) -> Result<[u8; ED25519_SIGNATURE_LEN], SignedHttpError> {
189 require_version(headers.signature_version)?;
190 let tool_signature =
191 decode_array::<ED25519_SIGNATURE_LEN>(headers.tool_signature, HEADER_TOOL_SIGNATURE)?;
192 verify_signature(
193 tool_public_key,
194 &tool_signature_message(leader_signature, nonce, result_bytes),
195 tool_signature,
196 "tool".to_string(),
197 )?;
198 Ok(tool_signature)
199}
200
201pub fn tool_signature_message(
202 leader_signature: &[u8; ED25519_SIGNATURE_LEN],
203 nonce: &[u8; SHA256_LEN],
204 result_bytes: &[u8],
205) -> Vec<u8> {
206 let mut message = Vec::with_capacity(
207 TOOL_RESPONSE_DOMAIN.len() + ED25519_SIGNATURE_LEN + SHA256_LEN + SHA256_LEN,
208 );
209 message.extend_from_slice(TOOL_RESPONSE_DOMAIN);
210 message.extend_from_slice(leader_signature);
211 message.extend_from_slice(nonce);
212 message.extend_from_slice(&sha256(result_bytes));
213 message
214}
215
216pub fn sha256(bytes: &[u8]) -> [u8; SHA256_LEN] {
217 Sha256::digest(bytes).into()
218}
219
220fn required<'a>(value: Option<&'a str>, header: &'static str) -> Result<&'a str, SignedHttpError> {
221 value.ok_or(SignedHttpError::MissingHeader(header))
222}
223
224fn require_version(version: Option<&str>) -> Result<(), SignedHttpError> {
225 let version = required(version, HEADER_SIGNATURE_VERSION)?;
226 if version != SIGNATURE_VERSION_V2 {
227 return Err(SignedHttpError::UnsupportedVersion(version.to_string()));
228 }
229 Ok(())
230}
231
232fn decode_array<const N: usize>(
233 encoded: Option<&str>,
234 header: &'static str,
235) -> Result<[u8; N], SignedHttpError> {
236 let bytes = URL_SAFE_NO_PAD
237 .decode(required(encoded, header)?)
238 .map_err(|source| SignedHttpError::InvalidBase64 { header, source })?;
239 bytes
240 .as_slice()
241 .try_into()
242 .map_err(|_| SignedHttpError::InvalidLength {
243 header,
244 actual: bytes.len(),
245 expected: N,
246 })
247}
248
249fn verify_signature(
250 public_key: [u8; 32],
251 message: &[u8],
252 signature: [u8; 64],
253 identity: String,
254) -> Result<(), SignedHttpError> {
255 let public_key = VerifyingKey::from_bytes(&public_key)
256 .map_err(|_| SignedHttpError::InvalidPublicKey { identity })?;
257 public_key
258 .verify_strict(message, &Signature::from_bytes(&signature))
259 .map_err(|_| SignedHttpError::InvalidSignature)
260}
261
262#[derive(Clone, Debug)]
263pub struct AllowedLeaders {
264 keys: HashMap<(String, u64), [u8; 32]>,
265 source_path: Option<PathBuf>,
266}
267
268impl AllowedLeaders {
269 pub fn from_path(path: impl AsRef<Path>) -> Result<Self, SignedHttpError> {
270 let path = path.as_ref();
271 let bytes = fs::read(path)?;
272 let file: AllowedLeadersFileV1 = serde_json::from_slice(&bytes)
273 .map_err(|error| SignedHttpError::InvalidAllowedLeadersFile(error.to_string()))?;
274 let mut allowed = Self::try_from(file)?;
275 allowed.source_path = Some(path.to_path_buf());
276 Ok(allowed)
277 }
278
279 pub fn source_path(&self) -> Option<&Path> {
280 self.source_path.as_deref()
281 }
282}
283
284impl LeaderKeyResolver for AllowedLeaders {
285 fn leader_public_key(&self, leader_id: &str, leader_key_id: u64) -> Option<[u8; 32]> {
286 self.keys
287 .get(&(leader_id.to_string(), leader_key_id))
288 .copied()
289 }
290}
291
292#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
293#[serde(deny_unknown_fields)]
294pub struct AllowedLeadersFileV1 {
295 pub version: u8,
296 pub leaders: Vec<AllowedLeaderFileV1>,
297}
298
299#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
300#[serde(deny_unknown_fields)]
301pub struct AllowedLeaderFileV1 {
302 pub leader_id: String,
303 pub keys: Vec<AllowedLeaderKeyFileV1>,
304}
305
306#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
307#[serde(deny_unknown_fields)]
308pub struct AllowedLeaderKeyFileV1 {
309 pub kid: u64,
310 pub public_key: String,
311}
312
313impl TryFrom<AllowedLeadersFileV1> for AllowedLeaders {
314 type Error = SignedHttpError;
315
316 fn try_from(file: AllowedLeadersFileV1) -> Result<Self, Self::Error> {
317 if file.version != 1 {
318 return Err(SignedHttpError::InvalidAllowedLeadersFile(format!(
319 "unsupported version {}, expected 1",
320 file.version
321 )));
322 }
323 let mut keys = HashMap::new();
324 for leader in file.leaders {
325 for key in leader.keys {
326 let public_key = decode_public_key(&key.public_key)?;
327 if keys
328 .insert((leader.leader_id.clone(), key.kid), public_key)
329 .is_some()
330 {
331 return Err(SignedHttpError::InvalidAllowedLeadersFile(format!(
332 "duplicate leader key leader_id={} kid={}",
333 leader.leader_id, key.kid
334 )));
335 }
336 }
337 }
338 Ok(Self {
339 keys,
340 source_path: None,
341 })
342 }
343}
344
345fn decode_public_key(value: &str) -> Result<[u8; 32], SignedHttpError> {
346 let raw = value.strip_prefix("0x").unwrap_or(value);
347 let bytes = hex::decode(raw).map_err(|error| {
348 SignedHttpError::InvalidAllowedLeadersFile(format!(
349 "invalid Ed25519 public key '{value}': {error}"
350 ))
351 })?;
352 bytes.as_slice().try_into().map_err(|_| {
353 SignedHttpError::InvalidAllowedLeadersFile(format!(
354 "invalid Ed25519 public key length {}, expected 32",
355 bytes.len()
356 ))
357 })
358}