canic_core/infra/ic/
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,
16    cdk::{
17        api::{certified_data_set, in_replicated_execution},
18        types::Principal,
19    },
20    infra::ic::IcInfraError,
21};
22use ic_canister_sig_creation::{
23    CanisterSigPublicKey, IC_ROOT_PUBLIC_KEY, hash_with_domain, parse_canister_sig_cbor,
24    signature_map::{CanisterSigInputs, LABEL_SIG, SignatureMap},
25};
26use ic_signature_verification::verify_canister_sig;
27use std::cell::RefCell;
28
29thread_local! {
30    /// Transient signature map, kept in heap memory only.
31    /// Entries expire automatically after ~1 minute.
32    static SIGNATURES: RefCell<SignatureMap> = RefCell::new(SignatureMap::default());
33}
34
35///
36/// SignatureOpsError
37///
38
39#[derive(Debug, ThisError)]
40pub enum SignatureOpsError {
41    #[error("cannot parse signature")]
42    CannotParseSignature,
43
44    #[error("invalid signature")]
45    InvalidSignature,
46}
47
48impl From<SignatureOpsError> for Error {
49    fn from(err: SignatureOpsError) -> Self {
50        IcInfraError::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/// - must be called from an update context
63///
64pub fn prepare(domain: &[u8], seed: &[u8], message: &[u8]) -> Result<(), Error> {
65    ensure_update_context()?;
66
67    let sig_inputs = CanisterSigInputs {
68        domain,
69        seed,
70        message,
71    };
72
73    SIGNATURES.with_borrow_mut(|sigs| {
74        sigs.add_signature(&sig_inputs);
75    });
76
77    // Commit new certified root
78    SIGNATURES.with_borrow(|sigs| {
79        certified_data_set(hash_with_domain(LABEL_SIG, &sigs.root_hash()));
80    });
81
82    Ok(())
83}
84
85///
86/// Retrieves a prepared canister signature as CBOR-encoded bytes.
87///
88/// Returns `None` if the signature has expired or was never prepared.
89///
90/// This is intended for use in query calls.
91///
92#[must_use]
93pub fn get(domain: &[u8], seed: &[u8], message: &[u8]) -> Option<Vec<u8>> {
94    let sig_inputs = CanisterSigInputs {
95        domain,
96        seed,
97        message,
98    };
99
100    SIGNATURES.with_borrow(|sigs| sigs.get_signature_as_cbor(&sig_inputs, None).ok())
101}
102
103///
104/// High-level convenience helper that combines [`prepare`] and [`get`]
105/// in one call. Suitable for simple use-cases where you don’t split update/query.
106///
107pub fn sign(domain: &[u8], seed: &[u8], message: &[u8]) -> Result<Option<Vec<u8>>, Error> {
108    prepare(domain, seed, message)?;
109
110    Ok(get(domain, seed, message))
111}
112
113///
114/// Verify a user token that was issued by the auth canister.
115/// Callers must pass the domain separator and seed that were used during signing.
116///
117/// - `domain`:    the domain separator used during signing
118/// - `seed`:      the seed that derived the signing public key
119/// - `message`: the CBOR-encoded message Token
120/// - `signature`:  the CBOR canister signature returned by auth
121/// - `issuer_pid`: the Principal of the auth canister (the one that signed)
122///
123pub fn verify(
124    domain: &[u8],
125    seed: &[u8],
126    message: &[u8],
127    signature_cbor: &[u8],
128    issuer_pid: Principal,
129) -> Result<(), Error> {
130    // 1️⃣ Parse CBOR
131    parse_canister_sig_cbor(signature_cbor).map_err(|_| SignatureOpsError::CannotParseSignature)?;
132
133    // 2️⃣ Verify the IC canister signature cryptographically
134    let public_key = CanisterSigPublicKey::new(issuer_pid, seed.to_vec()).to_der();
135    let domain_prefixed_message = domain_prefixed_message(domain, message);
136    verify_canister_sig(
137        &domain_prefixed_message,
138        signature_cbor,
139        &public_key,
140        &IC_ROOT_PUBLIC_KEY,
141    )
142    .map_err(|_| SignatureOpsError::InvalidSignature)?;
143
144    Ok(())
145}
146
147///
148/// Returns the canister’s current signature root hash.
149/// Useful for debugging or introspection.
150///
151#[must_use]
152pub fn root_hash() -> Vec<u8> {
153    SIGNATURES.with_borrow(|sigs| sigs.root_hash().to_vec())
154}
155
156#[allow(clippy::cast_possible_truncation)]
157fn domain_prefixed_message(domain: &[u8], message: &[u8]) -> Vec<u8> {
158    // Mirror the preimage hashed by `hash_with_domain`.
159    let mut buf = Vec::with_capacity(1 + domain.len() + message.len());
160    buf.push(domain.len() as u8);
161    buf.extend_from_slice(domain);
162    buf.extend_from_slice(message);
163    buf
164}
165
166fn ensure_update_context() -> Result<(), Error> {
167    if in_replicated_execution() {
168        return Ok(());
169    }
170
171    Err(Error::custom(
172        "signature preparation must be called from an update context",
173    ))
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}