Skip to main content

nexus_sdk/signed_http/v2/
wire.rs

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}