heddle_thread_api/
request_proof.rs1use api::{heddle::api::common::CallContext, v2::MethodDescriptor};
5
6use crate::transport::Error;
7
8pub const PROOF_WINDOW_MILLIS: u64 = api::request_proof::PROOF_WINDOW_MILLIS;
9
10pub struct VerifiedProof<'a> {
11 identity: &'a str,
12 nonce: &'a [u8],
13}
14impl VerifiedProof<'_> {
15 pub fn identity(&self) -> &str {
16 self.identity
17 }
18 pub fn nonce(&self) -> &[u8] {
19 self.nonce
20 }
21}
22
23pub fn verify<'a>(
24 context: &'a CallContext,
25 method: &'static MethodDescriptor,
26 body: &[u8],
27 effective_key: &[u8; 32],
28 now_millis: i64,
29) -> Result<VerifiedProof<'a>, Error> {
30 let verified = api::request_proof::verify_native_request_proof(
31 context,
32 method,
33 body,
34 effective_key,
35 now_millis,
36 )
37 .map_err(|error| match error {
38 api::request_proof::RequestProofError::OperationId => {
39 Error::Protocol("request and context operation IDs differ or are missing")
40 }
41 api::request_proof::RequestProofError::InvalidProof => {
42 Error::Protocol("invalid or expired request PoP")
43 }
44 api::request_proof::RequestProofError::Identity => {
45 Error::Protocol("request signing identity differs from Biscuit")
46 }
47 api::request_proof::RequestProofError::Signature => {
48 Error::Protocol("invalid request signature")
49 }
50 api::request_proof::RequestProofError::RequestMetadata => {
51 Error::Protocol("request operation ID could not be decoded")
52 }
53 })?;
54 Ok(VerifiedProof {
55 identity: verified.identity,
56 nonce: verified.nonce,
57 })
58}
59
60#[cfg(test)]
61mod tests {
62 use api::{heddle::api::common::RequestProof, v2::client::Rpc};
63 use crypto::{Ed25519Signer, Signer};
64 use prost::Message;
65
66 use super::*;
67
68 #[test]
69 fn proof_binds_exact_method_bytes_effective_key_time_and_operation_identity() {
70 let method = crate::rpc::ThreadServiceRenameThread::METHOD;
71 let signer = Ed25519Signer::from_seed(&[17; 32]).expect("signer");
72 let key: [u8; 32] = signer.public_key().try_into().expect("public key");
73 let body = crate::contract::RenameThreadRequest {
74 client_operation_id: "rename-1".into(),
75 ..Default::default()
76 }
77 .encode_to_vec();
78 let identity = format!("principal:device-key:{}", hex::encode(key));
79 let nonce = vec![2; 16];
80 let timestamp = 1_000_000;
81 let signature = signer
82 .sign(&api::signing::unary_bytes(
83 &identity,
84 method.path,
85 timestamp,
86 &nonce,
87 &body,
88 ))
89 .expect("signing");
90 let context = CallContext {
91 client_operation_id: "rename-1".into(),
92 request_proof: Some(RequestProof {
93 algorithm: "ed25519".into(),
94 signing_identity: identity.clone(),
95 nonce: nonce.clone(),
96 timestamp_millis: timestamp,
97 signature,
98 }),
99 ..Default::default()
100 };
101 let verified = verify(&context, method, &body, &key, timestamp).expect("valid PoP");
102 assert_eq!(verified.identity(), identity);
103 assert_eq!(verified.nonce(), nonce);
104 let mut changed = body.clone();
105 changed.extend([0xa0, 0x06, 1]); assert!(verify(&context, method, &changed, &key, timestamp).is_err());
107 assert!(
108 verify(
109 &context,
110 crate::rpc::ThreadServiceChangeLifecycle::METHOD,
111 &body,
112 &key,
113 timestamp
114 )
115 .is_err()
116 );
117 assert!(verify(&context, method, &body, &[8; 32], timestamp).is_err());
118 assert!(verify(&context, method, &body, &key, timestamp + 60_001).is_err());
119 assert!(verify(&context, method, &body, &key, timestamp - 60_001).is_err());
120 let mut wrong_context = context.clone();
121 wrong_context.client_operation_id = "different".into();
122 assert!(verify(&wrong_context, method, &body, &key, timestamp).is_err());
123 wrong_context = context;
124 wrong_context
125 .request_proof
126 .as_mut()
127 .expect("proof")
128 .signing_identity = "principal:mutable-handle".into();
129 assert!(verify(&wrong_context, method, &body, &key, timestamp).is_err());
130 }
131}