use base64::{
Engine,
engine::general_purpose::{URL_SAFE, URL_SAFE_NO_PAD},
};
use candid::{CandidType, Principal};
use ciborium::from_reader;
use http::header::{AUTHORIZATION, HeaderMap, HeaderName};
use ic_auth_types::{
ByteBufB64, DelegationCompact, SignedDelegation, SignedDelegationCompact,
deterministic_cbor_into_vec,
};
use ic_canister_sig_creation::delegation_signature_msg;
use serde::{Deserialize, Serialize};
#[cfg(feature = "identity")]
use ic_agent::{Identity, Signature};
use ic_canister_sig_creation::IC_ROOT_PK_DER;
use crate::{Algorithm, sha3_256, user_public_key_from_der, verify_basic_sig, verify_canister_sig};
// pub use ic_signature_verification::verify_canister_sig;
/// The Internet Computer's anonymous principal identifier.
/// This is used when no authenticated identity is provided.
pub const ANONYMOUS_PRINCIPAL: Principal = Principal::anonymous();
/// Maximum allowed time drift in milliseconds for delegation verification.
/// This prevents replay attacks while allowing for reasonable clock differences.
pub const PERMITTED_DRIFT_MS: u64 = 300 * 1000;
/// Domain separator for delegation signature messages.
pub const IC_REQUEST_AUTH_DELEGATION_DOMAIN_SEPARATOR: &[u8] = b"\x1Aic-request-auth-delegation";
/// HTTP header for the caller's public key used in authentication.
/// This header contains the base64url-encoded public key of the caller.
pub static HEADER_IC_AUTH_PUBKEY: HeaderName = HeaderName::from_static("ic-auth-pubkey");
/// HTTP header for the request content hash.
/// This header contains a base64url-encoded hash of the request content,
/// which can be customized based on business logic requirements.
pub static HEADER_IC_AUTH_CONTENT_DIGEST: HeaderName =
HeaderName::from_static("ic-auth-content-digest");
/// HTTP header for the signature of the content digest.
/// This header contains a base64url-encoded cryptographic signature
/// that proves the authenticity of the content digest.
pub static HEADER_IC_AUTH_SIGNATURE: HeaderName = HeaderName::from_static("ic-auth-signature");
/// HTTP header for the delegation chain used in authentication.
/// This header contains a base64url-encoded CBOR representation of the delegation chain
/// that connects the public key to the signature.
pub static HEADER_IC_AUTH_DELEGATION: HeaderName = HeaderName::from_static("ic-auth-delegation");
/// HTTP header for the authenticated user principal.
/// This header contains the textual representation of the authenticated user's principal ID,
/// or the anonymous principal if authentication fails or is not provided.
pub static HEADER_IC_AUTH_USER: HeaderName = HeaderName::from_static("ic-auth-user");
/// Verifies a signature using the public key and the mainnet IC root public key.
///
/// # Arguments
/// * `public_key` - The DER-encoded public key to verify against
/// * `msg` - The message that was signed
/// * `signature` - The signature to verify
/// * `current_time_ns` - The current time in nanoseconds since the Unix epoch, used for validating canister signatures with time-based constraints
///
/// # Returns
/// * `Ok(())` if the signature is valid
/// * `Err(String)` with an error message if verification fails
pub fn verify_sig(
public_key: &[u8],
msg: &[u8],
signature: &[u8],
current_time_ns: &u128,
) -> Result<(), String> {
verify_sig_with_rootkey(IC_ROOT_PK_DER, public_key, msg, signature, current_time_ns)
}
/// Verifies a signature using the public key and a specified IC root public key.
/// This function allows verification against different IC networks by providing
/// a custom root public key.
///
/// # Arguments
/// * `ic_root_public_key_raw` - The raw IC root public key to use for verification
/// * `public_key` - The DER-encoded public key to verify against
/// * `msg` - The message that was signed
/// * `signature` - The signature to verify
/// * `current_time_ns` - The current time in nanoseconds since the Unix epoch, used for validating canister signatures with time-based constraints
///
/// # Returns
/// * `Ok(())` if the signature is valid
/// * `Err(String)` with an error message if verification fails
pub fn verify_sig_with_rootkey(
ic_root_public_key_raw: &[u8],
public_key: &[u8],
msg: &[u8],
signature: &[u8],
current_time_ns: &u128,
) -> Result<(), String> {
let (alg, pk) = user_public_key_from_der(public_key)?;
match alg {
Algorithm::IcCanisterSignature => verify_canister_sig(
msg,
signature,
public_key,
ic_root_public_key_raw,
current_time_ns,
None,
),
_ => verify_basic_sig(alg, &pk, msg, signature),
}
}
/// The authentication envelope for Internet Computer authentication.
///
/// This structure encapsulates all the necessary components for authenticating
/// a request to an Internet Computer service. It includes the public key of the
/// sender, the signature, the content digest, and an optional delegation chain.
///
/// The envelope can be serialized to and from various formats, including bytes,
/// base64 strings, and HTTP headers, making it versatile for different transport
/// mechanisms.
#[derive(Clone, Debug, CandidType, Deserialize, Serialize)]
pub struct SignedEnvelope {
/// The public key of the self-authenticating principal this request is from.
/// This is the head of the delegation chain (if any) and is used to derive
/// the principal ID of the sender.
#[serde(rename = "p", alias = "pubkey", alias = "public_key")]
pub pubkey: ByteBufB64,
/// A cryptographic signature authorizing the request.
/// When delegations are involved, this is the signature from the tail of the
/// delegation chain, not necessarily made by the owner of `pubkey`.
#[serde(rename = "s", alias = "signature")]
pub signature: ByteBufB64,
/// The request content's hash digest that was signed by the sender.
/// This is typically a SHA-256 or SHA3-256 hash of the request content.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[serde(rename = "h", alias = "digest")]
pub digest: Option<ByteBufB64>,
/// The chain of delegations connecting `pubkey` to `signature`, in order.
/// Each delegation authorizes the next entity in the chain to sign on behalf
/// of the previous entity, forming a chain of trust from the original identity
/// to the actual signer.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[serde(rename = "d", alias = "delegation")]
pub delegation: Option<Vec<SignedDelegationCompact>>,
}
#[cfg(feature = "identity")]
impl TryFrom<Signature> for SignedEnvelope {
type Error = String;
fn try_from(sig: Signature) -> Result<Self, Self::Error> {
Ok(Self {
pubkey: sig
.public_key
.ok_or_else(|| "missing public_key".to_string())?
.into(),
signature: sig
.signature
.ok_or_else(|| "missing signature".to_string())?
.into(),
digest: None,
delegation: sig.delegations.map(|delegations| {
delegations
.into_iter()
.map(|d| SignedDelegationCompact {
delegation: DelegationCompact {
pubkey: d.delegation.pubkey.into(),
expiration: d.delegation.expiration,
targets: d.delegation.targets,
},
signature: d.signature.into(),
})
.collect::<Vec<_>>()
}),
})
}
}
impl SignedEnvelope {
/// Returns the sender's principal ID derived from the public key.
///
/// This computes a self-authenticating principal ID based on the public key
/// in the envelope, which uniquely identifies the sender.
///
/// # Returns
/// * `Principal` - The principal ID of the sender
pub fn sender(&self) -> Principal {
Principal::self_authenticating(&self.pubkey)
}
/// Encodes the SignedEnvelope into a binary representation.
///
/// # Returns
/// * `Vec<u8>` - The CBOR-encoded binary representation of the envelope
pub fn to_bytes(&self) -> Vec<u8> {
deterministic_cbor_into_vec(&self).expect("failed to encode SignedEnvelope")
}
/// Decodes a SignedEnvelope from its binary representation.
///
/// # Arguments
/// * `bytes` - The CBOR-encoded binary representation of the envelope
///
/// # Returns
/// * `Result<Self, String>` - The decoded envelope or an error message
pub fn from_bytes(bytes: &[u8]) -> Result<Self, String> {
from_reader(bytes).map_err(|err| format!("failed to decode SignedEnvelope data: {err:?}"))
}
/// Encodes the SignedEnvelope into a base64url string.
///
/// # Returns
/// * `String` - The base64url-encoded representation of the envelope
pub fn to_base64(&self) -> String {
URL_SAFE.encode(self.to_bytes())
}
/// Decodes a SignedEnvelope from a base64url string.
///
/// # Arguments
/// * `s` - The base64url-encoded string representation of the envelope
///
/// # Returns
/// * `Result<Self, String>` - The decoded envelope or an error message
pub fn from_base64(s: &str) -> Result<Self, String> {
decode_base64(s).and_then(|data| Self::from_bytes(&data))
}
/// Creates a SignedEnvelope by signing a message with the provided identity.
///
/// This computes the SHA3-256 hash of the message and signs it with the identity.
///
/// # Arguments
/// * `identity` - The identity to sign with, implementing the `Identity` trait
/// * `message` - The message to sign
///
/// # Returns
/// * `Result<Self, String>` - The signed envelope or an error message
#[cfg(feature = "identity")]
pub fn sign_message(identity: &impl Identity, message: &[u8]) -> Result<Self, String> {
Self::sign_digest(identity, sha3_256(message).into())
}
/// Creates a SignedEnvelope by signing a pre-computed digest with the provided identity.
///
/// # Arguments
/// * `identity` - The identity to sign with, implementing the `Identity` trait
/// * `digest` - The pre-computed digest to sign
///
/// # Returns
/// * `Result<Self, String>` - The signed envelope or an error message
#[cfg(feature = "identity")]
pub fn sign_digest(identity: &impl Identity, digest: Vec<u8>) -> Result<Self, String> {
let sig = identity
.sign_arbitrary(&digest)
.map_err(|err| format!("{:?}", err))?;
let envelope = Self {
pubkey: sig
.public_key
.ok_or_else(|| "missing public_key".to_string())?
.into(),
signature: sig
.signature
.ok_or_else(|| "missing signature".to_string())?
.into(),
digest: Some(digest.into()),
delegation: sig.delegations.map(|delegations| {
delegations
.into_iter()
.map(|d| SignedDelegationCompact {
delegation: DelegationCompact {
pubkey: d.delegation.pubkey.into(),
expiration: d.delegation.expiration,
targets: d.delegation.targets,
},
signature: d.signature.into(),
})
.collect::<Vec<_>>()
}),
};
Ok(envelope)
}
/// Verifies the SignedEnvelope according to the Internet Computer authentication rules.
///
/// Verification rules:
/// - Delegation chain length must not exceed 5
/// - Delegations must not be expired (considering the permitted time drift)
/// - Each signature in the chain must verify against the corresponding public key
/// - If delegation targets are specified, the expected target must be included
/// - The content digest must match the expected digest (if provided)
///
/// # Arguments
/// * `now_ms` - The current time in milliseconds since the Unix epoch
/// * `expect_target` - Optional canister ID that should be in the delegation targets
/// * `expect_digest` - Optional expected content digest to verify against
///
/// # Returns
/// * `Ok(())` if verification succeeds
/// * `Err(String)` with a detailed error message if verification fails
pub fn verify(
&self,
now_ms: u64,
expect_target: Option<Principal>,
expect_digest: Option<&[u8]>,
) -> Result<(), String> {
let current_time_ns = now_ms as u128 * 1_000_000;
let digest = match (self.digest.as_ref(), expect_digest) {
(Some(digest), Some(expect_digest)) => {
if digest.as_slice() != expect_digest {
return Err("Content digest does not match".to_string());
}
digest
}
(Some(digest), None) => digest.as_slice(),
(None, Some(expect_digest)) => expect_digest,
(None, None) => {
return Err("No content digest provided for verification".to_string());
}
};
let mut last_verified = &self.pubkey;
if let Some(delegation) = &self.delegation {
if delegation.len() > 5 {
return Err(format!(
"Delegation chain length exceeds the limit 5: {}",
delegation.len()
));
}
for d in delegation {
if d.delegation.expiration / 1_000_000 < now_ms - PERMITTED_DRIFT_MS {
return Err(format!(
"Delegation has expired:\n\
Provided expiry: {}\n\
Local replica timestamp: {}",
d.delegation.expiration,
now_ms * 1_000_000,
));
}
let targets = match &d.delegation.targets {
Some(targets) => {
if let Some(target) = &expect_target {
// Should check if the expected target is in the delegation targets
if !targets.contains(target) {
return Err(format!(
"Expected target canister ID '{expect_target:?}' is not in the delegation targets: {:?}",
targets
));
}
}
Some(
targets
.iter()
.map(|p| p.as_slice().to_vec())
.collect::<Vec<Vec<u8>>>(),
)
}
None => None,
};
let msg = delegation_signature_msg(
d.delegation.pubkey.as_slice(),
d.delegation.expiration,
targets.as_ref(),
);
let mut message = Vec::with_capacity(
IC_REQUEST_AUTH_DELEGATION_DOMAIN_SEPARATOR.len() + msg.len(),
);
message.extend_from_slice(IC_REQUEST_AUTH_DELEGATION_DOMAIN_SEPARATOR);
message.extend(msg);
verify_sig(last_verified, &message, &d.signature, ¤t_time_ns)?;
last_verified = &d.delegation.pubkey;
}
}
verify_sig(last_verified, digest, &self.signature, ¤t_time_ns)
}
/// Extracts a SignedEnvelope from the Authorization header.
///
/// This looks for an "ICP" authentication scheme in the Authorization header
/// and decodes the associated token as a SignedEnvelope.
///
/// # Arguments
/// * `headers` - The HTTP headers to extract from
///
/// # Returns
/// * `Option<Self>` - The extracted envelope, or None if not found or invalid
pub fn from_authorization(headers: &HeaderMap) -> Option<Self> {
if let Some(token) = headers.get(AUTHORIZATION)
&& let Ok(token) = token.to_str()
&& let Some(token) = token.strip_prefix("ICP ")
&& let Ok(envelope) = Self::from_base64(token)
{
return Some(envelope);
}
None
}
/// Adds the SignedEnvelope to the Authorization header.
///
/// This encodes the envelope as a base64url string and adds it to the
/// Authorization header with the "ICP" authentication scheme.
///
/// # Arguments
/// * `headers` - The HTTP headers to add to
///
/// # Returns
/// * `Result<(), String>` - Success or an error message if adding fails
pub fn to_authorization(&self, headers: &mut HeaderMap) -> Result<(), String> {
headers.insert(
AUTHORIZATION,
format!("ICP {}", self.to_base64())
.parse()
.map_err(|err| format!("insert {AUTHORIZATION} header failed: {err}"))?,
);
Ok(())
}
/// Extracts a SignedEnvelope from the IC-Auth-* HTTP headers.
///
/// This looks for the individual components of the envelope in separate headers
/// and reconstructs the envelope from them.
///
/// # Arguments
/// * `headers` - The HTTP headers to extract from
///
/// # Returns
/// * `Option<Self>` - The extracted envelope, or None if not found or invalid
pub fn from_headers(headers: &HeaderMap) -> Option<Self> {
if let Some(pubkey) = extract_data(headers, &HEADER_IC_AUTH_PUBKEY)
&& let Some(digest) = extract_data(headers, &HEADER_IC_AUTH_CONTENT_DIGEST)
&& let Some(signature) = extract_data(headers, &HEADER_IC_AUTH_SIGNATURE)
{
let mut envelope = Self {
pubkey: pubkey.into(),
signature: signature.into(),
digest: Some(digest.into()),
delegation: None,
};
match extract_data(headers, &HEADER_IC_AUTH_DELEGATION) {
Some(data) => {
if let Ok(delegation) = from_reader(&data[..]) {
envelope.delegation = Some(delegation);
return Some(envelope);
}
}
None => return Some(envelope),
}
}
None
}
/// Adds the SignedEnvelope components to the IC-Auth-* HTTP headers.
///
/// This breaks down the envelope into its components and adds each one
/// to a separate HTTP header.
///
/// # Arguments
/// * `headers` - The HTTP headers to add to
///
/// # Returns
/// * `Result<(), String>` - Success or an error message
pub fn to_headers(&self, headers: &mut HeaderMap) -> Result<(), String> {
headers.insert(
&HEADER_IC_AUTH_PUBKEY,
URL_SAFE_NO_PAD
.encode(&self.pubkey)
.parse()
.map_err(|err| format!("insert {HEADER_IC_AUTH_PUBKEY} header failed: {err}"))?,
);
if let Some(digest) = &self.digest {
headers.insert(
&HEADER_IC_AUTH_CONTENT_DIGEST,
URL_SAFE_NO_PAD.encode(digest).parse().map_err(|err| {
format!("insert {HEADER_IC_AUTH_CONTENT_DIGEST} header failed: {err}")
})?,
);
}
headers.insert(
&HEADER_IC_AUTH_SIGNATURE,
URL_SAFE_NO_PAD
.encode(&self.signature)
.parse()
.map_err(|err| format!("insert {HEADER_IC_AUTH_SIGNATURE} header failed: {err}"))?,
);
if let Some(delegations) = &self.delegation {
headers.insert(
&HEADER_IC_AUTH_DELEGATION,
URL_SAFE_NO_PAD
.encode(deterministic_cbor_into_vec(&delegations)?)
.parse()
.map_err(|err| {
format!("insert {HEADER_IC_AUTH_DELEGATION} header failed: {err}")
})?,
);
}
Ok(())
}
}
/// Verifies a delegation chain.
///
/// This function checks the validity of a chain of signed delegations,
/// ensuring that each delegation is not expired, and that the signatures
/// are valid according to the provided IC root public key.
///
/// # Arguments
/// * `user_pubkey` - The der public key of the user to sign the delegation chain
/// * `session_pubkey` - The public key of the session to verify against
/// * `delegations` - The chain of signed delegations to verify
/// * `now_ms` - The current time in milliseconds since the Unix epoch
/// * `ic_root_public_key_raw` - Optional raw IC root public key for signature verification
pub fn verify_delegation_chain(
user_pubkey: &[u8],
session_pubkey: &[u8],
delegations: &[SignedDelegationCompact],
now_ms: u64,
ic_root_public_key_raw: Option<&[u8]>,
) -> Result<(), String> {
if delegations.is_empty() {
return Err("Delegation chain is empty".to_string());
}
let current_time_ns = now_ms as u128 * 1_000_000;
let ic_root_public_key_raw = ic_root_public_key_raw.unwrap_or(IC_ROOT_PK_DER);
let mut last_verified = user_pubkey;
for d in delegations {
if d.delegation.expiration / 1_000_000 < now_ms - PERMITTED_DRIFT_MS {
return Err(format!(
"Delegation has expired:\n\
Provided expiry: {}\n\
Local replica timestamp: {}",
d.delegation.expiration,
now_ms * 1_000_000,
));
}
let msg = delegation_signature_msg(
d.delegation.pubkey.as_slice(),
d.delegation.expiration,
d.delegation
.targets
.as_ref()
.map(|targets| {
targets
.iter()
.map(|p| p.as_slice().to_vec())
.collect::<Vec<Vec<u8>>>()
})
.as_ref(),
);
let mut message =
Vec::with_capacity(IC_REQUEST_AUTH_DELEGATION_DOMAIN_SEPARATOR.len() + msg.len());
message.extend_from_slice(IC_REQUEST_AUTH_DELEGATION_DOMAIN_SEPARATOR);
message.extend(msg);
verify_sig_with_rootkey(
ic_root_public_key_raw,
last_verified,
&message,
&d.signature,
¤t_time_ns,
)?;
last_verified = &d.delegation.pubkey;
}
if last_verified != session_pubkey {
return Err(format!(
"Last verified public key does not match session public key:\n\
Last verified: {}\n\
Session public key: {}",
hex::encode(last_verified),
hex::encode(session_pubkey)
));
}
Ok(())
}
/// Extracts base64url-encoded data from an HTTP header.
///
/// # Arguments
/// * `headers` - The HTTP headers to extract from
/// * `key` - The name of the header to extract
///
/// # Returns
/// * `Option<Vec<u8>>` - The decoded data, or None if not found or invalid
pub fn extract_data(headers: &HeaderMap, key: &HeaderName) -> Option<Vec<u8>> {
if let Some(val) = headers.get(key)
&& let Ok(val) = val.to_str()
&& let Ok(data) = decode_base64(val)
{
return Some(data);
}
None
}
/// Extracts the authenticated user principal from the HTTP headers.
///
/// This looks for the IC-Auth-User header and parses it as a Principal.
/// If the header is not found or invalid, it returns the anonymous principal.
///
/// # Arguments
/// * `headers` - The HTTP headers to extract from
///
/// # Returns
/// * `Principal` - The authenticated user principal or anonymous principal
pub fn extract_user(headers: &HeaderMap) -> Principal {
if let Some(caller) = headers.get(&HEADER_IC_AUTH_USER) {
if let Ok(caller) = Principal::from_text(caller.to_str().unwrap_or_default()) {
caller
} else {
ANONYMOUS_PRINCIPAL
}
} else {
ANONYMOUS_PRINCIPAL
}
}
/// Decodes base64url-encoded data.
///
/// This function handles both padded and unpadded base64url data.
///
/// # Arguments
/// * `data` - The base64url-encoded string to decode
///
/// # Returns
/// * `Result<Vec<u8>, String>` - The decoded data or an error message
pub fn decode_base64(data: &str) -> Result<Vec<u8>, String> {
URL_SAFE_NO_PAD
.decode(data.trim().trim_end_matches('='))
.map_err(|err| format!("failed to decode base64 data: {err}"))
}
/// SignedEnvelopeFull is a full representation of the SignedEnvelope.
/// It includes the full field names for serialization and deserialization.
#[derive(Clone, Debug, CandidType, Deserialize, Serialize)]
pub struct SignedEnvelopeFull {
#[serde(alias = "p")]
pub pubkey: ByteBufB64,
#[serde(alias = "s")]
pub signature: ByteBufB64,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[serde(alias = "h")]
pub digest: Option<ByteBufB64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[serde(alias = "d")]
pub delegation: Option<Vec<SignedDelegation>>,
}
impl From<SignedEnvelope> for SignedEnvelopeFull {
fn from(envelope: SignedEnvelope) -> Self {
Self {
pubkey: envelope.pubkey,
signature: envelope.signature,
digest: envelope.digest,
delegation: envelope
.delegation
.map(|delegations| delegations.into_iter().map(Into::into).collect()),
}
}
}
impl From<SignedEnvelopeFull> for SignedEnvelope {
fn from(envelope: SignedEnvelopeFull) -> Self {
Self {
pubkey: envelope.pubkey,
signature: envelope.signature,
digest: envelope.digest,
delegation: envelope
.delegation
.map(|delegations| delegations.into_iter().map(Into::into).collect()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use ciborium::value::Value;
use ic_agent::{Identity, identity::BasicIdentity};
use ic_canister_sig_creation::CanisterSigPublicKey;
use crate::unix_timestamp;
#[test]
fn test_envelope_with_ed25519() {
let secret = [8u8; 32];
let id = BasicIdentity::from_raw_key(&secret);
println!("id: {:?}", id.sender().unwrap().to_text());
// jjn6g-sh75l-r3cxb-wxrkl-frqld-6p6qq-d4ato-wske5-op7s5-n566f-bqe
let msg = b"hello world";
let mut headers = HeaderMap::new();
let se = SignedEnvelope::sign_message(&id, msg).unwrap();
se.to_headers(&mut headers).unwrap();
let mut se2 = SignedEnvelope::from_headers(&headers).unwrap();
assert!(
se2.verify(unix_timestamp().as_millis() as u64, None, None)
.is_ok()
);
se2.digest = Some(sha3_256(b"hello world 2").to_vec().into());
assert!(
se2.verify(unix_timestamp().as_millis() as u64, None, None)
.is_err()
);
let digest = sha3_256(msg);
let sig = id.sign_arbitrary(digest.as_slice()).unwrap();
let se: SignedEnvelope = sig.try_into().unwrap();
assert!(
se.verify(
unix_timestamp().as_millis() as u64,
None,
Some(digest.as_slice())
)
.is_ok()
);
}
#[test]
fn test_envelope_with_ed25519_2() {
// test data from ts/ic-auth/src/identity.test.ts
let secret = [8u8; 32];
let id = BasicIdentity::from_raw_key(&secret);
let msg = "pGFk92FoWCDy_PBrUtbrh328ZTWvrZnuiE2EMKHfMz_1M6f3JN1nq2FwWCwwKjAFBgMrZXADIQATmPYsbRpFfFG6aktfPb0vafypMhYhjciZfkFr0X2TymFzWEAzEYt2uq3q2BiMmgz91CLI6Sj0Vs90pE-bTd37h35FpBOonchIBqXyjtBpnfguDbZkKzy_VWbs9bDx29_5lqwD";
let se = SignedEnvelope::from_base64(msg).unwrap();
assert_eq!(id.public_key().unwrap(), se.pubkey.as_slice());
assert_eq!(
se.digest.as_ref().unwrap().as_slice(),
hex::decode("f2fcf06b52d6eb877dbc6535afad99ee884d8430a1df333ff533a7f724dd67ab")
.unwrap()
.as_slice()
);
assert!(
se.verify(unix_timestamp().as_millis() as u64, None, None)
.is_ok()
);
let msg = "o2Fk92FwWCwwKjAFBgMrZXADIQATmPYsbRpFfFG6aktfPb0vafypMhYhjciZfkFr0X2TymFzWEAzEYt2uq3q2BiMmgz91CLI6Sj0Vs90pE-bTd37h35FpBOonchIBqXyjtBpnfguDbZkKzy_VWbs9bDx29_5lqwD";
let se = SignedEnvelope::from_base64(msg).unwrap();
assert_eq!(id.public_key().unwrap(), se.pubkey.as_slice());
assert_eq!(se.digest, None);
assert!(
se.verify(unix_timestamp().as_millis() as u64, None, None)
.is_err()
);
let obj = vec![
(Value::from("z"), Value::from("z")), // 0x617a
(Value::from("aa"), Value::from("aa")), // 0x626161
(Value::from(10), Value::from(10)), // 0x0a
(Value::from(100), Value::from(100)), // 0x1864
(Value::from(-1), Value::from(-1)), // 0x20
(Value::from(false), Value::from(false)), // 0xf4
];
let data = deterministic_cbor_into_vec(&Value::Map(obj)).unwrap();
let digest = sha3_256(&data);
assert!(
se.verify(
unix_timestamp().as_millis() as u64,
None,
Some(digest.as_slice())
)
.is_ok()
);
// generated by ic-certification 2.6
let msg = "pGFkgaJhZKJhZRsYojcfvOoZmmFwWCwwKjAFBgMrZXADIQBoGlRAsc_ADaKVxzccHBGsazda-ERmveqkjSZFbiY2nmFzWQZu2dn3omtjZXJ0aWZpY2F0ZVkE1dnZ96NkdHJlZYMBgwGDAYIEWCD_2rktIbM7CQPjMFxdtgoXDsQBe6ZnfYHz_VEkO7uCtYMCSGNhbmlzdGVygwGDAYMBgwJKAAAAAAAAAAcBAYMBgwGDAk5jZXJ0aWZpZWRfZGF0YYIDWCBWgGiPBtG2u85cDRFAPYs7vLdc6Grw571wSc0RMCI6EYIEWCDY9k96_KalXU7m3tmwIAusZlHK9MehkgIStaA8m_HfNoIEWCBOYkQtPpTRBP_gYe1cnFAcC8xgoKTJJTJlEp9i4bgOfoIEWCBthgttL0RxWKx9zQUzB1VVoDJfNVBABnygDRTTlPpcd4IEWCAfyKogUeU4zw3Pa2EBGB5VcNaukbds8ZT7AbSR6GX-gYIEWCDm4cUcLUxvcw_J2442dS94YI_tAHxqDsyTEqltTeRe4IIEWCBODzfzIXgSp1yunK4EJegz62agSuPIA2wlNiYf8RilioMBggRYICnLk_WKVCD1pHHANP7Kh0HvezdFZ1X21aGEwGM67W9ggwGCBFggfbVemnKYEja9TKRlj9yBlXGcvPfICHl9a299larerwSDAkR0aW1lggNJmrOgreu2wMwYaXNpZ25hdHVyZVgwqclPwHXkj30JDkbDXJZG5gddlh7PgmYEy5XS41RLyngn4KATI205Wt1cee-lKHXoamRlbGVnYXRpb26iaXN1Ym5ldF9pZFgdQ9yvEYDbgv2nCM46x6A6YGCr3hPpVGxg6MzmXQJrY2VydGlmaWNhdGVZApTZ2feiZHRyZWWDAYIEWCDtgh8WsbKpiZoLi5XYylCxX__YyZPUZsIk0PXjz1lzvYMBggRYIFunwSpsZKpJ5QG34-6Hdf5fiGjJrkCJRjDPdKogQZzlgwGDAkZzdWJuZXSDAYMBggRYIH78fWQs2KX21INhFAUooPeg_qG9FaEZ-FnJvQByCxnYgwGDAYMBgwGDAlgdQ9yvEYDbgv2nCM46x6A6YGCr3hPpVGxg6MzmXQKDAYMCT2NhbmlzdGVyX3Jhbmdlc4IDWDLZ2feCgkoAAAAAAAAABwEBSgAAAAAAAAAHAQGCSgAAAAACEAAAAQFKAAAAAAIf__8BAYMCSnB1YmxpY19rZXmCA1iFMIGCMB0GDSsGAQQBgtx8BQMBAgEGDCsGAQQBgtx8BQMCAQNhALiJ5Njs3jKCWgg15m1SnjGedDTM6N_jrO8RBSZZl13LZLkUsrJ-jXTvit3bHngQtgBp9qUJUzy8-XCxJo82dQVlV5gl1xnyg-ykrW4rt26B_Up2EY9NOZnuKxOqWdH8aYIEWCDNNRYZFlJHwcKUuJIXQK9JIA0pKYarBTb9K3DDUzpPD4IEWCCcu0ovvg1X5FqsJC-HA-rJOFf6CdjDxCqs4Ii-cSOwS4IEWCANOW8zq66lZnKBOKlXR-J03L7dcvb1a5pIDj7u4eHu54IEWCAmUyo7WsE_oDGyz13rElnLdnC06YydNtMc98fxHuNuDIIEWCB2Q0u6125BV4e6HKAOUmknD4_423JMC2hEXc2mwamhSoMCRHRpbWWCA0nPvfXihLHAzBhpc2lnbmF0dXJlWDCulZzovZrP2dWIIMhEBaSh5XU9J_aMKtKUW6JgNpzOTHAYDTCa1XyOdPpZ00e6ix9kdHJlZYMBggRYIHYbG1hqqpq8_WWQRV3C6xttakOgXO079nv_qXI6sETrgwJDc2lngwGCBFggqSaSUsJM66WL_J9fC3tzQBJsHddRM7iKFSHNW_dnjzuDAYIEWCCwKoJHyzof_3-jNNjHETV_ezpXKqNRTc76XGllr_3zHoMBggRYIJJpmDUOjCZuLgTeQwfKb0B7aJCbce1Nkt6svPUVwhpngwGCBFggRAxMN7esuXj82dGA0oo0jQFZL_E9_NIW3QZuNI8Zwx-DAYMBggRYIMGyphmMiwQtCOVs7007IKrTQdJMcTbReVp1F_LuJHxsgwGCBFggdHEIJNsCUNILPOaPC72zPGcDzhAPP3QeFCJHEo4C52yDAlgg5P8-40zMpjFL_rEmGbtJM39mBn6oASZiC_1kSmOgrEWDAlggJ8HNs_IqkCfk2fuw-rIgFqkpkSqPIxHFoVgIw4ysbaqCA0CCBFggwy969dGj_RU_qiA4zFkKgMgkmTw6Dg_uzIICxJCXkoFhaFggrQaFGeXvwG47mPtg184iJUpbfwDtMVhSJQinJDD7c6lhcFg-MDwwDAYKKwYBBAGDuEMBAgMsAAoAAAAAAAAABwEB4tq0V5C7RTXNzCnXExRTjoLBLwxA2yf_dlWf5jo_uFRhc1hAfcBZym1voNCADrdalYEZAWT_d_zlMVQG7X_X4lR9zTqvHxj-iwT0XBIN3nvHJIBx8wnxOOW2sOOu0hSpiYKgDQ";
let se = SignedEnvelope::from_base64(msg).unwrap();
println!("SignedEnvelope: {:?}", se);
let rt = se.verify(1772449812590u64, None, None);
println!("Verification result: {:?}", rt);
assert!(rt.is_ok());
// generated by ic-certification 3.1
let msg = "pGFkgaJhZKJhZRsYprjiTQ1kp2FwWCwwKjAFBgMrZXADIQCA6d4AfqWkdFNu5tbaLp7r9jDxilfJvcLWRdIYzSior2FzWQc02dn3omtjZXJ0aWZpY2F0ZVkGWdnZ96NkdHJlZYMBgwGDAYIEWCD_2rktIbM7CQPjMFxdtgoXDsQBe6ZnfYHz_VEkO7uCtYMCSGNhbmlzdGVygwGDAYMBgwJKAAAAAAAAAAcBAYMBgwGDAk5jZXJ0aWZpZWRfZGF0YYIDWCB2gerHLFm4j1TskyiMYwuJVjti6AkhLhUkmf3yVzgt44IEWCDY9k96_KalXU7m3tmwIAusZlHK9MehkgIStaA8m_HfNoIEWCClii6jhbz6sPHd0uk34rJi6t4x2zU7ap7pu2pHQee1S4IEWCBthgttL0RxWKx9zQUzB1VVoDJfNVBABnygDRTTlPpcd4IEWCA7n254mzmU7xueyXf_BwN4PmMyXI3ZkEy2ep_1MtzWEYIEWCDSHJjM3167Pm98kHUeDMXaLF_cPTiJsu1zlQKZG7gnQoIEWCBZMddBC0-Quz23yaa-wGXhf4sJJ0CMVlNApcf6P0M2toMBggRYIIRNgDlknNZ00VrpChW1u2EkiJpLHm16Aaw9s81wxyw9gwGCBFggMm2roqFHTU5jlTeTnwz61Y9afsoeE1dBam7LurSMO1mDAkR0aW1lggNJp8mtrpTv4M4YaXNpZ25hdHVyZVgwsUfGmq1hpWv0RXnDU4isnnsrytYCYohlaDj8KpVrF95OaVWcsgyBWomgsYtLoxPqamRlbGVnYXRpb26iaXN1Ym5ldF9pZFgdQ9yvEYDbgv2nCM46x6A6YGCr3hPpVGxg6MzmXQJrY2VydGlmaWNhdGVZBBjZ2feiZHRyZWWDAYMBggRYIK3tAhG-5CMHRe6B6HYrumYzszgynK2_Cyk3lgxiz-VegwGDAk9jYW5pc3Rlcl9yYW5nZXODAYMBggRYIPWzPdcCUOwykUxZYJMI-zG0o1JPBtxcycR-ltOoWzQKgwGDAYMBgwGDAlgdQ9yvEYDbgv2nCM46x6A6YGCr3hPpVGxg6MzmXQKDAkoAAAAAAAAABwEBggNYMtnZ94KCSgAAAAAAAAAHAQFKAAAAAAAAAAcBAYJKAAAAAAIQAAABAUoAAAAAAh___wEBggRYIJ2wKoflpjM_8kD8PoQJl7Pvux0kvc21FZQlcEpPjebeggRYIGBtpBoQkI8sWSD0kMaU5qqTdy-kUVf1tmSbla42it92ggRYIOyolKEQ1iwzQOQhsmpRZ-DqycNrnX_QlOFSvAY74bT8ggRYIKvGtteIKoBa9myTiVb5LgymcFkhzR8PhHkWmb9ddOKMggRYIHddYNv5vlTceU_HgpFF5d20rbT5bWhGm-sx-i-4LQsLggRYIAH0KcEdL8U24W7AywFr3_rR_1KtobDR9S_4zxJRVn0UgwGCBFggeePKvLIybJiV9N45REvhjwjws9fpaA2eiaYKSPqhZ_yDAYMCRnN1Ym5ldIMBgwGCBFggiISkVrMM0Rk0jF71z1nHVHXloWSxxt03W_wPRhKANMKDAYMBgwGDAYMCWB1D3K8RgNuC_acIzjrHoDpgYKveE-lUbGDozOZdAoMBgwGCBFggUIlMwLMiEhS4oN4qyA9F8qOf6QkRzQ0IjhmShRyLJ-mDAkpwdWJsaWNfa2V5ggNYhTCBgjAdBg0rBgEEAYLcfAUDAQIBBgwrBgEEAYLcfAUDAgEDYQC4ieTY7N4ygloINeZtUp4xnnQ0zOjf46zvEQUmWZddy2S5FLKyfo1074rd2x54ELYAafalCVM8vPlwsSaPNnUFZVeYJdcZ8oPspK1uK7dugf1KdhGPTTmZ7isTqlnR_GmCBFggOtxzXIoxJu5vYSDBubz8MoJVDYZct-cjWYzLeTdyU5KCBFggNgnQcvmXshXCdPo1-Q72q0aowcpaEHxogyGI4WwCWPKCBFggj4x3eo7K7qsgb-JEuWw_9DcMe2RulRtusZKIgE7JCAyCBFgg2cEDpvexMjtphCR0PV8O8jGeRQXzwnGxohAmgy9KAbKCBFggYOlh9AO7NO3rkGDg86e0FJjFS5hp97IJEtK2fTj5Fo2CBFggOv_0dwIPh6D4tdK_kb9vQEszT-3myESFPY98--7BKfWDAkR0aW1lggNJo8Kk9ent4M4YaXNpZ25hdHVyZVgwhYPOO9jW_tQ5NAMfHRb2rICF9TnGmipUNRTkhaIOvsiy_VkrpQJGtSA7noPJuKEEZHRyZWWDAYIEWCAd1PxBigg-1t83kV57jr2buakKUEVB_liWTXLdBoAtOIMCQ3NpZ4MBgwGDAlggBYX1pW_0nbODY6WVI_XoWnz46JsL1yG2omXetIl1irWDAlggkxDcFnea8djFW-ISPfIwEdE-KvsXYYMq6_WnceSpqhmCA0CCBFggv3BAqPUThOCNdaS-f7TRhEu6A1izhQIvkbUeggveE7OCBFggRpEubsoN-OjKpzy8IAo9jGIQFCCVntAMhOVobFPyHJVhaFggrQaFGeXvwG47mPtg184iJUpbfwDtMVhSJQinJDD7c6lhcFg-MDwwDAYKKwYBBAGDuEMBAgMsAAoAAAAAAAAABwEBkVc8F_k6Ukkl1TQM_crFVBrhBzqDMR65DDzNGLoK-E5hc1hAsLo4H4SdoWumrYdekdP1oBIeOE-W5G8BanUak3w7TPCSZv1IBRu0iMW9x1AoA3EftD9ckUM_54hG21KCEknWCA";
let se = SignedEnvelope::from_base64(msg).unwrap();
println!("SignedEnvelope: {:?}", se);
let rt = se.verify(1773718385139u64, None, None);
println!("Verification result: {:?}", rt);
assert!(rt.is_ok());
}
#[test]
fn test_envelope_with_iccsa() {
let msg = hex::decode("086c81b03b34184d2365b88a7d94ad9cc0f4e98970b6c10068aae4e407333339")
.unwrap();
let sig =
hex::decode("d9d9f7a26b636572746966696361746558a1d9d9f7a26474726565830183024863616e697374657283024a0000000000000001010183024e6365727469666965645f646174618203582053e3b19ab292296b52b451b0662af2d86ac707569b39825fc31f62aca41406d483024474696d6582034387ad4b697369676e61747572655830a95766af95898e1c8492de7b7d9e6c601ea9d9958113f6c0491ef044ed5ebb03d31983abfa40ebbef7068ebaf7e66f05647472656583024373696783025820591047009df12cb39741d672f270045fd15beec2b0b84c1d71bda98b758726cd83025820d37372239856cdf2ae158e5ac365f15501a9e5612a970ddd7b3199c522b54194820340")
.unwrap();
let pk_der =
hex::decode("303c300c060a2b0601040183b8430102032c000a000000000000000101011f809d0136deeed8e0187447d20ac0e13e0201e1dede8c437eada3e8dc349f85")
.unwrap();
let root =
hex::decode("308182301d060d2b0601040182dc7c0503010201060c2b0601040182dc7c05030201036100b90210504fe157d1df412e500ced967ef794dc7aa88c84d764b74b6bc2cf0e575d79f331927df062240c88a28e1802c60b407c7bce541b50310d775919bcd0f799222c3738bc3bcc8bf05af5f52ee2afec54c460bda35c6c379267924db2d374")
.unwrap();
let (alg, _pk) = user_public_key_from_der(&pk_der).unwrap();
assert_eq!(alg, Algorithm::IcCanisterSignature);
let cspk = CanisterSigPublicKey::try_from(pk_der.as_slice()).unwrap();
println!("canister_id: {}", cspk.canister_id.to_text());
// canister_id: rrkah-fqaaa-aaaaa-aaaaq-cai
let res = verify_sig_with_rootkey(&root, &pk_der, &msg, &sig, &0);
println!("Verification result: {:?}", res);
assert!(res.is_ok());
}
#[test]
#[ignore]
fn test_verify_delegation_chain() {
let user_pubkey = hex::decode(
"303C300C060A2B0601040183B8430102032C000A0000000000000007010116FB513D360579FA1102D36E3BC8D53FB966F3AC9F717842B2B54C227582D786",
).unwrap();
let session_pubkey = hex::decode(
"302A300506032B6570032100C6C020379C06F82F81111E1DA776F143C4F532EBE2D9FB16461F1243B5A92BAA",
).unwrap();
let delegations = vec![
SignedDelegationCompact {
delegation: DelegationCompact {
pubkey: hex::decode(
"302A300506032B65700321005EC6DE6BD72919EA56CCA4E8E7124CEF75807DC212F1AE1FC3BA58903FC8795A",
).unwrap().into(),
expiration: 1746365411593000000,
targets: None,
},
signature: hex::decode(
"D9D9F7A26B63657274696669636174655904AFD9D9F7A36474726565830183018301820458207F795EAF211FB3DA321D2291429BAD43A869A1CCFD761FF5A35F1A362322A1F483024863616E697374657283018301830183024A000000000000000701018301830183024E6365727469666965645F64617461820358201BD26E2A134BB173ACE579802B2BD138D28B3307CE5415BAD1304EDC8EB6A5E182045820D8F64F7AFCA6A55D4EE6DED9B0200BAC6651CAF4C7A1920212B5A03C9BF1DF3682045820F5ECD4D3EDCD85DE4C3B07B8A8D77F69DEFCB5BD652071FFD46B2A6604029112820458205085E5811DEACD817B4FF38A37C93B8421B7DDC92E5489FDF0D3F5CF36E320728204582032698C8D6A87E6831B3E8AA11641F27E77B692C6F866FD3D9D3E917EA570515682045820F7C9916E3BDEB2AC59E18441EBEFDDA60B801A91F8508B4F1B2ADA5B7F62AAA682045820989249384F8855B851E3F07C55E66DEFA4719612B1D9565E4C2B47918F902240830182045820FBF74833516364406FEC8290DFB4593E369120B408F757754CF11E96DF87AA8D83024474696D658203498CF3BBB0EDC2959E18697369676E61747572655830AF6204D323E367981055D0FB24E03070BDD6EC0D1BFABE2E3408B9C4792F1D4C44FE49A3AE9FD6ABD53D53DCF38E0E0E6A64656C65676174696F6EA2697375626E65745F6964581D43DCAF1180DB82FDA708CE3AC7A03A6060ABDE13E9546C60E8CCE65D026B6365727469666963617465590294D9D9F7A264747265658301820458200E077A1AA8E3A69E473446FB605EF74212198EC90D4D0010B3DC091AE3F01B798301830182045820954285DD391C0258F30BD79123B51AB939B93982F24426FE94E376FC13F5D3D38302467375626E6574830183018301820458200985315BBE905B7F9336D7064793905B005689F3C9C2A21AD9A31FBE6CDD5599830182045820466A70286CF9ACE9801CA53E22AF6EE059A094FD60498606D484B6854058307D83018301820458208B2F6C15078AE4D3B93470915CA53E373327F37EA74BA1B8177D986BB79B31AE8302581D43DCAF1180DB82FDA708CE3AC7A03A6060ABDE13E9546C60E8CCE65D02830183024F63616E69737465725F72616E67657382035832D9D9F782824A000000000000000701014A00000000000000070101824A000000000210000001014A00000000021FFFFF010183024A7075626C69635F6B657982035885308182301D060D2B0601040182DC7C0503010201060C2B0601040182DC7C050302010361008819AAA868DA353E3451BB97675FFFAA711E3C1C1230E39E1FEEB0AF9FE03E67C9393F08D796C1E42B528ABB5FCB4159199284B00096F6DAFA93B4711F1AC65F594B67CE2C0B35710E0391C5424CB754779A1C6084F6E77B584E7C8CF7FE9D89820458202C51DB7B5650B7A3DBBB8530A7449CC6F90144778B62F20F3C26D72E95E50698820458206961EF137C2AEE0B0467082EF6D3C12C03E93013B602A4CB6214270E484863F182045820EA6D87F551BD7F433852FA3F8697E653676C2DA59615618B39C50D74069123C983024474696D65820349A0BFA380BAC2959E18697369676E61747572655830B82F7761EC5C9A30672188D7A5F7EE82B540E51A86239C5A2A8E51DA6CB19CA3980DD8125440EC49C54865CED7EAA10F6474726565830182045820ECF5038E1037F1181243DAABBD82BE6045A5B0B9F1025C905FA339E2C1305608830243736967830183018204582029D9F6DAFC4411D79A0B7A64FCFF42FCE426974773A4178BA421DF59165D9E32830182045820ABD07F88910310B251F025105AEA620A46984E3D677A54EEC324134F6ED4DDD783018204582055D9BE42D0A247D96B4C67822E937EAD71567B5BE0A4965150F01D2AB51AC971830182045820CCB0E0C4819032E6C3C7B0E359F8A7297EED11A484FD7D8081CCA2461371E0328301830258206BB18EF359E4DBB2372D7C255BA9351BC862ACF06D9D2B14CDB1271EEFAA597E8302582015451785051A113622599AD74E94CC066EEF4BAC88AC36A42403C65CEDFFE8E0820340820458203A229EE09D8AC3B9C0540F596654FED78B3D5ABDD03657F66AD3240856214167820458206C0048C242D41CF83B77D782F6733B95B7AA137736741D05DACA0385D79BF466").unwrap().into()
}, SignedDelegationCompact{
delegation: DelegationCompact {
pubkey: hex::decode(
"302A300506032B6570032100C6C020379C06F82F81111E1DA776F143C4F532EBE2D9FB16461F1243B5A92BAA",
).unwrap().into(),
expiration: 1746365411593000000,
targets: None,
},
signature: hex::decode("18397232DD4AE43103E1884E956F91B44188E40A288DBCB73BF99DC27DBFAB1E1F0FAB76C44E0A0206F34887D5197B46C2D57876B0DB4C28E97967FDA8807908").unwrap().into(),
}];
let rt = verify_delegation_chain(
&user_pubkey,
&session_pubkey,
&delegations,
1746365411593,
None,
);
println!("Verification result: {:?}", rt);
assert!(rt.is_ok(), "Delegation chain verification failed: {:?}", rt);
}
}