Skip to main content

nexus_sdk/signed_http/v3/
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_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}