diff --git a/Cargo.toml b/Cargo.toml index f64cc75..11adda8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ opt-level = 's' version = "0.1.0" edition = "2021" repository = "https://github.com/ldclabs/ic-cose" -keywords = ["config", "cbor", "canister", "icp"] +keywords = ["config", "cbor", "canister", "icp", "encryption"] categories = ["web-programming"] license = "MIT OR Apache-2.0" @@ -49,5 +49,5 @@ crc32fast = "1.4" url = "2.5" once_cell = "1.19" getrandom = { version = "0.2", features = ["custom"] } -coset = { git = "https://github.com/ldclabs/coset.git", rev = "50583c5df52b653eb9b17c2940ad4b7fe3d67ff1" } +coset = "0.3.8" aes-gcm = "0.10" diff --git a/README.md b/README.md index da80212..a84036c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ic-cose +# IC-COSE ⚙️ A decentralized COnfiguration service with Signing and Encryption on the Internet Computer. ## Overview diff --git a/src/ic_cose_canister/src/api_crypto.rs b/src/ic_cose_canister/src/api_crypto.rs index 1e7d3fc..d3a3225 100644 --- a/src/ic_cose_canister/src/api_crypto.rs +++ b/src/ic_cose_canister/src/api_crypto.rs @@ -1,4 +1,10 @@ -use ic_cose_types::{cose::*, crypto::ecdh_x25519, format_error, mac3_256}; +use ic_cose_types::{ + cose::{ + cose_aes256_key, ecdh::ecdh_x25519, encrypt0::cose_encrypt0, mac3_256, CborSerializable, + }, + format_error, + types::{ECDHInput, ECDHOutput, PublicKeyInput, SettingPathInput, SignInput}, +}; use serde_bytes::ByteBuf; use crate::{rand_bytes, store}; @@ -25,14 +31,6 @@ async fn schnorr_sign(_input: SignInput) -> Result { Err("not implemented".to_string()) } -// #[ic_cdk::update] -// async fn ecdh_public_key(path: SettingPathInput, ecdh: ECDHInput) -> Result, String> { -// path.validate()?; -// let caller = ic_cdk::caller(); -// let spk = store::SettingPathKey::from_path(path.into(), caller); -// store::ns::ecdh_public_key(&caller, &spk, &ecdh).await -// } - #[ic_cdk::update] async fn ecdh_encrypted_cose_key( path: SettingPathInput, diff --git a/src/ic_cose_canister/src/api_query.rs b/src/ic_cose_canister/src/api_query.rs index e33d3e6..cf4c4cb 100644 --- a/src/ic_cose_canister/src/api_query.rs +++ b/src/ic_cose_canister/src/api_query.rs @@ -1,4 +1,4 @@ -use ic_cose_types::{ +use ic_cose_types::types::{ namespace::NamespaceInfo, setting::{SettingInfo, SettingPath}, state::StateInfo, diff --git a/src/ic_cose_canister/src/api_update.rs b/src/ic_cose_canister/src/api_update.rs index 08ab6a6..e4c989f 100644 --- a/src/ic_cose_canister/src/api_update.rs +++ b/src/ic_cose_canister/src/api_update.rs @@ -1,5 +1,12 @@ use ic_cose_types::{ - cose::*, crypto::ecdh_x25519, format_error, mac3_256, setting::*, OwnedRef, MILLISECONDS, + cose::{ + ecdh::ecdh_x25519, + encrypt0::{cose_decrypt0, cose_encrypt0}, + get_cose_key_secret, mac3_256, CborSerializable, CoseKey, + }, + format_error, + types::{setting::*, ECDHInput, ECDHOutput}, + OwnedRef, MILLISECONDS, }; use crate::{rand_bytes, store}; diff --git a/src/ic_cose_canister/src/lib.rs b/src/ic_cose_canister/src/lib.rs index 2251595..6c88223 100644 --- a/src/ic_cose_canister/src/lib.rs +++ b/src/ic_cose_canister/src/lib.rs @@ -1,5 +1,5 @@ use candid::Principal; -use ic_cose_types::{cose::*, namespace::*, setting::*, state::StateInfo}; +use ic_cose_types::{types::namespace::*, types::setting::*, types::state::StateInfo, types::*}; use serde_bytes::ByteBuf; use std::collections::BTreeSet; diff --git a/src/ic_cose_canister/src/store.rs b/src/ic_cose_canister/src/store.rs index f76805f..da0744e 100644 --- a/src/ic_cose_canister/src/store.rs +++ b/src/ic_cose_canister/src/store.rs @@ -1,7 +1,8 @@ use candid::Principal; use ciborium::{from_reader, from_reader_with_buffer, into_writer}; use ic_cose_types::{ - cose::try_decode_encrypt0, namespace::NamespaceInfo, setting::*, sha3_256_n, state::StateInfo, + cose::{encrypt0::try_decode_encrypt0, sha3_256_n}, + types::{namespace::NamespaceInfo, setting::*, state::StateInfo}, ByteN, }; use ic_stable_structures::{ @@ -245,7 +246,7 @@ impl SettingPathKey { pub fn from_path(val: SettingPath, caller: Principal) -> Self { Self( val.ns, - if val.client { 1 } else { 0 }, + if val.client_owned { 1 } else { 0 }, val.subject.unwrap_or(caller), val.key, val.version, diff --git a/src/ic_cose_types/Cargo.toml b/src/ic_cose_types/Cargo.toml index 4d2fa12..ed08748 100644 --- a/src/ic_cose_types/Cargo.toml +++ b/src/ic_cose_types/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ic_cose_types" -description = "A Rust types library used for integrating with ic-cose cluster." +description = "A Rust types library used for integrating with IC-COSE." publish = true repository = "https://github.com/ldclabs/ic-cose/tree/main/src/ic_cose_types" version.workspace = true diff --git a/src/ic_cose_types/src/cose.rs b/src/ic_cose_types/src/cose.rs deleted file mode 100644 index f1e3be4..0000000 --- a/src/ic_cose_types/src/cose.rs +++ /dev/null @@ -1,143 +0,0 @@ -use candid::{CandidType, Principal}; -use coset::{ - CoseEncrypt0, CoseEncrypt0Builder, CoseKeyBuilder, HeaderBuilder, Label, RegisteredLabel, - TaggedCborSerializable, -}; -use serde::{Deserialize, Serialize}; -use serde_bytes::ByteBuf; - -use crate::{ - crypto::{aes256_gcm_decrypt, aes256_gcm_encrypt}, - format_error, skip_prefix, validate_key, ByteN, -}; - -pub use coset::{iana, CborSerializable, CoseKey}; - -pub const CBOR_TAG: [u8; 3] = [0xd9, 0xd9, 0xf7]; -pub const ENCRYPT0_TAG: [u8; 1] = [0xd0]; -pub const SIGN1_TAG: [u8; 1] = [0xd2]; - -#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] -pub struct PublicKeyInput { - pub ns: String, - pub derivation_path: Vec, - pub algorithm: Option, -} - -#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] -pub struct SignInput { - pub ns: String, - pub derivation_path: Vec, - pub message: ByteBuf, - pub algorithm: Option, -} - -#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] -pub struct ECDHInput { - pub nonce: ByteN<12>, // should be random for each request - pub public_key: ByteN<32>, // client side ECDH public key - pub partial_key: Option>, // should provide for encrypted payload with BYOK -} - -#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] -pub struct ECDHOutput { - pub payload: T, // should be random for each request - pub public_key: ByteN<32>, // server side ECDH public key -} - -#[derive(CandidType, Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] -pub struct SettingPathInput { - pub ns: String, - pub client: bool, - pub subject: Option, // default is caller - pub key: Option, -} - -impl SettingPathInput { - pub fn validate(&self) -> Result<(), String> { - validate_key(&self.ns)?; - if let Some(ref key) = self.key { - validate_key(key)?; - } - Ok(()) - } -} - -pub fn try_decode_encrypt0(payload: &[u8]) -> Result { - CoseEncrypt0::from_slice(skip_prefix(&ENCRYPT0_TAG, payload)).map_err(format_error) -} - -pub fn cose_encrypt0( - payload: &[u8], // plain payload - secret: &[u8; 32], - aad: &[u8], - nonce: [u8; 12], -) -> Result { - let protected = HeaderBuilder::new() - .algorithm(iana::Algorithm::A256GCM) - .build(); - let unprotected = HeaderBuilder::new().iv(nonce.to_vec()); - - let e0 = CoseEncrypt0Builder::new() - .protected(protected) - .unprotected(unprotected.build()) - .create_ciphertext(payload, aad, |plain_data, enc| { - aes256_gcm_encrypt(secret, &nonce, enc, plain_data).unwrap() - }) - .build(); - let payload = e0.to_tagged_vec().map_err(format_error)?; - Ok(ByteBuf::from(payload)) -} - -pub fn cose_decrypt0( - payload: &[u8], // COSE_Encrypt0 item - secret: &[u8; 32], - aad: &[u8], -) -> Result { - let e0 = CoseEncrypt0::from_slice(skip_prefix(&ENCRYPT0_TAG, payload)).map_err(format_error)?; - let nonce = e0.unprotected.iv.first_chunk::<12>().ok_or_else(|| { - format!( - "invalid nonce length, expected 12, got {}", - e0.unprotected.iv.len() - ) - })?; - let plain_data = e0.decrypt(aad, |cipher_data, enc| { - aes256_gcm_decrypt(secret, nonce, enc, cipher_data) - })?; - Ok(ByteBuf::from(plain_data)) -} - -pub fn cose_aes256_key(secret: [u8; 32]) -> CoseKey { - CoseKeyBuilder::new_symmetric_key(secret.into()) - .algorithm(iana::Algorithm::A256GCM) - .build() -} - -pub fn get_cose_key_secret(key: CoseKey) -> Result<[u8; 32], String> { - let key_label = match key.kty { - RegisteredLabel::Assigned(iana::KeyType::Symmetric) => { - Label::Int(iana::SymmetricKeyParameter::K as i64) - } - RegisteredLabel::Assigned(iana::KeyType::OKP) => { - Label::Int(iana::OkpKeyParameter::D as i64) - } - RegisteredLabel::Assigned(iana::KeyType::EC2) => { - Label::Int(iana::Ec2KeyParameter::D as i64) - } - _ => { - return Err("unsupport key type".to_string()); - } - }; - - for (label, value) in key.params { - if label == key_label { - let val: [u8; 32] = value - .into_bytes() - .map_err(|_| "invalid secret key".to_string())? - .try_into() - .map_err(|_| "invalid secret key".to_string())?; - return Ok(val); - } - } - Err("missing secret key".to_string()) -} diff --git a/src/ic_cose_types/src/crypto.rs b/src/ic_cose_types/src/cose/aes.rs similarity index 76% rename from src/ic_cose_types/src/crypto.rs rename to src/ic_cose_types/src/cose/aes.rs index 6733e37..94594dd 100644 --- a/src/ic_cose_types/src/crypto.rs +++ b/src/ic_cose_types/src/cose/aes.rs @@ -1,17 +1,7 @@ use aes_gcm::{aead::KeyInit, AeadInPlace, Aes256Gcm, Key, Nonce}; -use x25519_dalek::{PublicKey, SharedSecret, StaticSecret}; use crate::format_error; -pub fn ecdh_x25519(secret: [u8; 32], their_public: [u8; 32]) -> (SharedSecret, PublicKey) { - let secret = StaticSecret::from(secret); - let public = PublicKey::from(&secret); - ( - secret.diffie_hellman(&PublicKey::from(their_public)), - public, - ) -} - pub fn aes256_gcm_encrypt( key: &[u8; 32], nonce: &[u8; 12], diff --git a/src/ic_cose_types/src/cose/cwt.rs b/src/ic_cose_types/src/cose/cwt.rs new file mode 100644 index 0000000..a567c58 --- /dev/null +++ b/src/ic_cose_types/src/cose/cwt.rs @@ -0,0 +1,42 @@ +use coset::{ + cwt::{ClaimName, ClaimsSet, Timestamp}, + iana, CborSerializable, +}; +use num_traits::ToPrimitive; + +const CLOCK_SKEW: i64 = 5 * 60; // 5 minutes +pub static SCOPE_NAME: ClaimName = ClaimName::Assigned(iana::CwtClaimName::Scope); + +pub fn cwt_from_cwt(data: &[u8], now_sec: i64) -> Result { + let claims = ClaimsSet::from_slice(data).map_err(|err| format!("invalid claims: {}", err))?; + if let Some(ref exp) = claims.expiration_time { + let exp = match exp { + Timestamp::WholeSeconds(v) => *v, + Timestamp::FractionalSeconds(v) => (*v).to_i64().unwrap_or_default(), + }; + if exp < now_sec - CLOCK_SKEW { + return Err("token expired".to_string()); + } + } + if let Some(ref nbf) = claims.not_before { + let nbf = match nbf { + Timestamp::WholeSeconds(v) => *v, + Timestamp::FractionalSeconds(v) => (*v).to_i64().unwrap_or_default(), + }; + if nbf > now_sec + CLOCK_SKEW { + return Err("token not yet valid".to_string()); + } + } + + Ok(claims) +} + +pub fn get_scope(claims: &ClaimsSet) -> Result { + let scope = claims + .rest + .iter() + .find(|(key, _)| key == &SCOPE_NAME) + .ok_or("missing scope")?; + let scope = scope.1.as_text().ok_or("invalid scope text")?; + Ok(scope.to_string()) +} diff --git a/src/ic_cose_types/src/cose/ecdh.rs b/src/ic_cose_types/src/cose/ecdh.rs new file mode 100644 index 0000000..f2afb72 --- /dev/null +++ b/src/ic_cose_types/src/cose/ecdh.rs @@ -0,0 +1,10 @@ +use x25519_dalek::{PublicKey, SharedSecret, StaticSecret}; + +pub fn ecdh_x25519(secret: [u8; 32], their_public: [u8; 32]) -> (SharedSecret, PublicKey) { + let secret = StaticSecret::from(secret); + let public = PublicKey::from(&secret); + ( + secret.diffie_hellman(&PublicKey::from(their_public)), + public, + ) +} diff --git a/src/ic_cose_types/src/cose/ecdsa.rs b/src/ic_cose_types/src/cose/ecdsa.rs new file mode 100644 index 0000000..a44cc89 --- /dev/null +++ b/src/ic_cose_types/src/cose/ecdsa.rs @@ -0,0 +1,29 @@ +use serde_bytes::ByteBuf; + +use super::sha256; +use crate::format_error; + +pub use k256::ecdsa::{ + signature::hazmat::{PrehashSigner, PrehashVerifier}, + Signature, SigningKey, VerifyingKey, +}; + +pub fn secp256k1_verify_any( + public_keys: &[ByteBuf], + message: &[u8], + signature: &[u8], +) -> Result<(), String> { + let keys: Vec = public_keys + .iter() + .map(|key| VerifyingKey::from_sec1_bytes(key).map_err(format_error)) + .collect::>()?; + let sig = Signature::try_from(signature).map_err(format_error)?; + let digest = sha256(message); + match keys + .iter() + .any(|key| key.verify_prehash(&digest, &sig).is_ok()) + { + true => Ok(()), + false => Err("secp256k1 signature verification failed".to_string()), + } +} diff --git a/src/ic_cose_types/src/cose/ed25519.rs b/src/ic_cose_types/src/cose/ed25519.rs new file mode 100644 index 0000000..a8b9ed0 --- /dev/null +++ b/src/ic_cose_types/src/cose/ed25519.rs @@ -0,0 +1,23 @@ +use crate::{format_error, ByteN}; + +pub use ed25519_dalek::{Signature, SigningKey, VerifyingKey}; + +pub fn ed25519_verify_any( + public_keys: &[ByteN<32>], + message: &[u8], + signature: &[u8], +) -> Result<(), String> { + let keys: Vec = public_keys + .iter() + .map(|key| VerifyingKey::from_bytes(key).map_err(format_error)) + .collect::>()?; + let sig = Signature::from_slice(signature).map_err(format_error)?; + + match keys + .iter() + .any(|key| key.verify_strict(message, &sig).is_ok()) + { + true => Ok(()), + false => Err("ed25519 signature verification failed".to_string()), + } +} diff --git a/src/ic_cose_types/src/cose/encrypt0.rs b/src/ic_cose_types/src/cose/encrypt0.rs new file mode 100644 index 0000000..54d9edc --- /dev/null +++ b/src/ic_cose_types/src/cose/encrypt0.rs @@ -0,0 +1,55 @@ +use coset::{ + iana, CborSerializable, CoseEncrypt0, CoseEncrypt0Builder, HeaderBuilder, + TaggedCborSerializable, +}; +use serde_bytes::ByteBuf; + +use super::{ + aes::{aes256_gcm_decrypt, aes256_gcm_encrypt}, + skip_prefix, ENCRYPT0_TAG, +}; +use crate::format_error; + +pub fn try_decode_encrypt0(payload: &[u8]) -> Result { + CoseEncrypt0::from_slice(skip_prefix(&ENCRYPT0_TAG, payload)).map_err(format_error) +} + +pub fn cose_encrypt0( + payload: &[u8], // plain payload + secret: &[u8; 32], + aad: &[u8], + nonce: [u8; 12], +) -> Result { + let protected = HeaderBuilder::new() + .algorithm(iana::Algorithm::A256GCM) + .build(); + let unprotected = HeaderBuilder::new().iv(nonce.to_vec()); + + let e0 = CoseEncrypt0Builder::new() + .protected(protected) + .unprotected(unprotected.build()) + .create_ciphertext(payload, aad, |plain_data, enc| { + aes256_gcm_encrypt(secret, &nonce, enc, plain_data).unwrap() + }) + .build(); + let payload = e0.to_tagged_vec().map_err(format_error)?; + Ok(ByteBuf::from(payload)) +} + +pub fn cose_decrypt0( + payload: &[u8], // COSE_Encrypt0 item + secret: &[u8; 32], + aad: &[u8], +) -> Result { + let e0 = CoseEncrypt0::from_slice(skip_prefix(&ENCRYPT0_TAG, payload)).map_err(format_error)?; + let nonce = e0.unprotected.iv.first_chunk::<12>().ok_or_else(|| { + format!( + "invalid nonce length, expected 12, got {}", + e0.unprotected.iv.len() + ) + })?; + let plain_data = e0.decrypt(aad, |cipher_data, enc| { + aes256_gcm_decrypt(secret, nonce, enc, cipher_data) + })?; + Ok(ByteBuf::from(plain_data)) +} diff --git a/src/ic_cose_types/src/cose/mod.rs b/src/ic_cose_types/src/cose/mod.rs new file mode 100644 index 0000000..a5f942e --- /dev/null +++ b/src/ic_cose_types/src/cose/mod.rs @@ -0,0 +1,94 @@ +use coset::{CoseKeyBuilder, Label, RegisteredLabel}; +use hmac::{Hmac, Mac}; +use sha3::{Digest, Sha3_256}; + +pub mod aes; +pub mod cwt; +pub mod ecdh; +pub mod ecdsa; +pub mod ed25519; +pub mod encrypt0; +pub mod sign1; + +use crate::ByteN; + +pub use coset::{iana, CborSerializable, CoseKey}; + +pub const CBOR_TAG: [u8; 3] = [0xd9, 0xd9, 0xf7]; +pub const ENCRYPT0_TAG: [u8; 1] = [0xd0]; +pub const SIGN1_TAG: [u8; 1] = [0xd2]; + +pub fn crc32(data: &[u8]) -> u32 { + let mut h = crc32fast::Hasher::new(); + h.update(data); + h.finalize() +} + +pub fn sha256(data: &[u8]) -> [u8; 32] { + let mut hasher = sha2::Sha256::new(); + hasher.update(data); + hasher.finalize().into() +} + +pub fn sha3_256(data: &[u8]) -> [u8; 32] { + let mut hasher = Sha3_256::new(); + hasher.update(data); + hasher.finalize().into() +} + +pub fn sha3_256_n(array: [&[u8]; N]) -> [u8; 32] { + let mut hasher = Sha3_256::new(); + for data in array { + hasher.update(data); + } + hasher.finalize().into() +} + +pub fn mac3_256(key: &[u8], data: &[u8]) -> [u8; 32] { + let mut mac = Hmac::::new_from_slice(key).expect("HMAC can take key of any size"); + mac.update(data); + mac.finalize().into_bytes().into() +} + +pub fn skip_prefix<'a>(tag: &'a [u8], data: &'a [u8]) -> &'a [u8] { + if data.starts_with(tag) { + &data[tag.len()..] + } else { + data + } +} + +pub fn cose_aes256_key(secret: [u8; 32]) -> CoseKey { + CoseKeyBuilder::new_symmetric_key(secret.into()) + .algorithm(iana::Algorithm::A256GCM) + .build() +} + +pub fn get_cose_key_secret(key: CoseKey) -> Result<[u8; 32], String> { + let key_label = match key.kty { + RegisteredLabel::Assigned(iana::KeyType::Symmetric) => { + Label::Int(iana::SymmetricKeyParameter::K as i64) + } + RegisteredLabel::Assigned(iana::KeyType::OKP) => { + Label::Int(iana::OkpKeyParameter::D as i64) + } + RegisteredLabel::Assigned(iana::KeyType::EC2) => { + Label::Int(iana::Ec2KeyParameter::D as i64) + } + _ => { + return Err("unsupport key type".to_string()); + } + }; + + for (label, value) in key.params { + if label == key_label { + let val: [u8; 32] = value + .into_bytes() + .map_err(|_| "invalid secret key".to_string())? + .try_into() + .map_err(|_| "invalid secret key".to_string())?; + return Ok(val); + } + } + Err("missing secret key".to_string()) +} diff --git a/src/ic_cose_types/src/cose/sign1.rs b/src/ic_cose_types/src/cose/sign1.rs new file mode 100644 index 0000000..60b7a14 --- /dev/null +++ b/src/ic_cose_types/src/cose/sign1.rs @@ -0,0 +1,48 @@ +use coset::{iana, Algorithm, CborSerializable, CoseSign1, CoseSign1Builder, HeaderBuilder}; +use serde_bytes::ByteBuf; + +use super::{ecdsa::secp256k1_verify_any, ed25519::ed25519_verify_any, ByteN}; + +pub use iana::Algorithm::{EdDSA, ES256K}; +const ALG_ED25519: Algorithm = Algorithm::Assigned(EdDSA); +const ALG_SECP256K1: Algorithm = Algorithm::Assigned(ES256K); + +/// algorithm: EdDSA | ES256K +pub fn cose_sign1( + payload: Vec, + alg: iana::Algorithm, + key_id: Option>, +) -> Result { + let mut protected = HeaderBuilder::new().algorithm(alg); + if let Some(key_id) = key_id { + protected = protected.key_id(key_id); + } + + Ok(CoseSign1Builder::new() + .protected(protected.build()) + .payload(payload) + .build()) +} + +pub fn cose_sign1_from( + sign1_bytes: &[u8], + aad: &[u8], + secp256k1_pub_keys: &[ByteBuf], + ed25519_pub_keys: &[ByteN<32>], +) -> Result { + let cs1 = CoseSign1::from_slice(sign1_bytes) + .map_err(|err| format!("invalid COSE sign1 token: {}", err))?; + + match &cs1.protected.header.alg { + Some(ALG_SECP256K1) if !secp256k1_pub_keys.is_empty() => { + secp256k1_verify_any(secp256k1_pub_keys, &cs1.tbs_data(aad), &cs1.signature)?; + } + Some(ALG_ED25519) if !ed25519_pub_keys.is_empty() => { + ed25519_verify_any(ed25519_pub_keys, &cs1.tbs_data(aad), &cs1.signature)?; + } + alg => { + Err(format!("unsupported algorithm: {:?}", alg))?; + } + } + Ok(cs1) +} diff --git a/src/ic_cose_types/src/lib.rs b/src/ic_cose_types/src/lib.rs index 518af8a..d2d201d 100644 --- a/src/ic_cose_types/src/lib.rs +++ b/src/ic_cose_types/src/lib.rs @@ -3,29 +3,18 @@ use candid::Principal; use ciborium::into_writer; -use hmac::{Hmac, Mac}; use serde::Serialize; -use sha3::{Digest, Sha3_256}; -use std::{ - collections::{BTreeMap, BTreeSet}, - ops::Deref, -}; +use std::{collections::BTreeSet, ops::Deref}; +pub mod bytes; pub mod cose; -pub mod crypto; -pub mod namespace; -pub mod setting; -pub mod state; +pub mod types; -mod bytes; pub use bytes::*; pub static ANONYMOUS: Principal = Principal::anonymous(); pub const MILLISECONDS: u64 = 1_000_000u64; -// should update to ICRC3Map -pub type MapValue = BTreeMap; - pub fn format_error(err: T) -> String where T: std::fmt::Debug, @@ -33,32 +22,6 @@ where format!("{:?}", err) } -pub fn crc32(data: &[u8]) -> u32 { - let mut h = crc32fast::Hasher::new(); - h.update(data); - h.finalize() -} - -pub fn sha3_256(data: &[u8]) -> [u8; 32] { - let mut hasher = Sha3_256::new(); - hasher.update(data); - hasher.finalize().into() -} - -pub fn sha3_256_n(array: [&[u8]; N]) -> [u8; 32] { - let mut hasher = Sha3_256::new(); - for data in array { - hasher.update(data); - } - hasher.finalize().into() -} - -pub fn mac3_256(key: &[u8], data: &[u8]) -> [u8; 32] { - let mut mac = Hmac::::new_from_slice(key).expect("HMAC can take key of any size"); - mac.update(data); - mac.finalize().into_bytes().into() -} - // to_cbor_bytes returns the CBOR encoding of the given object that implements the Serialize trait. pub fn to_cbor_bytes(obj: &impl Serialize) -> Vec { let mut buf: Vec = Vec::new(); @@ -66,14 +29,6 @@ pub fn to_cbor_bytes(obj: &impl Serialize) -> Vec { buf } -pub fn skip_prefix<'a>(tag: &'a [u8], data: &'a [u8]) -> &'a [u8] { - if data.starts_with(tag) { - &data[tag.len()..] - } else { - data - } -} - /// Validates the key of Namespace and Setting /// /// # Arguments diff --git a/src/ic_cose_types/src/types/mod.rs b/src/ic_cose_types/src/types/mod.rs new file mode 100644 index 0000000..13cd1ff --- /dev/null +++ b/src/ic_cose_types/src/types/mod.rs @@ -0,0 +1,59 @@ +use candid::{CandidType, Principal}; +use serde::{Deserialize, Serialize}; +use serde_bytes::ByteBuf; +use std::collections::BTreeMap; + +pub mod namespace; +pub mod setting; +pub mod state; + +use crate::{bytes::ByteN, validate_key}; + +// should update to ICRC3Map +pub type MapValue = BTreeMap; + +#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] +pub struct PublicKeyInput { + pub ns: String, + pub derivation_path: Vec, + pub algorithm: Option, +} + +#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] +pub struct SignInput { + pub ns: String, + pub derivation_path: Vec, + pub message: ByteBuf, + pub algorithm: Option, +} + +#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] +pub struct ECDHInput { + pub nonce: ByteN<12>, // should be random for each request + pub public_key: ByteN<32>, // client side ECDH public key + pub partial_key: Option>, // should provide for encrypted payload with BYOK +} + +#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] +pub struct ECDHOutput { + pub payload: T, // should be random for each request + pub public_key: ByteN<32>, // server side ECDH public key +} + +#[derive(CandidType, Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +pub struct SettingPathInput { + pub ns: String, + pub client_owned: bool, + pub subject: Option, // default is caller + pub key: Option, +} + +impl SettingPathInput { + pub fn validate(&self) -> Result<(), String> { + validate_key(&self.ns)?; + if let Some(ref key) = self.key { + validate_key(key)?; + } + Ok(()) + } +} diff --git a/src/ic_cose_types/src/namespace.rs b/src/ic_cose_types/src/types/namespace.rs similarity index 100% rename from src/ic_cose_types/src/namespace.rs rename to src/ic_cose_types/src/types/namespace.rs diff --git a/src/ic_cose_types/src/setting.rs b/src/ic_cose_types/src/types/setting.rs similarity index 96% rename from src/ic_cose_types/src/setting.rs rename to src/ic_cose_types/src/types/setting.rs index 04652e0..8cf22d1 100644 --- a/src/ic_cose_types/src/setting.rs +++ b/src/ic_cose_types/src/types/setting.rs @@ -4,7 +4,8 @@ use serde::{Deserialize, Serialize}; use serde_bytes::ByteBuf; use std::collections::{BTreeMap, BTreeSet}; -use crate::{cose::SettingPathInput, format_error, validate_key}; +use super::SettingPathInput; +use crate::{format_error, validate_key}; pub const CHUNK_SIZE: u32 = 256 * 1024; @@ -26,7 +27,7 @@ pub struct SettingInfo { #[derive(CandidType, Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] pub struct SettingPath { pub ns: String, - pub client: bool, + pub client_owned: bool, pub subject: Option, pub key: String, pub version: u32, @@ -44,7 +45,7 @@ impl From for SettingPath { fn from(input: SettingPathInput) -> Self { Self { ns: input.ns, - client: input.client, + client_owned: input.client_owned, subject: input.subject, key: input.key.unwrap_or_default(), version: 0, diff --git a/src/ic_cose_types/src/state.rs b/src/ic_cose_types/src/types/state.rs similarity index 100% rename from src/ic_cose_types/src/state.rs rename to src/ic_cose_types/src/types/state.rs