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