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