| 1 | |
|
| 2 | |
|
| 3 | |
|
| 4 | |
|
| 5 | |
|
| 6 | |
|
| 7 | |
|
| 8 | |
|
| 9 | |
|
| 10 | |
|
| 11 | |
|
| 12 | |
|
| 13 | | use crate::cbor::{self, Value}; |
| 14 | | use crate::identity::{Identity, Signature}; |
| 15 | | use crate::keys::KeyPair; |
| 16 | | use rand::RngCore; |
| 17 | |
|
| 18 | |
|
| 19 | | pub const WINDOW_MS: u64 = 5 * 60 * 1000; |
| 20 | |
|
| 21 | |
|
| 22 | | #[derive(Clone, Debug, PartialEq, Eq)] |
| 23 | | pub struct Proof { |
| 24 | | pub sig: Signature, |
| 25 | | pub method: String, |
| 26 | | pub body_hash: [u8; 32], |
| 27 | | pub time_ms: u64, |
| 28 | | pub nonce: [u8; 16], |
| 29 | | } |
| 30 | |
|
| 31 | | impl Proof { |
| 32 | | |
| 33 | 329× | pub fn sign(pair: &KeyPair, method: &str, body: &[u8]) -> Proof { |
| 34 | 329× | let mut nonce = [0u8; 16]; |
| 35 | 329× | rand::rngs::OsRng.fill_bytes(&mut nonce); |
| 36 | 329× | Proof::sign_at(pair, method, body, crate::ids::now_ms(), nonce) |
| 37 | 329× | } |
| 38 | |
|
| 39 | 330× | pub fn sign_at(pair: &KeyPair, method: &str, body: &[u8], time_ms: u64, nonce: [u8; 16]) -> Proof { |
| 40 | 330× | let body_hash = *blake3::hash(body).as_bytes(); |
| 41 | 330× | let sig = pair.sign(&message(method, &body_hash, time_ms, &nonce)); |
| 42 | 330× | Proof { sig, method: method.into(), body_hash, time_ms, nonce } |
| 43 | 330× | } |
| 44 | |
|
| 45 | | |
| 46 | 1022× | pub fn key(&self) -> [u8; 32] { |
| 47 | 1022× | self.sig.key.0 |
| 48 | 1022× | } |
| 49 | |
|
| 50 | | |
| 51 | 921× | pub fn verify(&self, identity: &Identity, body: Option<&[u8]>) -> Result<(), String> { |
| 52 | 921× | if let Some(b) = body { |
| 53 | 875× | if *blake3::hash(b).as_bytes() != self.body_hash { |
| 54 | 3× | return Err("the signature covers a different body".into()); |
| 55 | 872× | } |
| 56 | 46× | } |
| 57 | 918× | let msg = message(&self.method, &self.body_hash, self.time_ms, &self.nonce); |
| 58 | 918× | crate::identity::verify(identity, &self.sig, &msg).map_err(|e| format!("signature does not verify: {e}")) |
| 59 | 921× | } |
| 60 | |
|
| 61 | 331× | pub fn encode(&self) -> Vec<u8> { |
| 62 | 331× | cbor::encode(&Value::map([ |
| 63 | 331× | (0, self.sig.to_value()), |
| 64 | 331× | (1, Value::Text(self.method.clone())), |
| 65 | 331× | (2, Value::Bytes(self.body_hash.to_vec())), |
| 66 | 331× | (3, Value::Uint(self.time_ms)), |
| 67 | 331× | (4, Value::Bytes(self.nonce.to_vec())), |
| 68 | 331× | ])) |
| 69 | 331× | } |
| 70 | |
|
| 71 | 808× | pub fn decode(b: &[u8]) -> Result<Proof, String> { |
| 72 | 808× | let v = cbor::decode(b).map_err(|e| format!("malformed proof: {e}"))?; |
| 73 | 808× | let m = v.as_map().ok_or("malformed proof")?; |
| 74 | 4040× | let get = |k: u64| m.get(&k).ok_or_else(|| format!("proof lacks field {k}")); |
| 75 | | Ok(Proof { |
| 76 | 808× | sig: Signature::from_value(get(0)?).map_err(|e| format!("malformed signature: {e}"))?, |
| 77 | 808× | method: get(1)?.as_text().ok_or("proof method")?.to_string(), |
| 78 | 808× | body_hash: get(2)?.as_bytes().and_then(|b| b.try_into().ok()).ok_or("proof body hash")?, |
| 79 | 808× | time_ms: get(3)?.as_uint().ok_or("proof time")?, |
| 80 | 808× | nonce: get(4)?.as_bytes().and_then(|b| b.try_into().ok()).ok_or("proof nonce")?, |
| 81 | | }) |
| 82 | 808× | } |
| 83 | | } |
| 84 | |
|
| 85 | |
|
| 86 | 1248× | pub fn message(method: &str, body_hash: &[u8; 32], time_ms: u64, nonce: &[u8; 16]) -> Vec<u8> { |
| 87 | 1248× | let mut m = b"spectra1 request ".to_vec(); |
| 88 | 1248× | m.extend_from_slice(method.as_bytes()); |
| 89 | 1248× | m.push(0); |
| 90 | 1248× | m.extend_from_slice(body_hash); |
| 91 | 1248× | m.extend_from_slice(&time_ms.to_be_bytes()); |
| 92 | 1248× | m.extend_from_slice(nonce); |
| 93 | 1248× | m |
| 94 | 1248× | } |
| 95 | |
|
| 96 | | #[cfg(test)] |
| 97 | | mod tests { |
| 98 | | use super::*; |
| 99 | |
|
| 100 | | #[test] |
| 101 | 1× | fn a_proof_round_trips_and_binds_key_method_body_and_time() { |
| 102 | 1× | let a = KeyPair::generate("a", ""); |
| 103 | 1× | let b = KeyPair::generate("b", ""); |
| 104 | 1× | let p = Proof::sign(&a, "POST /mcp", b"{\"x\":1}"); |
| 105 | 1× | let q = Proof::decode(&p.encode()).unwrap(); |
| 106 | 1× | assert_eq!(p, q); |
| 107 | 1× | q.verify(&a.identity, Some(b"{\"x\":1}")).unwrap(); |
| 108 | 1× | assert!(q.verify(&a.identity, Some(b"{\"x\":2}")).is_err(), "another body"); |
| 109 | 1× | assert!(q.verify(&b.identity, None).is_err(), "another key"); |
| 110 | 1× | let mut r = q.clone(); |
| 111 | 1× | r.method = "POST /api".into(); |
| 112 | 1× | assert!(r.verify(&a.identity, None).is_err(), "another method"); |
| 113 | 1× | let mut t = q.clone(); |
| 114 | 1× | t.time_ms += 1; |
| 115 | 1× | assert!(t.verify(&a.identity, None).is_err(), "another time"); |
| 116 | 1× | } |
| 117 | | } |