mod admission;
mod encoding;
mod evidence;
mod pairing;
pub use evidence::{Evidence, EvidenceRecord, MAX_EVIDENCE_JSON_BYTES, Mailbox};
mod keys;
mod rotation;
pub use keys::{MAX_COVERAGE_BYTES, VerifiedKeys};
pub use rotation::{Generation, RotationMaterial};
#[cfg(any(test, feature = "test-support"))]
pub mod test_support;
#[cfg(test)]
pub(crate) mod tests;
pub use super::peer::Invitation;
use super::peer::{Recipient, verify};
use super::*;
use anyhow::Result;
pub use pairing::{Declaration, Device, Joiner, ProvisionalGrant, VerifiedEnrollment};
pub const MAX_DEVICES: usize = 32;
pub use crate::wire::enrollment::MAX_RECORD_BYTES;
pub const MAX_TRANSITIONS: usize = 256;
pub const MAX_GENERATIONS: usize = 64;
pub const MAX_CUTOFF: u64 = (i64::MAX - 1) as u64;
pub const MAX_KEY_PLAINTEXT_BYTES: usize =
GRANT_PREFIX_BYTES + 2 + GENERATION_BYTES * MAX_GENERATIONS;
pub const DECLARATION_BYTES: usize = 280;
pub const REQUEST_BYTES: usize = 314;
const CORE_BYTES: usize = 461;
const GRANT_PREFIX_BYTES: usize = 1 + 12 * 32 + 8;
const MEMBER_BYTES: usize = 164;
const GENERATION_BYTES: usize = 72;
const MAX_ADMISSION_BYTES: usize =
1257 + MEMBER_BYTES * MAX_DEVICES + 2 * GENERATION_BYTES * MAX_GENERATIONS;
const MAX_ROTATION_BYTES: usize =
540 + (MEMBER_BYTES + 306) * MAX_DEVICES + GENERATION_BYTES * MAX_GENERATIONS;
const MAX_REVOKE_BYTES: usize =
438 + MEMBER_BYTES * MAX_DEVICES + GENERATION_BYTES * MAX_GENERATIONS;
pub const MAX_CHAIN_BYTES: usize = GENESIS_BYTES
+ PUBLICATION_BYTES
+ crate::artifact::codec::MAX_DESCRIPTOR_BYTES
+ (MAX_GENERATIONS - 1) * MAX_ROTATION_BYTES
+ (MAX_TRANSITIONS + 1 - MAX_GENERATIONS)
* (DECLARATION_BYTES + REQUEST_BYTES + MAX_ADMISSION_BYTES);
const _: () = assert!(
MAX_ADMISSION_BYTES <= MAX_RECORD_BYTES
&& MAX_ROTATION_BYTES <= MAX_RECORD_BYTES
&& MAX_REVOKE_BYTES <= DECLARATION_BYTES + REQUEST_BYTES + MAX_ADMISSION_BYTES
);
const DOMAIN_VERSION: u32 = 1;
type Hash = [u8; 32];
fn check(condition: bool) -> Result<()> {
ensure!(condition, "error membership-invalid");
Ok(())
}
#[derive(Clone, PartialEq, Eq)]
struct Member {
device: Hash,
sign: Hash,
hpke: Hash,
verifier: Hash,
admission: Hash,
admitted_at: u64,
}
impl Member {
fn write(&self, out: &mut Vec<u8>) {
for field in [self.device, self.sign, self.hpke, self.verifier] {
out.extend(field);
}
out.extend(1_u32.to_be_bytes());
out.extend(self.admission);
}
}
#[derive(Clone)]
pub struct Membership {
genesis: Genesis,
publication: Publication,
members: Vec<Member>,
heads: Vec<Hash>,
handles: Vec<Hash>,
retired: Vec<Member>,
generations: Vec<Generation>,
pending: bool,
}
impl Membership {
pub fn from_publication(
genesis: &Genesis,
descriptor: &[u8],
publication: &[u8],
) -> Result<Self> {
let publication = Publication::from_record(genesis, descriptor, publication)?;
Ok(Self {
genesis: genesis.clone(),
heads: vec![publication.commitment()],
publication,
members: vec![Member {
device: genesis.device_id(),
sign: genesis.signing_public(),
hpke: genesis.hpke_public(),
verifier: genesis.verifier(),
admission: genesis.claim_id(),
admitted_at: 0,
}],
handles: vec![],
retired: vec![],
generations: vec![Generation {
id: genesis.context().generation_id,
commitment: genesis.generation_commitment(),
starts_after: 0,
}],
pending: false,
})
}
pub fn head(&self) -> Hash {
self.heads[self.heads.len() - 1]
}
pub fn sequence(&self) -> u64 {
self.heads.len() as u64
}
pub fn has_device(&self, device: Hash) -> bool {
self.members.iter().any(|member| member.device == device)
}
pub fn devices(&self) -> impl Iterator<Item = Hash> + '_ {
self.members.iter().map(|member| member.device)
}
pub fn admissions(&self) -> impl Iterator<Item = (Hash, u64)> + '_ {
self.members
.iter()
.map(|member| (member.device, member.admitted_at))
}
pub fn has_admission(&self, handle: Hash) -> bool {
self.members.iter().any(|member| member.admission == handle)
}
pub fn record_action(record: &[u8]) -> Result<(Hash, u8)> {
let (core, _, _, _) = encoding::components(record)?;
encoding::signer_action(core)
}
pub fn device_count(&self) -> usize {
self.members.len()
}
pub fn genesis(&self) -> &Genesis {
&self.genesis
}
pub fn publication(&self) -> &Publication {
&self.publication
}
pub fn extends(&self, previous: &Self) -> bool {
self.genesis == previous.genesis && self.heads.starts_with(&previous.heads)
}
pub fn authenticate(
&self,
auth: &super::peer::Authentication<'_>,
ancestor: bool,
) -> Result<()> {
ensure!(
auth.vault == self.genesis.context().vault_id
&& auth.genesis == self.genesis.commitment(),
Unauthorized
);
let member = self.member(&auth.device).map_err(|_| Unauthorized)?;
ensure!(
bool::from(member.verifier.ct_eq(&credential_verifier(
auth.vault,
auth.device,
auth.bearer
))),
Unauthorized
);
ensure!(
self.heads.contains(&auth.head),
"error membership-context-unknown"
);
if !ancestor && auth.head != self.head() {
anyhow::bail!(StaleContext);
}
Ok(())
}
pub fn head_at(&self, sequence: u64) -> Option<Hash> {
sequence
.checked_sub(1)
.and_then(|n| self.heads.get(n as usize))
.copied()
}
pub fn validate_key(&self, key: &LocalSharedStatePackageKey) -> Result<()> {
check(self.generations.len() == 1)?;
check(
generation_commitment(self.genesis.context(), key.protected_storage_bytes())
== self.genesis.generation_commitment(),
)
}
pub fn contains_head(&self, head: &Hash) -> bool {
self.heads.contains(head)
}
fn member(&self, device: &Hash) -> Result<&Member> {
self.members
.iter()
.find(|m| &m.device == device)
.context("error membership-signer")
}
fn unique(&self, recipient: &Recipient, handle: &Hash) -> Result<()> {
check(self.members.len() < MAX_DEVICES && !self.handles.contains(handle))?;
check(self.members.iter().chain(&self.retired).all(|m| {
m.device != recipient.device && m.sign != recipient.sign && m.hpke != recipient.hpke
}))
}
pub fn append(&self, declaration: &[u8], request: &[u8], record: &[u8]) -> Result<Self> {
let (core, _, _, _) = encoding::components(record)?;
let (_, action) = encoding::signer_action(core)?;
match action {
3 => self.admit(
&Declaration::from_record(self, declaration)?,
request,
record,
),
4 | 5 => {
check(declaration.is_empty() && request.is_empty())?;
self.finish(rotation::validate(self, record)?, record)
}
_ => anyhow::bail!("error membership-action"),
}
}
pub(super) fn admit(
&self,
declaration: &Declaration,
request: &[u8],
record: &[u8],
) -> Result<Self> {
let recipient = admission::validate(self, declaration, request, record)?;
let mut next = self.clone();
next.members.push(Member {
device: recipient.device,
sign: recipient.sign,
hpke: recipient.hpke,
verifier: recipient.verifier,
admission: declaration.handle,
admitted_at: self.sequence() + 1,
});
next.members.sort_by_key(|m| m.device);
next.handles.push(declaration.handle);
self.finish(next, record)
}
fn finish(&self, mut next: Self, record: &[u8]) -> Result<Self> {
next.heads.push(hash(record));
next.capacity()?;
Ok(next)
}
fn capacity(&self) -> Result<()> {
let reserve = usize::from(self.pending);
ensure!(
self.heads.len() - 1 + reserve <= MAX_TRANSITIONS
&& self.generations.len() + reserve <= MAX_GENERATIONS,
"error membership-change-limit"
);
Ok(())
}
pub fn ensure_change_capacity(&self) -> Result<()> {
ensure!(
self.heads.len() - 1 + 2 <= MAX_TRANSITIONS && self.generations.len() < MAX_GENERATIONS,
"error membership-change-limit"
);
Ok(())
}
pub fn rotation_pending(&self) -> bool {
self.pending
}
pub fn generations(&self) -> &[Generation] {
&self.generations
}
pub fn current_generation(&self) -> &Generation {
self.generations.last().expect("genesis generation")
}
pub fn generation_allows(&self, generation: Hash, sequence: u64) -> bool {
self.generations
.iter()
.position(|g| g.id == generation)
.is_some_and(|i| {
sequence > self.generations[i].starts_after
&& self
.generations
.get(i + 1)
.is_none_or(|g| sequence <= g.starts_after)
})
}
}
#[derive(Debug)]
pub struct StaleContext;
impl fmt::Display for StaleContext {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("error enrollment-context-stale")
}
}
impl std::error::Error for StaleContext {}
#[derive(Debug)]
pub struct Unauthorized;
impl fmt::Display for Unauthorized {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("error enrollment-unauthorized")
}
}
impl std::error::Error for Unauthorized {}
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ManagementPreparation {
pub evidence: Evidence,
pub high_water: u64,
}