canic_core/infra/ic/
signature.rs1use 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 static SIGNATURES: RefCell<SignatureMap> = RefCell::new(SignatureMap::default());
33}
34
35#[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
54pub 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 SIGNATURES.with_borrow(|sigs| {
79 certified_data_set(hash_with_domain(LABEL_SIG, &sigs.root_hash()));
80 });
81
82 Ok(())
83}
84
85#[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
103pub 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
113pub fn verify(
124 domain: &[u8],
125 seed: &[u8],
126 message: &[u8],
127 signature_cbor: &[u8],
128 issuer_pid: Principal,
129) -> Result<(), Error> {
130 parse_canister_sig_cbor(signature_cbor).map_err(|_| SignatureOpsError::CannotParseSignature)?;
132
133 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#[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 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#[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}