Skip to main content

heddle_api/
request_proof.rs

1//! Portable verification of the native CallContext request proof.
2//! Verification grants no resource authority and does not consume the nonce;
3//! the receiving host must durably reject replay before serving or writing.
4
5use ed25519_dalek::{Signature, VerifyingKey};
6
7use crate::{heddle::api::common::CallContext, v2::MethodDescriptor};
8
9pub const PROOF_WINDOW_MILLIS: u64 = 60_000;
10
11#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
12pub enum RequestProofError {
13    #[error("request and context operation IDs differ or are missing")]
14    OperationId,
15    #[error("request proof is missing, malformed, or outside its time window")]
16    InvalidProof,
17    #[error("request signing identity differs from verified capability key")]
18    Identity,
19    #[error("request signature is invalid")]
20    Signature,
21    #[error("request operation ID could not be decoded")]
22    RequestMetadata,
23}
24
25pub struct VerifiedRequestProof<'a> {
26    pub identity: &'a str,
27    pub nonce: &'a [u8],
28}
29
30/// Verify the exact v2 method and protobuf request bytes against the key
31/// resolved from an independently verified, currently authorized capability.
32pub fn verify_native_request_proof<'a>(
33    context: &'a CallContext,
34    method: &'static MethodDescriptor,
35    body: &[u8],
36    effective_key: &[u8; 32],
37    now_millis: i64,
38) -> Result<VerifiedRequestProof<'a>, RequestProofError> {
39    let operation_id = method
40        .client_operation_id(body)
41        .map_err(|_| RequestProofError::RequestMetadata)?
42        .unwrap_or_default();
43    if operation_id != context.client_operation_id
44        || (method.client_operation_id_required && operation_id.is_empty())
45    {
46        return Err(RequestProofError::OperationId);
47    }
48    let proof = context
49        .request_proof
50        .as_ref()
51        .ok_or(RequestProofError::InvalidProof)?;
52    if proof.algorithm != "ed25519"
53        || proof.nonce.len() != 16
54        || now_millis.abs_diff(proof.timestamp_millis) > PROOF_WINDOW_MILLIS
55    {
56        return Err(RequestProofError::InvalidProof);
57    }
58    let identity = format!("principal:device-key:{}", hex::encode(effective_key));
59    if proof.signing_identity != identity {
60        return Err(RequestProofError::Identity);
61    }
62    let verifying_key =
63        VerifyingKey::from_bytes(effective_key).map_err(|_| RequestProofError::Signature)?;
64    let signature: [u8; 64] = proof
65        .signature
66        .as_slice()
67        .try_into()
68        .map_err(|_| RequestProofError::Signature)?;
69    verifying_key
70        .verify_strict(
71            &crate::signing::unary_bytes(
72                &identity,
73                method.path,
74                proof.timestamp_millis,
75                &proof.nonce,
76                body,
77            ),
78            &Signature::from_bytes(&signature),
79        )
80        .map_err(|_| RequestProofError::Signature)?;
81    Ok(VerifiedRequestProof {
82        identity: &proof.signing_identity,
83        nonce: &proof.nonce,
84    })
85}