mod revocation;
mod rules;
mod verified;
pub use revocation::{LoginRevocationChange, LoginRevocationError, MemoryLoginRevocations};
pub use rules::{PortableClaimsContext, PortableClaimsError, validate_claims_context};
pub use verified::VerifiedPortableClaims;
use crate::canonical::{CanonicalAuthError, encode_principal, encode_role_grants};
use crate::certificate_issuance::{GrantValidationError, validate_grants};
use ic_auth_protocol_types::{
AudienceId, CanonicalId, DelegatedRoleGrant, PortableClaims, PortableMechanism, Principal,
};
use sha2::{Digest, Sha256};
use thiserror::Error;
const DOMAIN: &[u8] = b"IC-AUTH-PORTABLE-CREDENTIAL\0";
const SCHEMA: u16 = 1;
const MAX_GRANTS: usize = 16;
const MAX_SCOPES: usize = 32;
const MAX_LABEL_BYTES: usize = 64;
pub const MAX_CLAIMS_BYTES: usize = 65_536;
#[derive(Debug, Error, PartialEq, Eq)]
pub enum PortableEncodingError {
#[error("portable claims exceed their byte bound")]
TooLarge,
#[error("truncated portable claims")]
Truncated,
#[error("wrong portable domain, schema or mechanism")]
WrongDomain,
#[error("portable claims contain trailing bytes")]
TrailingBytes,
#[error("invalid portable principal, label or count")]
InvalidShape,
#[error("anonymous account subject")]
AnonymousSubject,
#[error(transparent)]
Grants(#[from] GrantValidationError),
#[error(transparent)]
Canonical(#[from] CanonicalAuthError),
}
pub fn encode_claims(claims: &PortableClaims) -> Result<Vec<u8>, PortableEncodingError> {
validate_shape(claims)?;
let mut out = Vec::new();
out.extend_from_slice(DOMAIN);
out.extend_from_slice(&SCHEMA.to_be_bytes());
out.push(match claims.mechanism {
PortableMechanism::Certified => 0,
PortableMechanism::Ed25519 => 1,
});
out.extend_from_slice(claims.audience.canonical_network_id.as_bytes());
out.extend_from_slice(claims.audience.fleet_id.as_bytes());
encode_principal(&mut out, claims.issuer)?;
out.extend_from_slice(&claims.key_binding_id);
out.extend_from_slice(&claims.browser_public_key);
encode_principal(&mut out, claims.account_subject)?;
for time in [
claims.login_started_at_ns,
claims.login_deadline_ns,
claims.issued_at_ns,
claims.admission_expires_at_ns,
] {
out.extend_from_slice(&time.to_be_bytes());
}
out.extend_from_slice(&claims.nonce);
encode_role_grants(&mut out, &claims.grants)?;
if out.len() > MAX_CLAIMS_BYTES {
return Err(PortableEncodingError::TooLarge);
}
Ok(out)
}
pub fn decode_claims(bytes: &[u8]) -> Result<PortableClaims, PortableEncodingError> {
if bytes.len() > MAX_CLAIMS_BYTES {
return Err(PortableEncodingError::TooLarge);
}
let mut input = Reader(bytes);
if input.take(DOMAIN.len())? != DOMAIN || input.array::<2>()? != SCHEMA.to_be_bytes() {
return Err(PortableEncodingError::WrongDomain);
}
let mechanism = match input.array::<1>()? {
[0] => PortableMechanism::Certified,
[1] => PortableMechanism::Ed25519,
_ => return Err(PortableEncodingError::WrongDomain),
};
let audience = AudienceId {
canonical_network_id: CanonicalId::from_bytes(input.array()?),
fleet_id: CanonicalId::from_bytes(input.array()?),
};
let issuer = input.principal()?;
let key_binding_id = input.array()?;
let browser_public_key = input.array()?;
let account_subject = input.principal()?;
let login_started_at_ns = u64::from_be_bytes(input.array()?);
let login_deadline_ns = u64::from_be_bytes(input.array()?);
let issued_at_ns = u64::from_be_bytes(input.array()?);
let admission_expires_at_ns = u64::from_be_bytes(input.array()?);
let nonce = input.array()?;
let count = input.count(MAX_GRANTS)?;
let mut grants = Vec::with_capacity(count);
for _ in 0..count {
let target = input
.label()?
.parse()
.map_err(|_| PortableEncodingError::InvalidShape)?;
let count = input.count(MAX_SCOPES)?;
let mut scopes = Vec::with_capacity(count);
for _ in 0..count {
scopes.push(input.label()?.to_owned());
}
grants.push(DelegatedRoleGrant { target, scopes });
}
if !input.0.is_empty() {
return Err(PortableEncodingError::TrailingBytes);
}
let claims = PortableClaims {
mechanism,
audience,
issuer,
key_binding_id,
browser_public_key,
account_subject,
login_started_at_ns,
login_deadline_ns,
issued_at_ns,
admission_expires_at_ns,
nonce,
grants,
};
validate_shape(&claims)?;
Ok(claims)
}
pub fn claims_hash(claims: &PortableClaims) -> Result<[u8; 32], PortableEncodingError> {
Ok(Sha256::digest(encode_claims(claims)?).into())
}
pub fn browser_principal(public_key: &[u8; 32]) -> Principal {
let mut der = [0; 44];
der[..12].copy_from_slice(&[
0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0,
]);
der[12..].copy_from_slice(public_key);
Principal::self_authenticating(der)
}
fn validate_shape(claims: &PortableClaims) -> Result<(), PortableEncodingError> {
if claims.account_subject == Principal::anonymous() {
return Err(PortableEncodingError::AnonymousSubject);
}
validate_grants(&claims.grants, MAX_GRANTS, MAX_SCOPES)?;
if claims
.grants
.iter()
.any(|grant| grant.target.as_str().len() > MAX_LABEL_BYTES)
{
return Err(PortableEncodingError::InvalidShape);
}
Ok(())
}
struct Reader<'a>(&'a [u8]);
impl<'a> Reader<'a> {
fn take(&mut self, len: usize) -> Result<&'a [u8], PortableEncodingError> {
let (head, tail) = self
.0
.split_at_checked(len)
.ok_or(PortableEncodingError::Truncated)?;
self.0 = tail;
Ok(head)
}
fn array<const N: usize>(&mut self) -> Result<[u8; N], PortableEncodingError> {
self.take(N)?
.try_into()
.map_err(|_| PortableEncodingError::Truncated)
}
fn length(&mut self, max: usize) -> Result<usize, PortableEncodingError> {
let len = u32::from_be_bytes(self.array()?);
let len = usize::try_from(len).map_err(|_| PortableEncodingError::InvalidShape)?;
if len > max {
return Err(PortableEncodingError::InvalidShape);
}
Ok(len)
}
fn count(&mut self, max: usize) -> Result<usize, PortableEncodingError> {
let count = self.length(max)?;
if count == 0 {
return Err(PortableEncodingError::InvalidShape);
}
Ok(count)
}
fn principal(&mut self) -> Result<Principal, PortableEncodingError> {
let len = self.length(29)?;
Ok(Principal::from_slice(self.take(len)?))
}
fn label(&mut self) -> Result<&'a str, PortableEncodingError> {
let len = self.count(MAX_LABEL_BYTES)?;
std::str::from_utf8(self.take(len)?).map_err(|_| PortableEncodingError::InvalidShape)
}
}