canic_core/ops/
signature.rs1use 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 static SIGNATURES: RefCell<SignatureMap> = RefCell::new(SignatureMap::default());
30}
31
32#[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
54pub 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 SIGNATURES.with_borrow(|sigs| {
76 certified_data_set(hash_with_domain(LABEL_SIG, &sigs.root_hash()));
77 });
78}
79
80#[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#[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
108pub fn verify(
119 domain: &[u8],
120 seed: &[u8],
121 message: &[u8],
122 signature_cbor: &[u8],
123 issuer_pid: Principal,
124) -> Result<(), Error> {
125 parse_canister_sig_cbor(signature_cbor).map_err(|_| SignatureOpsError::CannotParseSignature)?;
127
128 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
142pub 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#[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 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#[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}