use std::{cmp::Ordering, fmt};
use sha2::{Digest, Sha256};
use thiserror::Error;
pub const SHA256_BYTES: usize = 32;
pub const PUBLIC_KEY_BYTES: usize = 32;
pub const NONCE_BYTES: usize = 16;
pub const MAX_UNIX_TIME: u64 = 253_402_300_799;
pub const MAX_ID_LIST_ITEMS: usize = 64;
pub const CONFORMANCE_EVIDENCE_DOMAIN: &[u8] =
b"steadymux.catalog-authority/conformance-evidence/v1\0";
pub const PROMOTION_APPLY_DOMAIN: &[u8] = b"steadymux.catalog-authority/promotion-apply/v1\0";
pub const PROMOTION_ROLLBACK_DOMAIN: &[u8] = b"steadymux.catalog-authority/promotion-rollback/v1\0";
pub const TRUST_POLICY_UPDATE_DOMAIN: &[u8] =
b"steadymux.catalog-authority/trust-policy-update/v1\0";
pub const TRUST_POLICY_BOOTSTRAP_DOMAIN: &[u8] =
b"steadymux.catalog-authority/trust-policy-bootstrap/v1\0";
pub const CONFORMANCE_EVIDENCE_MAX_PAYLOAD_BYTES: usize = 4 * 1024;
pub const PROMOTION_APPLY_MAX_PAYLOAD_BYTES: usize = 4 * 1024;
pub const PROMOTION_ROLLBACK_MAX_PAYLOAD_BYTES: usize = 2 * 1024;
pub const TRUST_POLICY_UPDATE_MAX_PAYLOAD_BYTES: usize = 8 * 1024;
pub const TRUST_POLICY_BOOTSTRAP_MAX_PAYLOAD_BYTES: usize = 4 * 1024;
const CONFORMANCE_MAX_LIFETIME_SECONDS: u64 = 86_400;
const AUTHORIZATION_MAX_LIFETIME_SECONDS: u64 = 300;
const ROLLBACK_MAX_WINDOW_SECONDS: u64 = 86_400;
pub type Sha256Digest = [u8; SHA256_BYTES];
pub type KeyId = [u8; SHA256_BYTES];
pub type PublicKey = [u8; PUBLIC_KEY_BYTES];
pub type Nonce16 = [u8; NONCE_BYTES];
macro_rules! ascii_newtype {
($name:ident, $kind:literal, $maximum:expr, $validator:ident) => {
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct $name(String);
impl $name {
pub fn parse(value: impl Into<String>) -> Result<Self, CatalogAuthorityPayloadError> {
let value = value.into();
validate_ascii_value($kind, value.as_bytes(), $maximum, $validator)?;
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_string(self) -> String {
self.0
}
}
impl TryFrom<String> for $name {
type Error = CatalogAuthorityPayloadError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::parse(value)
}
}
impl TryFrom<&str> for $name {
type Error = CatalogAuthorityPayloadError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::parse(value)
}
}
impl AsRef<str> for $name {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for $name {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
};
}
ascii_newtype!(Id128, "ID128", 128, valid_id128);
ascii_newtype!(Model256, "MODEL256", 256, valid_model256);
ascii_newtype!(Ref256, "REF256", 256, valid_ref256);
impl Id128 {
fn encoded_cmp(&self, other: &Self) -> Ordering {
self.0
.len()
.cmp(&other.0.len())
.then_with(|| self.0.as_bytes().cmp(other.0.as_bytes()))
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct IdList(Vec<Id128>);
impl IdList {
pub fn new(values: Vec<Id128>) -> Result<Self, CatalogAuthorityPayloadError> {
validate_id_list(&values)?;
Ok(Self(values))
}
pub fn as_slice(&self) -> &[Id128] {
&self.0
}
pub fn into_vec(self) -> Vec<Id128> {
self.0
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn len(&self) -> usize {
self.0.len()
}
}
impl TryFrom<Vec<Id128>> for IdList {
type Error = CatalogAuthorityPayloadError;
fn try_from(values: Vec<Id128>) -> Result<Self, Self::Error> {
Self::new(values)
}
}
macro_rules! wire_enum {
($name:ident, $kind:literal, { $($variant:ident = $value:literal),+ $(,)? }) => {
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum $name {
$($variant = $value),+
}
impl $name {
pub const fn wire_value(self) -> u8 {
self as u8
}
}
impl WireEnum for $name {
const KIND: &'static str = $kind;
fn from_wire(value: u8) -> Result<Self, CatalogAuthorityPayloadError> {
match value {
$($value => Ok(Self::$variant),)+
_ => Err(CatalogAuthorityPayloadError::UnknownEnumValue {
kind: Self::KIND,
value,
}),
}
}
fn to_wire(self) -> u8 {
self.wire_value()
}
}
impl TryFrom<u8> for $name {
type Error = CatalogAuthorityPayloadError;
fn try_from(value: u8) -> Result<Self, Self::Error> {
Self::from_wire(value)
}
}
};
}
wire_enum!(EvidenceClass, "EVIDENCE_CLASS", {
Fixture = 0x01,
Live = 0x02,
});
wire_enum!(EvidenceResult, "EVIDENCE_RESULT", {
Pass = 0x01,
Fail = 0x02,
});
wire_enum!(Environment, "ENVIRONMENT", {
Production = 0x01,
});
wire_enum!(AnchorRole, "ANCHOR_ROLE", {
TrustPolicyAdmin = 0x01,
OperatorApply = 0x02,
ConformanceAttest = 0x03,
Rollback = 0x04,
});
wire_enum!(AnchorStatus, "ANCHOR_STATUS", {
Pending = 0x01,
Active = 0x02,
Retired = 0x03,
Revoked = 0x04,
});
wire_enum!(TrustUpdateAction, "TRUST_UPDATE_ACTION", {
Add = 0x01,
Activate = 0x02,
Retire = 0x03,
Revoke = 0x04,
ReplaceScopes = 0x05,
});
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ConformanceEvidencePayload {
pub signing_key_id: KeyId,
pub trust_policy_id: Id128,
pub trust_policy_sha256: Sha256Digest,
pub environment: Environment,
pub evidence_id: Id128,
pub provider_id: Id128,
pub phase_id: Id128,
pub account_authority_id: Id128,
pub configuration_authority_id: Id128,
pub provider_model_id: Model256,
pub model_sha256: Sha256Digest,
pub catalog_ref: Ref256,
pub catalog_version: Id128,
pub catalog_sha256: Sha256Digest,
pub policy_version: Id128,
pub policy_sha256: Sha256Digest,
pub conformance_suite_version: Id128,
pub conformance_suite_sha256: Sha256Digest,
pub source_identity: Ref256,
pub source_sha256: Sha256Digest,
pub evaluator_id: Id128,
pub evaluator_version: Id128,
pub evaluator_sha256: Sha256Digest,
pub evidence_class: EvidenceClass,
pub execution_started_at: u64,
pub execution_ended_at: u64,
pub result: EvidenceResult,
pub artifact_manifest_sha256: Sha256Digest,
pub issued_at: u64,
pub not_before: u64,
pub expires_at: u64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PromotionApplyPayload {
pub signing_key_id: KeyId,
pub trust_policy_id: Id128,
pub trust_policy_sha256: Sha256Digest,
pub environment: Environment,
pub promotion_id: Id128,
pub provider_id: Id128,
pub phase_id: Id128,
pub account_ref: Id128,
pub catalog_ref: Ref256,
pub provider_model_id: Model256,
pub model_sha256: Sha256Digest,
pub catalog_version: Id128,
pub catalog_sha256: Sha256Digest,
pub policy_version: Id128,
pub policy_sha256: Sha256Digest,
pub conformance_suite_sha256: Sha256Digest,
pub conformance_envelope_sha256: Sha256Digest,
pub evidence_class: EvidenceClass,
pub configuration_authority_id: Id128,
pub configuration_sha256: Sha256Digest,
pub configuration_authority_sha256: Sha256Digest,
pub expected_state_generation: u64,
pub state_preimage_sha256: Sha256Digest,
pub state_postimage_sha256: Sha256Digest,
pub rollback_snapshot_sha256: Sha256Digest,
pub authorization_nonce: Nonce16,
pub issued_at: u64,
pub not_before: u64,
pub expires_at: u64,
pub rollback_not_after: u64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PromotionRollbackPayload {
pub signing_key_id: KeyId,
pub trust_policy_id: Id128,
pub trust_policy_sha256: Sha256Digest,
pub environment: Environment,
pub rollback_id: Id128,
pub promotion_id: Id128,
pub applied_receipt_sha256: Sha256Digest,
pub configuration_authority_id: Id128,
pub configuration_authority_sha256: Sha256Digest,
pub expected_state_generation: u64,
pub state_preimage_sha256: Sha256Digest,
pub rollback_snapshot_sha256: Sha256Digest,
pub state_postimage_sha256: Sha256Digest,
pub authorization_nonce: Nonce16,
pub issued_at: u64,
pub not_before: u64,
pub expires_at: u64,
pub original_rollback_not_after: u64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TrustPolicyUpdatePayload {
pub signing_admin_key_id: KeyId,
pub trust_policy_id: Id128,
pub environment: Environment,
pub update_id: Id128,
pub action: TrustUpdateAction,
pub expected_authority_generation: u64,
pub expected_trust_policy_generation: u64,
pub authority_state_preimage_sha256: Sha256Digest,
pub authority_state_postimage_sha256: Sha256Digest,
pub old_trust_policy_sha256: Sha256Digest,
pub new_trust_policy_sha256: Sha256Digest,
pub subject_key_id: KeyId,
pub subject_public_key: PublicKey,
pub subject_role: AnchorRole,
pub new_status: AnchorStatus,
pub provider_scopes: IdList,
pub phase_scopes: IdList,
pub account_authority_scopes: IdList,
pub configuration_authority_scopes: IdList,
pub policy_scopes: IdList,
pub activation_at: u64,
pub retirement_at: u64,
pub authorization_nonce: Nonce16,
pub issued_at: u64,
pub not_before: u64,
pub expires_at: u64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TrustPolicyBootstrapPayload {
pub bootstrap_key_id: KeyId,
pub bootstrap_public_key: PublicKey,
pub trust_policy_id: Id128,
pub authority_store_id: Id128,
pub environment: Environment,
pub expected_authority_generation: u64,
pub expected_trust_policy_generation: u64,
pub uninitialized_state_sha256: Sha256Digest,
pub initial_trust_policy_sha256: Sha256Digest,
pub provider_scopes: IdList,
pub phase_scopes: IdList,
pub account_authority_scopes: IdList,
pub configuration_authority_scopes: IdList,
pub policy_scopes: IdList,
pub authorization_nonce: Nonce16,
pub issued_at: u64,
pub not_before: u64,
pub expires_at: u64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CatalogAuthorityPayload {
ConformanceEvidence(ConformanceEvidencePayload),
PromotionApply(PromotionApplyPayload),
PromotionRollback(PromotionRollbackPayload),
TrustPolicyUpdate(TrustPolicyUpdatePayload),
TrustPolicyBootstrap(TrustPolicyBootstrapPayload),
}
impl ConformanceEvidencePayload {
pub fn decode(bytes: &[u8]) -> Result<Self, CatalogAuthorityPayloadError> {
let mut reader = Reader::for_kind(bytes, CatalogAuthorityPayloadKind::ConformanceEvidence)?;
let payload = Self {
signing_key_id: reader.read_fixed()?,
trust_policy_id: reader.read_id128()?,
trust_policy_sha256: reader.read_fixed()?,
environment: reader.read_enum()?,
evidence_id: reader.read_id128()?,
provider_id: reader.read_id128()?,
phase_id: reader.read_id128()?,
account_authority_id: reader.read_id128()?,
configuration_authority_id: reader.read_id128()?,
provider_model_id: reader.read_model256()?,
model_sha256: reader.read_fixed()?,
catalog_ref: reader.read_ref256()?,
catalog_version: reader.read_id128()?,
catalog_sha256: reader.read_fixed()?,
policy_version: reader.read_id128()?,
policy_sha256: reader.read_fixed()?,
conformance_suite_version: reader.read_id128()?,
conformance_suite_sha256: reader.read_fixed()?,
source_identity: reader.read_ref256()?,
source_sha256: reader.read_fixed()?,
evaluator_id: reader.read_id128()?,
evaluator_version: reader.read_id128()?,
evaluator_sha256: reader.read_fixed()?,
evidence_class: reader.read_enum()?,
execution_started_at: reader.read_unix_time()?,
execution_ended_at: reader.read_unix_time()?,
result: reader.read_enum()?,
artifact_manifest_sha256: reader.read_fixed()?,
issued_at: reader.read_unix_time()?,
not_before: reader.read_unix_time()?,
expires_at: reader.read_unix_time()?,
};
reader.finish()?;
require_canonical(bytes, &payload.encode()?)?;
Ok(payload)
}
pub fn encode(&self) -> Result<Vec<u8>, CatalogAuthorityPayloadError> {
validate_conformance_times(
self.execution_started_at,
self.execution_ended_at,
self.issued_at,
self.not_before,
self.expires_at,
)?;
let mut writer = Writer::new(PayloadKind::ConformanceEvidence);
writer.write_fixed(&self.signing_key_id);
writer.write_id128(&self.trust_policy_id)?;
writer.write_fixed(&self.trust_policy_sha256);
writer.write_enum(self.environment);
writer.write_id128(&self.evidence_id)?;
writer.write_id128(&self.provider_id)?;
writer.write_id128(&self.phase_id)?;
writer.write_id128(&self.account_authority_id)?;
writer.write_id128(&self.configuration_authority_id)?;
writer.write_model256(&self.provider_model_id)?;
writer.write_fixed(&self.model_sha256);
writer.write_ref256(&self.catalog_ref)?;
writer.write_id128(&self.catalog_version)?;
writer.write_fixed(&self.catalog_sha256);
writer.write_id128(&self.policy_version)?;
writer.write_fixed(&self.policy_sha256);
writer.write_id128(&self.conformance_suite_version)?;
writer.write_fixed(&self.conformance_suite_sha256);
writer.write_ref256(&self.source_identity)?;
writer.write_fixed(&self.source_sha256);
writer.write_id128(&self.evaluator_id)?;
writer.write_id128(&self.evaluator_version)?;
writer.write_fixed(&self.evaluator_sha256);
writer.write_enum(self.evidence_class);
writer.write_unix_time(self.execution_started_at)?;
writer.write_unix_time(self.execution_ended_at)?;
writer.write_enum(self.result);
writer.write_fixed(&self.artifact_manifest_sha256);
writer.write_unix_time(self.issued_at)?;
writer.write_unix_time(self.not_before)?;
writer.write_unix_time(self.expires_at)?;
writer.finish(PayloadKind::ConformanceEvidence)
}
}
impl PromotionApplyPayload {
pub fn decode(bytes: &[u8]) -> Result<Self, CatalogAuthorityPayloadError> {
let mut reader = Reader::for_kind(bytes, CatalogAuthorityPayloadKind::PromotionApply)?;
let payload = Self {
signing_key_id: reader.read_fixed()?,
trust_policy_id: reader.read_id128()?,
trust_policy_sha256: reader.read_fixed()?,
environment: reader.read_enum()?,
promotion_id: reader.read_id128()?,
provider_id: reader.read_id128()?,
phase_id: reader.read_id128()?,
account_ref: reader.read_id128()?,
catalog_ref: reader.read_ref256()?,
provider_model_id: reader.read_model256()?,
model_sha256: reader.read_fixed()?,
catalog_version: reader.read_id128()?,
catalog_sha256: reader.read_fixed()?,
policy_version: reader.read_id128()?,
policy_sha256: reader.read_fixed()?,
conformance_suite_sha256: reader.read_fixed()?,
conformance_envelope_sha256: reader.read_fixed()?,
evidence_class: reader.read_enum()?,
configuration_authority_id: reader.read_id128()?,
configuration_sha256: reader.read_fixed()?,
configuration_authority_sha256: reader.read_fixed()?,
expected_state_generation: reader.read_u64()?,
state_preimage_sha256: reader.read_fixed()?,
state_postimage_sha256: reader.read_fixed()?,
rollback_snapshot_sha256: reader.read_fixed()?,
authorization_nonce: reader.read_fixed()?,
issued_at: reader.read_unix_time()?,
not_before: reader.read_unix_time()?,
expires_at: reader.read_unix_time()?,
rollback_not_after: reader.read_unix_time()?,
};
reader.finish()?;
require_canonical(bytes, &payload.encode()?)?;
Ok(payload)
}
pub fn encode(&self) -> Result<Vec<u8>, CatalogAuthorityPayloadError> {
validate_generation("expected_state_generation", self.expected_state_generation)?;
if self.state_postimage_sha256 == self.state_preimage_sha256 {
return Err(CatalogAuthorityPayloadError::InvalidDigestRelationship {
kind: PayloadKind::PromotionApply.name(),
relationship: "state_postimage_sha256 must differ from state_preimage_sha256",
});
}
if self.rollback_snapshot_sha256 != self.state_preimage_sha256 {
return Err(CatalogAuthorityPayloadError::InvalidDigestRelationship {
kind: PayloadKind::PromotionApply.name(),
relationship: "rollback_snapshot_sha256 must equal state_preimage_sha256",
});
}
validate_authorization_window(
PayloadKind::PromotionApply,
self.issued_at,
self.not_before,
self.expires_at,
)?;
validate_unix_time(self.rollback_not_after)?;
if self.expires_at >= self.rollback_not_after {
return Err(CatalogAuthorityPayloadError::InvalidTimeRelationship {
kind: PayloadKind::PromotionApply.name(),
relationship: "expires_at must precede rollback_not_after",
});
}
let rollback_deadline = self
.issued_at
.checked_add(ROLLBACK_MAX_WINDOW_SECONDS)
.ok_or(CatalogAuthorityPayloadError::ArithmeticOverflow {
relationship: "issued_at + rollback maximum window",
})?;
if self.rollback_not_after > rollback_deadline {
return Err(CatalogAuthorityPayloadError::InvalidTimeRelationship {
kind: PayloadKind::PromotionApply.name(),
relationship: "rollback_not_after exceeds issued_at by more than 86400 seconds",
});
}
let mut writer = Writer::new(PayloadKind::PromotionApply);
writer.write_fixed(&self.signing_key_id);
writer.write_id128(&self.trust_policy_id)?;
writer.write_fixed(&self.trust_policy_sha256);
writer.write_enum(self.environment);
writer.write_id128(&self.promotion_id)?;
writer.write_id128(&self.provider_id)?;
writer.write_id128(&self.phase_id)?;
writer.write_id128(&self.account_ref)?;
writer.write_ref256(&self.catalog_ref)?;
writer.write_model256(&self.provider_model_id)?;
writer.write_fixed(&self.model_sha256);
writer.write_id128(&self.catalog_version)?;
writer.write_fixed(&self.catalog_sha256);
writer.write_id128(&self.policy_version)?;
writer.write_fixed(&self.policy_sha256);
writer.write_fixed(&self.conformance_suite_sha256);
writer.write_fixed(&self.conformance_envelope_sha256);
writer.write_enum(self.evidence_class);
writer.write_id128(&self.configuration_authority_id)?;
writer.write_fixed(&self.configuration_sha256);
writer.write_fixed(&self.configuration_authority_sha256);
writer.write_u64(self.expected_state_generation);
writer.write_fixed(&self.state_preimage_sha256);
writer.write_fixed(&self.state_postimage_sha256);
writer.write_fixed(&self.rollback_snapshot_sha256);
writer.write_fixed(&self.authorization_nonce);
writer.write_unix_time(self.issued_at)?;
writer.write_unix_time(self.not_before)?;
writer.write_unix_time(self.expires_at)?;
writer.write_unix_time(self.rollback_not_after)?;
writer.finish(PayloadKind::PromotionApply)
}
}
impl PromotionRollbackPayload {
pub fn decode(bytes: &[u8]) -> Result<Self, CatalogAuthorityPayloadError> {
let mut reader = Reader::for_kind(bytes, CatalogAuthorityPayloadKind::PromotionRollback)?;
let payload = Self {
signing_key_id: reader.read_fixed()?,
trust_policy_id: reader.read_id128()?,
trust_policy_sha256: reader.read_fixed()?,
environment: reader.read_enum()?,
rollback_id: reader.read_id128()?,
promotion_id: reader.read_id128()?,
applied_receipt_sha256: reader.read_fixed()?,
configuration_authority_id: reader.read_id128()?,
configuration_authority_sha256: reader.read_fixed()?,
expected_state_generation: reader.read_u64()?,
state_preimage_sha256: reader.read_fixed()?,
rollback_snapshot_sha256: reader.read_fixed()?,
state_postimage_sha256: reader.read_fixed()?,
authorization_nonce: reader.read_fixed()?,
issued_at: reader.read_unix_time()?,
not_before: reader.read_unix_time()?,
expires_at: reader.read_unix_time()?,
original_rollback_not_after: reader.read_unix_time()?,
};
reader.finish()?;
require_canonical(bytes, &payload.encode()?)?;
Ok(payload)
}
pub fn encode(&self) -> Result<Vec<u8>, CatalogAuthorityPayloadError> {
validate_generation("expected_state_generation", self.expected_state_generation)?;
if self.state_postimage_sha256 == self.state_preimage_sha256 {
return Err(CatalogAuthorityPayloadError::InvalidDigestRelationship {
kind: PayloadKind::PromotionRollback.name(),
relationship: "state_postimage_sha256 must differ from state_preimage_sha256",
});
}
if self.rollback_snapshot_sha256 == self.state_preimage_sha256
|| self.rollback_snapshot_sha256 == self.state_postimage_sha256
{
return Err(CatalogAuthorityPayloadError::InvalidDigestRelationship {
kind: PayloadKind::PromotionRollback.name(),
relationship: "rollback snapshot, state preimage, and state postimage must differ",
});
}
validate_authorization_window(
PayloadKind::PromotionRollback,
self.issued_at,
self.not_before,
self.expires_at,
)?;
validate_unix_time(self.original_rollback_not_after)?;
if self.original_rollback_not_after < self.expires_at {
return Err(CatalogAuthorityPayloadError::InvalidTimeRelationship {
kind: PayloadKind::PromotionRollback.name(),
relationship: "original_rollback_not_after must be at least expires_at",
});
}
let mut writer = Writer::new(PayloadKind::PromotionRollback);
writer.write_fixed(&self.signing_key_id);
writer.write_id128(&self.trust_policy_id)?;
writer.write_fixed(&self.trust_policy_sha256);
writer.write_enum(self.environment);
writer.write_id128(&self.rollback_id)?;
writer.write_id128(&self.promotion_id)?;
writer.write_fixed(&self.applied_receipt_sha256);
writer.write_id128(&self.configuration_authority_id)?;
writer.write_fixed(&self.configuration_authority_sha256);
writer.write_u64(self.expected_state_generation);
writer.write_fixed(&self.state_preimage_sha256);
writer.write_fixed(&self.rollback_snapshot_sha256);
writer.write_fixed(&self.state_postimage_sha256);
writer.write_fixed(&self.authorization_nonce);
writer.write_unix_time(self.issued_at)?;
writer.write_unix_time(self.not_before)?;
writer.write_unix_time(self.expires_at)?;
writer.write_unix_time(self.original_rollback_not_after)?;
writer.finish(PayloadKind::PromotionRollback)
}
}
impl TrustPolicyUpdatePayload {
pub fn decode(bytes: &[u8]) -> Result<Self, CatalogAuthorityPayloadError> {
let mut reader = Reader::for_kind(bytes, CatalogAuthorityPayloadKind::TrustPolicyUpdate)?;
let payload = Self {
signing_admin_key_id: reader.read_fixed()?,
trust_policy_id: reader.read_id128()?,
environment: reader.read_enum()?,
update_id: reader.read_id128()?,
action: reader.read_enum()?,
expected_authority_generation: reader.read_u64()?,
expected_trust_policy_generation: reader.read_u64()?,
authority_state_preimage_sha256: reader.read_fixed()?,
authority_state_postimage_sha256: reader.read_fixed()?,
old_trust_policy_sha256: reader.read_fixed()?,
new_trust_policy_sha256: reader.read_fixed()?,
subject_key_id: reader.read_fixed()?,
subject_public_key: reader.read_fixed()?,
subject_role: reader.read_enum()?,
new_status: reader.read_enum()?,
provider_scopes: reader.read_id_list()?,
phase_scopes: reader.read_id_list()?,
account_authority_scopes: reader.read_id_list()?,
configuration_authority_scopes: reader.read_id_list()?,
policy_scopes: reader.read_id_list()?,
activation_at: reader.read_unix_time()?,
retirement_at: reader.read_u64()?,
authorization_nonce: reader.read_fixed()?,
issued_at: reader.read_unix_time()?,
not_before: reader.read_unix_time()?,
expires_at: reader.read_unix_time()?,
};
reader.finish()?;
require_canonical(bytes, &payload.encode()?)?;
Ok(payload)
}
pub fn encode(&self) -> Result<Vec<u8>, CatalogAuthorityPayloadError> {
validate_generation(
"expected_authority_generation",
self.expected_authority_generation,
)?;
validate_generation(
"expected_trust_policy_generation",
self.expected_trust_policy_generation,
)?;
if self.old_trust_policy_sha256 == self.new_trust_policy_sha256 {
return Err(CatalogAuthorityPayloadError::InvalidDigestRelationship {
kind: PayloadKind::TrustPolicyUpdate.name(),
relationship: "new_trust_policy_sha256 must differ from old_trust_policy_sha256",
});
}
if self.authority_state_preimage_sha256 == self.authority_state_postimage_sha256 {
return Err(CatalogAuthorityPayloadError::InvalidDigestRelationship {
kind: PayloadKind::TrustPolicyUpdate.name(),
relationship: "authority_state_postimage_sha256 must differ from authority_state_preimage_sha256",
});
}
validate_key_id(
"subject_key_id",
&self.subject_key_id,
&self.subject_public_key,
)?;
validate_update_transition(self.action, self.new_status, self.retirement_at)?;
validate_id_list(self.provider_scopes.as_slice())?;
validate_id_list(self.phase_scopes.as_slice())?;
validate_id_list(self.account_authority_scopes.as_slice())?;
validate_id_list(self.configuration_authority_scopes.as_slice())?;
validate_id_list(self.policy_scopes.as_slice())?;
validate_unix_time(self.activation_at)?;
if self.retirement_at != 0 {
validate_unix_time(self.retirement_at)?;
if self.retirement_at <= self.activation_at {
return Err(CatalogAuthorityPayloadError::InvalidTimeRelationship {
kind: PayloadKind::TrustPolicyUpdate.name(),
relationship: "retirement_at must be zero or later than activation_at",
});
}
}
validate_authorization_window(
PayloadKind::TrustPolicyUpdate,
self.issued_at,
self.not_before,
self.expires_at,
)?;
let mut writer = Writer::new(PayloadKind::TrustPolicyUpdate);
writer.write_fixed(&self.signing_admin_key_id);
writer.write_id128(&self.trust_policy_id)?;
writer.write_enum(self.environment);
writer.write_id128(&self.update_id)?;
writer.write_enum(self.action);
writer.write_u64(self.expected_authority_generation);
writer.write_u64(self.expected_trust_policy_generation);
writer.write_fixed(&self.authority_state_preimage_sha256);
writer.write_fixed(&self.authority_state_postimage_sha256);
writer.write_fixed(&self.old_trust_policy_sha256);
writer.write_fixed(&self.new_trust_policy_sha256);
writer.write_fixed(&self.subject_key_id);
writer.write_fixed(&self.subject_public_key);
writer.write_enum(self.subject_role);
writer.write_enum(self.new_status);
writer.write_id_list(&self.provider_scopes)?;
writer.write_id_list(&self.phase_scopes)?;
writer.write_id_list(&self.account_authority_scopes)?;
writer.write_id_list(&self.configuration_authority_scopes)?;
writer.write_id_list(&self.policy_scopes)?;
writer.write_unix_time(self.activation_at)?;
writer.write_unix_time(self.retirement_at)?;
writer.write_fixed(&self.authorization_nonce);
writer.write_unix_time(self.issued_at)?;
writer.write_unix_time(self.not_before)?;
writer.write_unix_time(self.expires_at)?;
writer.finish(PayloadKind::TrustPolicyUpdate)
}
}
impl TrustPolicyBootstrapPayload {
pub fn decode(bytes: &[u8]) -> Result<Self, CatalogAuthorityPayloadError> {
let mut reader =
Reader::for_kind(bytes, CatalogAuthorityPayloadKind::TrustPolicyBootstrap)?;
let payload = Self {
bootstrap_key_id: reader.read_fixed()?,
bootstrap_public_key: reader.read_fixed()?,
trust_policy_id: reader.read_id128()?,
authority_store_id: reader.read_id128()?,
environment: reader.read_enum()?,
expected_authority_generation: reader.read_u64()?,
expected_trust_policy_generation: reader.read_u64()?,
uninitialized_state_sha256: reader.read_fixed()?,
initial_trust_policy_sha256: reader.read_fixed()?,
provider_scopes: reader.read_id_list()?,
phase_scopes: reader.read_id_list()?,
account_authority_scopes: reader.read_id_list()?,
configuration_authority_scopes: reader.read_id_list()?,
policy_scopes: reader.read_id_list()?,
authorization_nonce: reader.read_fixed()?,
issued_at: reader.read_unix_time()?,
not_before: reader.read_unix_time()?,
expires_at: reader.read_unix_time()?,
};
reader.finish()?;
require_canonical(bytes, &payload.encode()?)?;
Ok(payload)
}
pub fn encode(&self) -> Result<Vec<u8>, CatalogAuthorityPayloadError> {
if self.expected_authority_generation != 0 || self.expected_trust_policy_generation != 0 {
return Err(CatalogAuthorityPayloadError::InvalidBootstrapGeneration);
}
validate_key_id(
"bootstrap_key_id",
&self.bootstrap_key_id,
&self.bootstrap_public_key,
)?;
if !self.provider_scopes.is_empty()
|| !self.phase_scopes.is_empty()
|| !self.account_authority_scopes.is_empty()
|| !self.configuration_authority_scopes.is_empty()
|| !self.policy_scopes.is_empty()
{
return Err(CatalogAuthorityPayloadError::BootstrapScopesNotEmpty);
}
validate_authorization_window(
PayloadKind::TrustPolicyBootstrap,
self.issued_at,
self.not_before,
self.expires_at,
)?;
let mut writer = Writer::new(PayloadKind::TrustPolicyBootstrap);
writer.write_fixed(&self.bootstrap_key_id);
writer.write_fixed(&self.bootstrap_public_key);
writer.write_id128(&self.trust_policy_id)?;
writer.write_id128(&self.authority_store_id)?;
writer.write_enum(self.environment);
writer.write_u64(self.expected_authority_generation);
writer.write_u64(self.expected_trust_policy_generation);
writer.write_fixed(&self.uninitialized_state_sha256);
writer.write_fixed(&self.initial_trust_policy_sha256);
writer.write_id_list(&self.provider_scopes)?;
writer.write_id_list(&self.phase_scopes)?;
writer.write_id_list(&self.account_authority_scopes)?;
writer.write_id_list(&self.configuration_authority_scopes)?;
writer.write_id_list(&self.policy_scopes)?;
writer.write_fixed(&self.authorization_nonce);
writer.write_unix_time(self.issued_at)?;
writer.write_unix_time(self.not_before)?;
writer.write_unix_time(self.expires_at)?;
writer.finish(PayloadKind::TrustPolicyBootstrap)
}
}
impl CatalogAuthorityPayload {
pub fn decode(
kind: CatalogAuthorityPayloadKind,
bytes: &[u8],
) -> Result<Self, CatalogAuthorityPayloadError> {
match kind {
CatalogAuthorityPayloadKind::ConformanceEvidence => {
ConformanceEvidencePayload::decode(bytes).map(Self::ConformanceEvidence)
}
CatalogAuthorityPayloadKind::PromotionApply => {
PromotionApplyPayload::decode(bytes).map(Self::PromotionApply)
}
CatalogAuthorityPayloadKind::PromotionRollback => {
PromotionRollbackPayload::decode(bytes).map(Self::PromotionRollback)
}
CatalogAuthorityPayloadKind::TrustPolicyUpdate => {
TrustPolicyUpdatePayload::decode(bytes).map(Self::TrustPolicyUpdate)
}
CatalogAuthorityPayloadKind::TrustPolicyBootstrap => {
TrustPolicyBootstrapPayload::decode(bytes).map(Self::TrustPolicyBootstrap)
}
}
}
pub fn encode(&self) -> Result<Vec<u8>, CatalogAuthorityPayloadError> {
match self {
Self::ConformanceEvidence(payload) => payload.encode(),
Self::PromotionApply(payload) => payload.encode(),
Self::PromotionRollback(payload) => payload.encode(),
Self::TrustPolicyUpdate(payload) => payload.encode(),
Self::TrustPolicyBootstrap(payload) => payload.encode(),
}
}
pub fn signing_key_id(&self) -> &KeyId {
match self {
Self::ConformanceEvidence(payload) => &payload.signing_key_id,
Self::PromotionApply(payload) => &payload.signing_key_id,
Self::PromotionRollback(payload) => &payload.signing_key_id,
Self::TrustPolicyUpdate(payload) => &payload.signing_admin_key_id,
Self::TrustPolicyBootstrap(payload) => &payload.bootstrap_key_id,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Error)]
pub enum CatalogAuthorityPayloadError {
#[error("catalog authority payload is truncated")]
Truncated,
#[error("catalog authority payload has trailing bytes")]
TrailingBytes,
#[error("catalog authority payload is not the canonical encoding of its decoded value")]
NonCanonicalEncoding,
#[error("{kind} length is outside the canonical bound")]
InvalidLength { kind: &'static str },
#[error("{kind} length cannot be represented canonically")]
LengthOverflow { kind: &'static str },
#[error("{kind} does not match the canonical ASCII grammar")]
InvalidGrammar { kind: &'static str },
#[error("ID_LIST contains more than 64 entries")]
TooManyIds,
#[error("ID_LIST entries are not in strictly increasing encoded-byte order")]
IdListNotStrictlySorted,
#[error("{kind} contains unknown enum value {value:#04x}")]
UnknownEnumValue { kind: &'static str, value: u8 },
#[error("BOOL8 contains invalid value {0:#04x}")]
InvalidBool(u8),
#[error("Unix time exceeds the R1 maximum")]
UnixTimeOutOfRange,
#[error("{kind} payload has an invalid domain separator")]
InvalidDomain { kind: &'static str },
#[error("{kind} payload is {actual} bytes; maximum is {maximum} bytes")]
PayloadTooLarge {
kind: &'static str,
maximum: usize,
actual: usize,
},
#[error("{field} must not equal u64::MAX")]
InvalidGeneration { field: &'static str },
#[error("bootstrap authority and trust-policy generations must both be zero")]
InvalidBootstrapGeneration,
#[error("{kind} digest relationship is invalid: {relationship}")]
InvalidDigestRelationship {
kind: &'static str,
relationship: &'static str,
},
#[error("{field} does not equal SHA-256 of its public key")]
KeyIdMismatch { field: &'static str },
#[error("{kind} time relationship is invalid: {relationship}")]
InvalidTimeRelationship {
kind: &'static str,
relationship: &'static str,
},
#[error("arithmetic overflow while validating {relationship}")]
ArithmeticOverflow { relationship: &'static str },
#[error("trust update action {action:?} is incompatible with status {status:?}")]
InvalidTrustUpdateTransition {
action: TrustUpdateAction,
status: AnchorStatus,
},
#[error("retire trust update requires a nonzero retirement_at")]
RetireWithoutRetirementTime,
#[error("bootstrap scope lists must all be empty")]
BootstrapScopesNotEmpty,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum CatalogAuthorityPayloadKind {
ConformanceEvidence,
PromotionApply,
PromotionRollback,
TrustPolicyUpdate,
TrustPolicyBootstrap,
}
type PayloadKind = CatalogAuthorityPayloadKind;
impl CatalogAuthorityPayloadKind {
pub const fn name(self) -> &'static str {
match self {
Self::ConformanceEvidence => "conformance_evidence_v1",
Self::PromotionApply => "promotion_apply_v1",
Self::PromotionRollback => "promotion_rollback_v1",
Self::TrustPolicyUpdate => "trust_policy_update_v1",
Self::TrustPolicyBootstrap => "trust_policy_bootstrap_v1",
}
}
pub const fn domain(self) -> &'static [u8] {
match self {
Self::ConformanceEvidence => CONFORMANCE_EVIDENCE_DOMAIN,
Self::PromotionApply => PROMOTION_APPLY_DOMAIN,
Self::PromotionRollback => PROMOTION_ROLLBACK_DOMAIN,
Self::TrustPolicyUpdate => TRUST_POLICY_UPDATE_DOMAIN,
Self::TrustPolicyBootstrap => TRUST_POLICY_BOOTSTRAP_DOMAIN,
}
}
pub const fn maximum_payload_bytes(self) -> usize {
match self {
Self::ConformanceEvidence => CONFORMANCE_EVIDENCE_MAX_PAYLOAD_BYTES,
Self::PromotionApply => PROMOTION_APPLY_MAX_PAYLOAD_BYTES,
Self::PromotionRollback => PROMOTION_ROLLBACK_MAX_PAYLOAD_BYTES,
Self::TrustPolicyUpdate => TRUST_POLICY_UPDATE_MAX_PAYLOAD_BYTES,
Self::TrustPolicyBootstrap => TRUST_POLICY_BOOTSTRAP_MAX_PAYLOAD_BYTES,
}
}
const fn maximum_size(self) -> usize {
self.maximum_payload_bytes()
}
}
fn require_canonical(input: &[u8], reencoded: &[u8]) -> Result<(), CatalogAuthorityPayloadError> {
if input == reencoded {
Ok(())
} else {
Err(CatalogAuthorityPayloadError::NonCanonicalEncoding)
}
}
fn validate_unix_time(value: u64) -> Result<(), CatalogAuthorityPayloadError> {
if value > MAX_UNIX_TIME {
return Err(CatalogAuthorityPayloadError::UnixTimeOutOfRange);
}
Ok(())
}
fn validate_conformance_times(
execution_started_at: u64,
execution_ended_at: u64,
issued_at: u64,
not_before: u64,
expires_at: u64,
) -> Result<(), CatalogAuthorityPayloadError> {
for value in [
execution_started_at,
execution_ended_at,
issued_at,
not_before,
expires_at,
] {
validate_unix_time(value)?;
}
if execution_started_at > execution_ended_at || execution_ended_at > issued_at {
return Err(CatalogAuthorityPayloadError::InvalidTimeRelationship {
kind: PayloadKind::ConformanceEvidence.name(),
relationship: "execution_started_at <= execution_ended_at <= issued_at is required",
});
}
validate_time_window(
PayloadKind::ConformanceEvidence,
issued_at,
not_before,
expires_at,
CONFORMANCE_MAX_LIFETIME_SECONDS,
)
}
fn validate_authorization_window(
kind: PayloadKind,
issued_at: u64,
not_before: u64,
expires_at: u64,
) -> Result<(), CatalogAuthorityPayloadError> {
for value in [issued_at, not_before, expires_at] {
validate_unix_time(value)?;
}
validate_time_window(
kind,
issued_at,
not_before,
expires_at,
AUTHORIZATION_MAX_LIFETIME_SECONDS,
)
}
fn validate_time_window(
kind: PayloadKind,
issued_at: u64,
not_before: u64,
expires_at: u64,
maximum_lifetime: u64,
) -> Result<(), CatalogAuthorityPayloadError> {
if issued_at > not_before || not_before >= expires_at {
return Err(CatalogAuthorityPayloadError::InvalidTimeRelationship {
kind: kind.name(),
relationship: "issued_at <= not_before < expires_at is required",
});
}
let lifetime = expires_at.checked_sub(issued_at).ok_or(
CatalogAuthorityPayloadError::ArithmeticOverflow {
relationship: "expires_at - issued_at",
},
)?;
if lifetime > maximum_lifetime {
return Err(CatalogAuthorityPayloadError::InvalidTimeRelationship {
kind: kind.name(),
relationship: "authorization lifetime exceeds the payload maximum",
});
}
Ok(())
}
fn validate_generation(
field: &'static str,
value: u64,
) -> Result<(), CatalogAuthorityPayloadError> {
if value == u64::MAX {
return Err(CatalogAuthorityPayloadError::InvalidGeneration { field });
}
Ok(())
}
fn validate_key_id(
field: &'static str,
key_id: &KeyId,
public_key: &PublicKey,
) -> Result<(), CatalogAuthorityPayloadError> {
let computed: KeyId = Sha256::digest(public_key).into();
if key_id != &computed {
return Err(CatalogAuthorityPayloadError::KeyIdMismatch { field });
}
Ok(())
}
fn validate_update_transition(
action: TrustUpdateAction,
status: AnchorStatus,
retirement_at: u64,
) -> Result<(), CatalogAuthorityPayloadError> {
let valid = match action {
TrustUpdateAction::Add => status == AnchorStatus::Pending,
TrustUpdateAction::Activate => status == AnchorStatus::Active,
TrustUpdateAction::Retire => status == AnchorStatus::Retired,
TrustUpdateAction::Revoke => status == AnchorStatus::Revoked,
TrustUpdateAction::ReplaceScopes => status != AnchorStatus::Revoked,
};
if !valid {
return Err(CatalogAuthorityPayloadError::InvalidTrustUpdateTransition { action, status });
}
if action == TrustUpdateAction::Retire && retirement_at == 0 {
return Err(CatalogAuthorityPayloadError::RetireWithoutRetirementTime);
}
Ok(())
}
fn validate_ascii_value(
kind: &'static str,
bytes: &[u8],
maximum: usize,
validator: fn(&[u8]) -> bool,
) -> Result<(), CatalogAuthorityPayloadError> {
if bytes.is_empty() || bytes.len() > maximum {
return Err(CatalogAuthorityPayloadError::InvalidLength { kind });
}
if !validator(bytes) {
return Err(CatalogAuthorityPayloadError::InvalidGrammar { kind });
}
Ok(())
}
fn valid_first_byte(byte: u8) -> bool {
byte.is_ascii_alphanumeric()
}
fn valid_id128(bytes: &[u8]) -> bool {
bytes.first().copied().is_some_and(valid_first_byte)
&& bytes
.iter()
.skip(1)
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b':' | b'-'))
}
fn valid_model256(bytes: &[u8]) -> bool {
valid_path_like(bytes, false)
}
fn valid_ref256(bytes: &[u8]) -> bool {
valid_path_like(bytes, true)
}
fn valid_path_like(bytes: &[u8], reference: bool) -> bool {
bytes.first().copied().is_some_and(valid_first_byte)
&& bytes.last() != Some(&b'/')
&& !bytes
.windows(2)
.any(|window| matches!(window, b"//" | b".."))
&& bytes.iter().skip(1).all(|byte| {
byte.is_ascii_alphanumeric()
|| matches!(byte, b'.' | b'_' | b':' | b'/' | b'-')
|| (reference && matches!(byte, b'@' | b'+'))
})
}
fn validate_id_list(values: &[Id128]) -> Result<(), CatalogAuthorityPayloadError> {
if values.len() > MAX_ID_LIST_ITEMS {
return Err(CatalogAuthorityPayloadError::TooManyIds);
}
if values
.windows(2)
.any(|pair| pair[0].encoded_cmp(&pair[1]) != Ordering::Less)
{
return Err(CatalogAuthorityPayloadError::IdListNotStrictlySorted);
}
Ok(())
}
trait WireEnum: Copy {
const KIND: &'static str;
fn from_wire(value: u8) -> Result<Self, CatalogAuthorityPayloadError>;
fn to_wire(self) -> u8;
}
struct Writer {
bytes: Vec<u8>,
}
impl Writer {
fn new(kind: PayloadKind) -> Self {
Self {
bytes: kind.domain().to_vec(),
}
}
fn finish(self, kind: PayloadKind) -> Result<Vec<u8>, CatalogAuthorityPayloadError> {
if !self.bytes.starts_with(kind.domain()) {
return Err(CatalogAuthorityPayloadError::InvalidDomain { kind: kind.name() });
}
let actual = self.bytes.len();
let maximum = kind.maximum_size();
if actual > maximum {
return Err(CatalogAuthorityPayloadError::PayloadTooLarge {
kind: kind.name(),
maximum,
actual,
});
}
Ok(self.bytes)
}
fn write_u16(&mut self, value: u16) {
self.bytes.extend_from_slice(&value.to_be_bytes());
}
fn write_u64(&mut self, value: u64) {
self.bytes.extend_from_slice(&value.to_be_bytes());
}
fn write_unix_time(&mut self, value: u64) -> Result<(), CatalogAuthorityPayloadError> {
if value > MAX_UNIX_TIME {
return Err(CatalogAuthorityPayloadError::UnixTimeOutOfRange);
}
self.write_u64(value);
Ok(())
}
#[allow(dead_code)]
fn write_bool(&mut self, value: bool) {
self.bytes.push(u8::from(value));
}
fn write_enum<E: WireEnum>(&mut self, value: E) {
self.bytes.push(value.to_wire());
}
fn write_fixed<const N: usize>(&mut self, value: &[u8; N]) {
self.bytes.extend_from_slice(value);
}
fn write_id128(&mut self, value: &Id128) -> Result<(), CatalogAuthorityPayloadError> {
validate_ascii_value("ID128", value.as_str().as_bytes(), 128, valid_id128)?;
self.write_ascii("ID128", value.as_str())
}
fn write_model256(&mut self, value: &Model256) -> Result<(), CatalogAuthorityPayloadError> {
validate_ascii_value("MODEL256", value.as_str().as_bytes(), 256, valid_model256)?;
self.write_ascii("MODEL256", value.as_str())
}
fn write_ref256(&mut self, value: &Ref256) -> Result<(), CatalogAuthorityPayloadError> {
validate_ascii_value("REF256", value.as_str().as_bytes(), 256, valid_ref256)?;
self.write_ascii("REF256", value.as_str())
}
fn write_id_list(&mut self, values: &IdList) -> Result<(), CatalogAuthorityPayloadError> {
validate_id_list(values.as_slice())?;
let count = u16::try_from(values.len())
.map_err(|_| CatalogAuthorityPayloadError::LengthOverflow { kind: "ID_LIST" })?;
self.write_u16(count);
for value in values.as_slice() {
self.write_id128(value)?;
}
Ok(())
}
fn write_ascii(
&mut self,
kind: &'static str,
value: &str,
) -> Result<(), CatalogAuthorityPayloadError> {
let length = u16::try_from(value.len())
.map_err(|_| CatalogAuthorityPayloadError::LengthOverflow { kind })?;
self.write_u16(length);
self.bytes.extend_from_slice(value.as_bytes());
Ok(())
}
}
#[allow(dead_code)]
struct Reader<'a> {
bytes: &'a [u8],
offset: usize,
}
#[allow(dead_code)]
impl<'a> Reader<'a> {
fn for_kind(
bytes: &'a [u8],
kind: CatalogAuthorityPayloadKind,
) -> Result<Self, CatalogAuthorityPayloadError> {
let actual = bytes.len();
let maximum = kind.maximum_payload_bytes();
if actual > maximum {
return Err(CatalogAuthorityPayloadError::PayloadTooLarge {
kind: kind.name(),
maximum,
actual,
});
}
let mut reader = Self { bytes, offset: 0 };
let domain = reader.read_exact(kind.domain().len())?;
if domain != kind.domain() {
return Err(CatalogAuthorityPayloadError::InvalidDomain { kind: kind.name() });
}
Ok(reader)
}
fn finish(self) -> Result<(), CatalogAuthorityPayloadError> {
if self.offset == self.bytes.len() {
Ok(())
} else {
Err(CatalogAuthorityPayloadError::TrailingBytes)
}
}
fn read_u16(&mut self) -> Result<u16, CatalogAuthorityPayloadError> {
Ok(u16::from_be_bytes(self.read_fixed()?))
}
fn read_u64(&mut self) -> Result<u64, CatalogAuthorityPayloadError> {
Ok(u64::from_be_bytes(self.read_fixed()?))
}
fn read_unix_time(&mut self) -> Result<u64, CatalogAuthorityPayloadError> {
let value = self.read_u64()?;
if value > MAX_UNIX_TIME {
return Err(CatalogAuthorityPayloadError::UnixTimeOutOfRange);
}
Ok(value)
}
fn read_bool(&mut self) -> Result<bool, CatalogAuthorityPayloadError> {
match self.read_byte()? {
0x00 => Ok(false),
0x01 => Ok(true),
value => Err(CatalogAuthorityPayloadError::InvalidBool(value)),
}
}
fn read_enum<E: WireEnum>(&mut self) -> Result<E, CatalogAuthorityPayloadError> {
E::from_wire(self.read_byte()?)
}
fn read_fixed<const N: usize>(&mut self) -> Result<[u8; N], CatalogAuthorityPayloadError> {
let bytes = self.read_exact(N)?;
bytes
.try_into()
.map_err(|_| CatalogAuthorityPayloadError::Truncated)
}
fn read_id128(&mut self) -> Result<Id128, CatalogAuthorityPayloadError> {
self.read_ascii("ID128", 128, valid_id128)
.and_then(Id128::parse)
}
fn read_model256(&mut self) -> Result<Model256, CatalogAuthorityPayloadError> {
self.read_ascii("MODEL256", 256, valid_model256)
.and_then(Model256::parse)
}
fn read_ref256(&mut self) -> Result<Ref256, CatalogAuthorityPayloadError> {
self.read_ascii("REF256", 256, valid_ref256)
.and_then(Ref256::parse)
}
fn read_id_list(&mut self) -> Result<IdList, CatalogAuthorityPayloadError> {
let count = usize::from(self.read_u16()?);
if count > MAX_ID_LIST_ITEMS {
return Err(CatalogAuthorityPayloadError::TooManyIds);
}
let mut values = Vec::with_capacity(count);
for _ in 0..count {
values.push(self.read_id128()?);
}
IdList::new(values)
}
fn read_ascii(
&mut self,
kind: &'static str,
maximum: usize,
validator: fn(&[u8]) -> bool,
) -> Result<String, CatalogAuthorityPayloadError> {
let length = usize::from(self.read_u16()?);
if length == 0 || length > maximum {
return Err(CatalogAuthorityPayloadError::InvalidLength { kind });
}
let bytes = self.read_exact(length)?;
if !validator(bytes) {
return Err(CatalogAuthorityPayloadError::InvalidGrammar { kind });
}
let value = std::str::from_utf8(bytes)
.map_err(|_| CatalogAuthorityPayloadError::InvalidGrammar { kind })?;
Ok(value.to_owned())
}
fn read_byte(&mut self) -> Result<u8, CatalogAuthorityPayloadError> {
Ok(self.read_exact(1)?[0])
}
fn read_exact(&mut self, length: usize) -> Result<&'a [u8], CatalogAuthorityPayloadError> {
let end = self
.offset
.checked_add(length)
.ok_or(CatalogAuthorityPayloadError::LengthOverflow { kind: "payload" })?;
let bytes = self
.bytes
.get(self.offset..end)
.ok_or(CatalogAuthorityPayloadError::Truncated)?;
self.offset = end;
Ok(bytes)
}
}