Skip to main content

heddle_thread_api/
request_proof.rs

1//! Portable v2 request PoP. Hosts supply the effective key from an already
2//! verified Biscuit and separately enforce account attachment, scope, current
3//! revocation and durable nonce consumption. This check grants no authority.
4use 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]); // valid unknown protobuf field
106        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}