ic_auth_types 0.9.1

Shared Rust data types, byte wrappers, and deterministic CBOR helpers for IC-Auth.
Documentation
//! Shared data types and serialization helpers for IC-Auth.
//!
//! This crate contains the wire-facing structures used by the Rust verifier,
//! the TypeScript SDK, and applications that exchange IC-Auth delegations or
//! signed envelopes. Byte fields use Base64URL strings in human-readable
//! formats such as JSON while preserving compact byte strings in binary formats
//! such as CBOR.
//!
//! The CBOR helpers wrap [`cbor2`] and provide deterministic RFC 8949 encoding
//! for values that are signed or hashed. [`cbor_from_slice`] also contains the
//! compatibility path needed by IC/Candid-specific types such as
//! [`candid::Principal`].
//!
//! # Examples
//!
//! ```
//! use candid::Principal;
//! use ic_auth_types::{ByteBufB64, Delegation, deterministic_cbor_into_vec};
//!
//! let delegation = Delegation {
//!     pubkey: ByteBufB64::from(vec![1, 2, 3]),
//!     expiration: 1_900_000_000_000_000_000,
//!     targets: Some(vec![Principal::management_canister()]),
//! };
//!
//! let bytes = deterministic_cbor_into_vec(&delegation).unwrap();
//! assert!(!bytes.is_empty());
//! ```

use candid::{CandidType, Principal};
use serde::{Deserialize, Serialize};

mod bytes;
mod cbor;
mod xid;

pub use bytes::*;
pub use cbor::*;
pub use xid::*;
/// A delegation from one key to another.
///
/// If key A signs a delegation containing key B, then key B may be used to
/// authenticate as key A's corresponding principal(s).
#[derive(CandidType, Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub struct Delegation {
    /// The delegated-to key.
    #[serde(alias = "p")]
    pub pubkey: ByteBufB64,
    /// A nanosecond timestamp after which this delegation is no longer valid.
    #[serde(alias = "e")]
    pub expiration: u64,
    /// If present, this delegation only applies to requests sent to one of these canisters.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    #[serde(alias = "t")]
    pub targets: Option<Vec<Principal>>,
}

/// SignedDelegation is a [`Delegation`] that has been signed by an [`Identity`](https://docs.rs/ic-agent/latest/ic_agent/trait.Identity.html).
#[derive(CandidType, Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub struct SignedDelegation {
    /// The signed delegation.
    #[serde(alias = "d")]
    pub delegation: Delegation,
    /// The signature for the delegation.
    #[serde(alias = "s")]
    pub signature: ByteBufB64,
}

#[derive(CandidType, Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub struct SignInResponse {
    /// The session expiration time in nanoseconds since the UNIX epoch. This is the time at which
    /// the delegation will no longer be valid.
    pub expiration: u64,
    /// The user canister public key. This key is used to derive the user principal.
    pub user_key: ByteBufB64,
    /// The seed component used to derive the user key.
    pub seed: ByteBufB64,
}

/// DelegationCompact is a compact representation of a [`Delegation`].
/// It is used to reduce the size of the delegation when it is serialized.
#[derive(CandidType, Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub struct DelegationCompact {
    /// The delegated-to key, encoded as `p` in compact JSON/CBOR maps.
    #[serde(rename = "p", alias = "pubkey")]
    pub pubkey: ByteBufB64,
    /// The nanosecond UNIX timestamp after which the delegation is invalid,
    /// encoded as `e` in compact JSON/CBOR maps.
    #[serde(rename = "e", alias = "expiration")]
    pub expiration: u64,
    /// Optional canister targets, encoded as `t` in compact JSON/CBOR maps.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    #[serde(rename = "t", alias = "targets")]
    pub targets: Option<Vec<Principal>>,
}

/// SignedDelegationCompact is a compact representation of a [`SignedDelegation`].
/// It is used to reduce the size of the delegation when it is serialized.
#[derive(CandidType, Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub struct SignedDelegationCompact {
    /// The compact delegation payload, encoded as `d`.
    #[serde(rename = "d", alias = "delegation")]
    pub delegation: DelegationCompact,
    /// The signature over the delegation message, encoded as `s`.
    #[serde(rename = "s", alias = "signature")]
    pub signature: ByteBufB64,
}

impl From<DelegationCompact> for Delegation {
    fn from(d: DelegationCompact) -> Self {
        Self {
            pubkey: d.pubkey,
            expiration: d.expiration,
            targets: d.targets,
        }
    }
}

impl From<Delegation> for DelegationCompact {
    fn from(d: Delegation) -> Self {
        Self {
            pubkey: d.pubkey,
            expiration: d.expiration,
            targets: d.targets,
        }
    }
}

impl From<SignedDelegationCompact> for SignedDelegation {
    fn from(d: SignedDelegationCompact) -> Self {
        Self {
            delegation: d.delegation.into(),
            signature: d.signature,
        }
    }
}

impl From<SignedDelegation> for SignedDelegationCompact {
    fn from(d: SignedDelegation) -> Self {
        Self {
            delegation: d.delegation.into(),
            signature: d.signature,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_delegation_format() {
        let d = Delegation {
            pubkey: ByteBufB64(vec![1, 2, 3, 4]),
            expiration: 99,
            targets: Some(vec![Principal::management_canister()]),
        };

        let data = serde_json::to_string(&d).unwrap();
        println!("{data}");
        assert_eq!(
            data,
            r#"{"pubkey":"AQIDBA==","expiration":99,"targets":["aaaaa-aa"]}"#
        );
        let d1: Delegation = serde_json::from_str(&data).unwrap();
        assert_eq!(d, d1);

        let mut data = Vec::new();
        cbor2::to_writer(&d, &mut data).unwrap();
        println!("{}", hex::encode(&data));
        assert_eq!(
            data,
            hex::decode("a3667075626b657944010203046a65787069726174696f6e186367746172676574738140")
                .unwrap()
        );
        let d1: Delegation = cbor_from_slice(&data[..]).unwrap();
        assert_eq!(d, d1);
    }

    #[test]
    fn test_compact_conversion_and_candid_roundtrips() {
        let delegation = Delegation {
            pubkey: ByteBufB64(vec![1, 2, 3, 4]),
            expiration: 99,
            targets: Some(vec![Principal::management_canister()]),
        };
        let compact: DelegationCompact = delegation.clone().into();
        assert_eq!(compact.pubkey, delegation.pubkey);
        assert_eq!(compact.expiration, delegation.expiration);
        assert_eq!(compact.targets, delegation.targets);
        let expanded: Delegation = compact.clone().into();
        assert_eq!(expanded, delegation);

        let signed = SignedDelegation {
            delegation,
            signature: ByteBufB64(vec![5, 6, 7, 8]),
        };
        let compact_signed: SignedDelegationCompact = signed.clone().into();
        assert_eq!(compact_signed.delegation, compact);
        assert_eq!(compact_signed.signature, signed.signature);
        let expanded_signed: SignedDelegation = compact_signed.into();
        assert_eq!(expanded_signed, signed);

        let sign_in = SignInResponse {
            expiration: 123,
            user_key: ByteBufB64(vec![9, 10]),
            seed: ByteBufB64(vec![11, 12]),
        };
        let encoded = candid::encode_one(sign_in.clone()).unwrap();
        let decoded: SignInResponse = candid::decode_one(&encoded).unwrap();
        assert_eq!(decoded, sign_in);

        let encoded = candid::encode_one(signed.clone()).unwrap();
        let decoded: SignedDelegation = candid::decode_one(&encoded).unwrap();
        assert_eq!(decoded, signed);
    }

    #[test]
    fn test_candid_type_metadata_is_available() {
        let types = [
            Delegation::ty(),
            SignedDelegation::ty(),
            SignInResponse::ty(),
            DelegationCompact::ty(),
            SignedDelegationCompact::ty(),
        ];

        assert!(types.iter().all(|ty| !format!("{ty:?}").is_empty()));
    }
}