canic_core/ops/
signature.rs

1//!
2//! ops::signature
3//!
4//! High-level wrapper around IC canister signatures.
5//!
6//! This allows a canister to sign arbitrary messages without holding any private key.
7//!
8//! Internally uses `ic_canister_sig_creation` and certified data to produce
9//! verifiable, subnet-backed canister signatures.
10//!
11//! For verification, see: [`ic-standalone-sig-verifier`](https://crates.io/crates/ic-standalone-sig-verifier).
12//!
13
14use crate::{
15    Error, ThisError, cdk::api::certified_data_set, ops::OpsError, types::Principal,
16    utils::serialize::deserialize,
17};
18use ic_canister_sig_creation::{
19    CanisterSigPublicKey, IC_ROOT_PUBLIC_KEY, hash_with_domain, parse_canister_sig_cbor,
20    signature_map::{CanisterSigInputs, LABEL_SIG, SignatureMap},
21};
22use ic_signature_verification::verify_canister_sig;
23use serde::de::DeserializeOwned;
24use std::cell::RefCell;
25
26thread_local! {
27    /// Transient signature map, kept in heap memory only.
28    /// Entries expire automatically after ~1 minute.
29    static SIGNATURES: RefCell<SignatureMap> = RefCell::new(SignatureMap::default());
30}
31
32///
33/// SignatureOpsError
34///
35
36#[derive(Debug, ThisError)]
37pub enum SignatureOpsError {
38    #[error("cannot parse signature")]
39    CannotParseSignature,
40
41    #[error("cannot parse tokens")]
42    CannotParseTokens,
43
44    #[error("invalid signature")]
45    InvalidSignature,
46}
47
48impl From<SignatureOpsError> for Error {
49    fn from(err: SignatureOpsError) -> Self {
50        OpsError::from(err).into()
51    }
52}
53
54///
55/// Prepares a canister signature for a given message and seed.
56///
57/// This updates the canister's `certified_data` to include the
58/// new root hash so that the IC subnet will certify it.
59///
60/// - `seed` should uniquely identify the logical key context.
61/// - `message` is the data being signed.
62///
63pub fn prepare(domain: &[u8], seed: &[u8], message: &[u8]) {
64    let sig_inputs = CanisterSigInputs {
65        domain,
66        seed,
67        message,
68    };
69
70    SIGNATURES.with_borrow_mut(|sigs| {
71        sigs.add_signature(&sig_inputs);
72    });
73
74    // Commit new certified root
75    SIGNATURES.with_borrow(|sigs| {
76        certified_data_set(hash_with_domain(LABEL_SIG, &sigs.root_hash()));
77    });
78}
79
80///
81/// Retrieves a prepared canister signature as CBOR-encoded bytes.
82///
83/// Returns `None` if the signature has expired or was never prepared.
84///
85/// This is intended for use in query calls.
86///
87#[must_use]
88pub fn get(domain: &[u8], seed: &[u8], message: &[u8]) -> Option<Vec<u8>> {
89    let sig_inputs = CanisterSigInputs {
90        domain,
91        seed,
92        message,
93    };
94
95    SIGNATURES.with_borrow(|sigs| sigs.get_signature_as_cbor(&sig_inputs, None).ok())
96}
97
98///
99/// High-level convenience helper that combines [`prepare`] and [`get`]
100/// in one call. Suitable for simple use-cases where you don’t split update/query.
101///
102#[must_use]
103pub fn sign(domain: &[u8], seed: &[u8], message: &[u8]) -> Option<Vec<u8>> {
104    prepare(domain, seed, message);
105    get(domain, seed, message)
106}
107
108///
109/// Verify a user token that was issued by the auth canister.
110/// Callers must pass the domain separator and seed that were used during signing.
111///
112/// - `domain`:    the domain separator used during signing
113/// - `seed`:      the seed that derived the signing public key
114/// - `message`: the CBOR-encoded message Token
115/// - `signature`:  the CBOR canister signature returned by auth
116/// - `issuer_pid`: the Principal of the auth canister (the one that signed)
117///
118pub fn verify(
119    domain: &[u8],
120    seed: &[u8],
121    message: &[u8],
122    signature_cbor: &[u8],
123    issuer_pid: Principal,
124) -> Result<(), Error> {
125    // 1️⃣ Parse CBOR
126    parse_canister_sig_cbor(signature_cbor).map_err(|_| SignatureOpsError::CannotParseSignature)?;
127
128    // 2️⃣ Verify the IC canister signature cryptographically
129    let public_key = CanisterSigPublicKey::new(issuer_pid, seed.to_vec()).to_der();
130    let domain_prefixed_message = domain_prefixed_message(domain, message);
131    verify_canister_sig(
132        &domain_prefixed_message,
133        signature_cbor,
134        &public_key,
135        &IC_ROOT_PUBLIC_KEY,
136    )
137    .map_err(|_| SignatureOpsError::InvalidSignature)?;
138
139    Ok(())
140}
141
142///
143/// Parses CBOR-encoded message bytes into a strongly-typed value `T`.
144///
145/// This is a thin convenience wrapper over [`deserialize`], ensuring that
146/// all token deserialization uses the same canonical CBOR implementation.
147///
148pub fn parse_message<T>(message: &[u8]) -> Result<T, Error>
149where
150    T: DeserializeOwned,
151{
152    let token = deserialize::<T>(message).map_err(|_| SignatureOpsError::CannotParseTokens)?;
153
154    Ok(token)
155}
156
157///
158/// Returns the canister’s current signature root hash.
159/// Useful for debugging or introspection.
160///
161#[must_use]
162pub fn root_hash() -> Vec<u8> {
163    SIGNATURES.with_borrow(|sigs| sigs.root_hash().to_vec())
164}
165
166#[allow(clippy::cast_possible_truncation)]
167fn domain_prefixed_message(domain: &[u8], message: &[u8]) -> Vec<u8> {
168    // Mirror the preimage hashed by `hash_with_domain`.
169    let mut buf = Vec::with_capacity(1 + domain.len() + message.len());
170    buf.push(domain.len() as u8);
171    buf.extend_from_slice(domain);
172    buf.extend_from_slice(message);
173    buf
174}
175
176///
177/// TESTS
178///
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183    use candid::Principal;
184    use sha2::{Digest, Sha256};
185
186    const TEST_SIGNING_CANISTER_ID: &str = "rwlgt-iiaaa-aaaaa-aaaaa-cai";
187    const TEST_DOMAIN: &[u8] = b"toko";
188    const TEST_SEED: &[u8] = b"user-auth";
189    const CANISTER_SIG_CBOR: &[u8; 265] = b"\xd9\xd9\xf7\xa2\x6b\x63\x65\x72\x74\x69\x66\x69\x63\x61\x74\x65\x58\xa1\xd9\xd9\xf7\xa2\x64\x74\x72\x65\x65\x83\x01\x83\x02\x48\x63\x61\x6e\x69\x73\x74\x65\x72\x83\x02\x4a\x00\x00\x00\x00\x00\x00\x00\x01\x01\x01\x83\x02\x4e\x63\x65\x72\x74\x69\x66\x69\x65\x64\x5f\x64\x61\x74\x61\x82\x03\x58\x20\xa9\xea\x05\x9d\xf2\x7a\x09\x7e\xc4\x38\xdb\x35\x62\xb9\x55\xc3\xd3\xfa\x08\xeb\x17\xc1\x3c\xda\x63\x90\x42\xfa\xe0\xcf\x60\x36\x83\x02\x44\x74\x69\x6d\x65\x82\x03\x43\x87\xad\x4b\x69\x73\x69\x67\x6e\x61\x74\x75\x72\x65\x58\x30\xa4\xd5\xfd\x47\xa0\x88\x13\x5b\xed\x52\x22\x0c\xca\xa4\x76\xfb\x6c\x88\x95\xdd\xa3\x1e\x2a\x86\xa7\xa2\x97\xdc\x7a\x30\x81\x27\x1e\xf1\x1a\xee\xb5\xd2\xbb\x25\x83\x0d\xcb\xdd\x82\xad\x7a\x52\x64\x74\x72\x65\x65\x83\x02\x43\x73\x69\x67\x83\x02\x58\x20\x00\x42\xcd\x04\x7a\xad\x32\x06\x37\xce\xae\xe2\x1d\x48\x9e\xf4\xe5\x14\xce\x20\x1f\x19\x60\x68\x30\xa2\xaf\x7b\x7d\x9c\x86\x7d\x83\x02\x58\x20\x14\x9b\x80\x95\x11\x98\x27\xcf\xea\x0a\xa6\x6e\x7b\x7f\x80\xe9\x13\xca\xef\xa3\x1a\x60\x6d\xe4\x02\x69\xc3\xd8\x6c\xfe\xa5\x8d\x82\x03\x40";
190
191    #[test]
192    fn domain_prefix_matches_hash_with_domain() {
193        let domain = b"domain";
194        let message = b"payload";
195
196        let preimage = domain_prefixed_message(domain, message);
197
198        let mut hasher = Sha256::new();
199        hasher.update(&preimage);
200        let digest: [u8; 32] = hasher.finalize().into();
201
202        assert_eq!(digest, hash_with_domain(domain, message));
203    }
204
205    #[test]
206    fn verify_handles_short_principal_without_panicking() {
207        let issuer_pid = Principal::from_text(TEST_SIGNING_CANISTER_ID).unwrap();
208        let err = verify(
209            TEST_DOMAIN,
210            TEST_SEED,
211            b"payload",
212            CANISTER_SIG_CBOR,
213            issuer_pid,
214        )
215        .expect_err("expected invalid signature, not success");
216        assert_eq!(err.to_string(), "invalid signature");
217    }
218}