use std::collections::HashSet;
use crate::error::CellosError;
use crate::ExecutionCellDocument;
use url::Url;
fn is_sha256_digest(value: &str) -> bool {
let Some(hex) = value.strip_prefix("sha256:") else {
return false;
};
hex.len() == 64
&& hex
.chars()
.all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase())
}
pub(crate) fn is_portable_identifier(value: &str) -> bool {
let mut chars = value.chars();
let Some(first) = chars.next() else {
return false;
};
if !first.is_ascii_alphanumeric() {
return false;
}
if value.len() > 128 || value.contains("..") {
return false;
}
chars.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'))
}
fn ensure_portable_identifier(value: &str, field: &str) -> Result<(), CellosError> {
if is_portable_identifier(value) {
Ok(())
} else {
Err(CellosError::InvalidSpec(format!(
"{field} must match [A-Za-z0-9][A-Za-z0-9._-]{{0,127}} and must not contain '..'"
)))
}
}
fn is_kubernetes_namespace(value: &str) -> bool {
if value.is_empty() || value.len() > 63 || value.contains("..") {
return false;
}
let mut chars = value.chars();
let Some(first) = chars.next() else {
return false;
};
if !first.is_ascii_lowercase() && !first.is_ascii_digit() {
return false;
}
let Some(last) = value.chars().last() else {
return false;
};
if !last.is_ascii_lowercase() && !last.is_ascii_digit() {
return false;
}
value
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
}
fn ensure_kubernetes_namespace(value: &str, field: &str) -> Result<(), CellosError> {
if is_kubernetes_namespace(value) {
Ok(())
} else {
Err(CellosError::InvalidSpec(format!(
"{field} must match Kubernetes DNS label rules: lowercase alphanumeric plus '-', <=63 chars"
)))
}
}
fn is_dns_label(value: &str) -> bool {
if value.is_empty() || value.len() > 63 {
return false;
}
let bytes = value.as_bytes();
let first_ok = bytes
.first()
.copied()
.is_some_and(|b| b.is_ascii_alphanumeric());
let last_ok = bytes
.last()
.copied()
.is_some_and(|b| b.is_ascii_alphanumeric());
if !first_ok || !last_ok {
return false;
}
bytes
.iter()
.all(|b| b.is_ascii_alphanumeric() || *b == b'-')
}
pub(crate) fn is_fqdn_or_wildcard(value: &str) -> bool {
if value.is_empty() || value.len() > 253 {
return false;
}
if value
.split('.')
.all(|segment| !segment.is_empty() && segment.chars().all(|c| c.is_ascii_digit()))
&& value.contains('.')
{
return false;
}
let labels: Vec<&str> = value.split('.').collect();
if labels.len() < 2 {
return false;
}
let (first, rest) = labels.split_first().expect("non-empty above");
if *first == "*" {
if rest.is_empty() {
return false;
}
return rest.iter().all(|label| is_dns_label(label));
}
labels.iter().all(|label| is_dns_label(label))
}
fn parse_http_base_url(value: &str, field: &str) -> Result<Url, CellosError> {
let parsed = Url::parse(value).map_err(|_| {
CellosError::InvalidSpec(format!(
"{field} must be an absolute http(s) base URL without query or fragment"
))
})?;
let scheme = parsed.scheme();
if scheme != "http" && scheme != "https" {
return Err(CellosError::InvalidSpec(format!(
"{field} must use http or https"
)));
}
if parsed.host_str().is_none() || parsed.query().is_some() || parsed.fragment().is_some() {
return Err(CellosError::InvalidSpec(format!(
"{field} must be an absolute http(s) base URL without query or fragment"
)));
}
Ok(parsed)
}
pub fn check_policy_pack_version(
declared: Option<&str>,
allow_downgrade: bool,
) -> Result<(), CellosError> {
crate::policy::check_policy_pack_version_compatibility(declared, allow_downgrade)
}
pub fn validate_tenant_id_for_subject_token(tenant_id: &str) -> Result<(), CellosError> {
if tenant_id.is_empty() {
return Err(CellosError::InvalidSpec(
"spec.correlation.tenantId must be non-empty when present".into(),
));
}
for ch in tenant_id.chars() {
let bad = ch == '.' || ch == '*' || ch == '>' || ch.is_whitespace();
if bad {
return Err(CellosError::InvalidSpec(format!(
"spec.correlation.tenantId contains NATS-reserved char: {ch:?} (in {tenant_id:?})"
)));
}
}
Ok(())
}
pub fn validate_execution_cell_document(doc: &ExecutionCellDocument) -> Result<(), CellosError> {
if doc.api_version != "cellos.io/v1" {
return Err(CellosError::InvalidSpec(format!(
"unsupported apiVersion: {}",
doc.api_version
)));
}
if doc.kind != "ExecutionCell" {
return Err(CellosError::InvalidSpec(format!(
"unsupported kind: {}",
doc.kind
)));
}
ensure_portable_identifier(&doc.spec.id, "spec.id")?;
if let Some(correlation) = &doc.spec.correlation {
if let Some(tenant_id) = correlation.tenant_id.as_deref() {
validate_tenant_id_for_subject_token(tenant_id)?;
}
}
let authority_secret_refs = doc
.spec
.authority
.secret_refs
.as_ref()
.map(|refs| refs.iter().map(String::as_str).collect::<HashSet<_>>())
.unwrap_or_default();
for secret_ref in &authority_secret_refs {
ensure_portable_identifier(secret_ref, "authority.secretRefs[]")?;
}
if let Some(identity) = &doc.spec.identity {
ensure_portable_identifier(&identity.secret_ref, "spec.identity.secretRef")?;
if let Some(ttl_seconds) = identity.ttl_seconds {
if ttl_seconds > doc.spec.lifetime.ttl_seconds {
return Err(CellosError::InvalidSpec(
"spec.identity.ttlSeconds must be <= spec.lifetime.ttlSeconds".into(),
));
}
}
if !authority_secret_refs.contains(identity.secret_ref.as_str()) {
return Err(CellosError::InvalidSpec(format!(
"spec.identity.secretRef {:?} must also appear in authority.secretRefs",
identity.secret_ref
)));
}
}
if let Some(env) = &doc.spec.environment {
if env.image_reference.is_empty() {
return Err(CellosError::InvalidSpec(
"spec.environment.imageReference must be non-empty".into(),
));
}
if let Some(digest) = &env.image_digest {
if !is_sha256_digest(digest) {
return Err(CellosError::InvalidSpec(
"spec.environment.imageDigest must be a sha256:<hex64> digest when present"
.into(),
));
}
}
if let Some(template_id) = &env.template_id {
ensure_portable_identifier(template_id, "spec.environment.templateId")?;
}
}
if let Some(placement) = &doc.spec.placement {
if placement.pool_id.is_none()
&& placement.kubernetes_namespace.is_none()
&& placement.queue_name.is_none()
{
return Err(CellosError::InvalidSpec(
"spec.placement must set at least one placement hint".into(),
));
}
if let Some(pool_id) = &placement.pool_id {
ensure_portable_identifier(pool_id, "spec.placement.poolId")?;
}
if let Some(namespace) = &placement.kubernetes_namespace {
ensure_kubernetes_namespace(namespace, "spec.placement.kubernetesNamespace")?;
}
if let Some(queue_name) = &placement.queue_name {
ensure_portable_identifier(queue_name, "spec.placement.queueName")?;
}
}
if let Some(ingress) = &doc.spec.ingress {
if let Some(git) = &ingress.git {
if let Some(secret_ref) = &git.secret_ref {
ensure_portable_identifier(secret_ref, "spec.ingress.git.secretRef")?;
}
}
if let Some(image) = &ingress.oci_image {
if let Some(secret_ref) = &image.secret_ref {
ensure_portable_identifier(secret_ref, "spec.ingress.ociImage.secretRef")?;
}
}
}
if let Some(run) = &doc.spec.run {
if run.argv.is_empty() || run.argv.iter().any(|s| s.is_empty()) {
return Err(CellosError::InvalidSpec(
"spec.run.argv must be non-empty with no empty strings".into(),
));
}
if let Some(idx) = run.argv.iter().position(|s| s.as_bytes().contains(&0)) {
return Err(CellosError::InvalidSpec(format!(
"spec.run.argv[{}] contains NUL byte (would be silently truncated by execve)",
idx
)));
}
check_argv_size_within_kernel_cmdline_limit(&run.argv)?;
if let Some(timeout_ms) = run.timeout_ms {
let ttl_ms = doc.spec.lifetime.ttl_seconds.saturating_mul(1000);
if timeout_ms > ttl_ms {
return Err(CellosError::InvalidSpec(
"spec.run.timeoutMs must be <= spec.lifetime.ttlSeconds * 1000".into(),
));
}
}
if let Some(limits) = &run.limits {
if limits.memory_max_bytes == Some(0) {
return Err(CellosError::InvalidSpec(
"spec.run.limits.memoryMaxBytes must be > 0".into(),
));
}
if let Some(cpu_max) = &limits.cpu_max {
if cpu_max.quota_micros == 0 {
return Err(CellosError::InvalidSpec(
"spec.run.limits.cpuMax.quotaMicros must be > 0".into(),
));
}
if cpu_max.period_micros == Some(0) {
return Err(CellosError::InvalidSpec(
"spec.run.limits.cpuMax.periodMicros must be > 0".into(),
));
}
}
}
}
if let Some(rules) = &doc.spec.authority.egress_rules {
for r in rules {
if r.host.is_empty() {
return Err(CellosError::InvalidSpec(
"authority.egressRules[].host must be non-empty".into(),
));
}
}
}
if let Some(dns_authority) = &doc.spec.authority.dns_authority {
validate_dns_authority(dns_authority)?;
}
if let Some(cdn_authority) = &doc.spec.authority.cdn_authority {
validate_cdn_authority(cdn_authority)?;
}
if let Some(ref token) = doc.spec.authority.authority_derivation {
verify_authority_derivation_structural(&doc.spec, token)?;
}
if let Some(export) = &doc.spec.export {
let targets = export.targets.as_deref().unwrap_or(&[]);
let mut target_names = HashSet::new();
for target in targets {
ensure_portable_identifier(target.name(), "spec.export.targets[].name")?;
if !target_names.insert(target.name().to_string()) {
return Err(CellosError::InvalidSpec(format!(
"duplicate export target name {:?}",
target.name()
)));
}
if let Some(secret_ref) = target.secret_ref() {
if !authority_secret_refs.contains(secret_ref) {
return Err(CellosError::InvalidSpec(format!(
"export target {:?} secretRef {:?} must appear in authority.secretRefs",
target.name(),
secret_ref
)));
}
}
if let crate::ExportTarget::Http(target) = target {
let parsed =
parse_http_base_url(&target.base_url, "spec.export.targets[].baseUrl")?;
if let Some(rules) = &doc.spec.authority.egress_rules {
if !rules.is_empty() {
let host = parsed.host_str().expect("checked above");
let port = parsed
.port_or_known_default()
.expect("http/https always has a known default port");
let allowed = rules
.iter()
.any(|rule| rule.port == port && rule.host.eq_ignore_ascii_case(host));
if !allowed {
return Err(CellosError::InvalidSpec(format!(
"http export target {:?} host {}:{} must appear in authority.egressRules",
target.name, host, port
)));
}
}
}
}
}
if let Some(artifacts) = &export.artifacts {
for artifact in artifacts {
ensure_portable_identifier(&artifact.name, "spec.export.artifacts[].name")?;
match artifact.target.as_deref() {
Some(target_name) => {
ensure_portable_identifier(target_name, "spec.export.artifacts[].target")?;
if !target_names.contains(target_name) {
return Err(CellosError::InvalidSpec(format!(
"export artifact {:?} references unknown target {:?}",
artifact.name, target_name
)));
}
}
None if targets.len() > 1 => {
return Err(CellosError::InvalidSpec(format!(
"export artifact {:?} must set target when multiple export targets exist",
artifact.name
)));
}
None => {}
}
}
}
}
if let Some(telemetry) = &doc.spec.telemetry {
validate_telemetry_block(telemetry, doc.spec.authority.egress_rules.as_deref())?;
}
Ok(())
}
fn validate_telemetry_block(
telemetry: &crate::TelemetrySpec,
egress_rules: Option<&[crate::EgressRule]>,
) -> Result<(), CellosError> {
if telemetry.events.is_empty() {
return Err(CellosError::InvalidSpec(
"spec.telemetry.events must be non-empty".into(),
));
}
match telemetry.channel {
crate::TelemetryChannel::VsockCbor => {}
}
if !is_semver_shape(&telemetry.agent_version) {
return Err(CellosError::InvalidSpec(
"spec.telemetry.agentVersion must match MAJOR.MINOR.PATCH semver shape (optional -prerelease suffix)".into()
));
}
let has_egress = egress_rules.map(|r| !r.is_empty()).unwrap_or(false);
for event in &telemetry.events {
let trimmed = event.trim();
if trimmed.is_empty() {
return Err(CellosError::InvalidSpec(
"spec.telemetry.events[] entries must be non-empty".into(),
));
}
if trimmed.to_ascii_lowercase().starts_with("net.") && !has_egress {
return Err(CellosError::InvalidSpec(format!(
"telemetry_without_egress: spec.telemetry.events[] entry {trimmed:?} requires \
at least one authority.egressRules entry — net.* telemetry without declared \
egress is an unbacked observation claim"
)));
}
}
Ok(())
}
fn is_semver_shape(value: &str) -> bool {
let core = match value.split_once('-') {
Some((core, pre)) => {
if pre.is_empty()
|| !pre
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '-'))
{
return false;
}
core
}
None => value,
};
let parts: Vec<&str> = core.split('.').collect();
if parts.len() != 3 {
return false;
}
parts.iter().all(|p| {
!p.is_empty()
&& p.chars().all(|c| c.is_ascii_digit())
&& !(p.len() > 1 && p.starts_with('0'))
})
}
fn verify_authority_derivation_structural(
spec: &crate::ExecutionCellSpec,
token: &crate::AuthorityDerivationToken,
) -> Result<(), crate::error::CellosError> {
let spec_capability = crate::AuthorityCapability {
egress_rules: spec.authority.egress_rules.clone().unwrap_or_default(),
secret_refs: spec.authority.secret_refs.clone().unwrap_or_default(),
};
if !spec_capability.is_superset_of(&token.leaf_capability) {
return Err(crate::error::CellosError::InvalidSpec(
"spec.authorityDerivation.leafCapability exceeds spec.authority — child authority must be ⊆ declared authority".into()
));
}
Ok(())
}
fn validate_dns_authority(dns: &crate::DnsAuthority) -> Result<(), CellosError> {
let mut resolver_ids: HashSet<&str> = HashSet::new();
for resolver in &dns.resolvers {
ensure_portable_identifier(
&resolver.resolver_id,
"authority.dnsAuthority.resolvers[].resolverId",
)?;
if !resolver_ids.insert(resolver.resolver_id.as_str()) {
return Err(CellosError::InvalidSpec(format!(
"authority.dnsAuthority.resolvers[].resolverId duplicates value {:?}",
resolver.resolver_id
)));
}
if resolver.endpoint.is_empty() {
return Err(CellosError::InvalidSpec(
"authority.dnsAuthority.resolvers[].endpoint must be non-empty".into(),
));
}
if let Some(kid) = &resolver.trust_kid {
ensure_portable_identifier(kid, "authority.dnsAuthority.resolvers[].trustKid")?;
}
}
for hostname in &dns.hostname_allowlist {
if !is_fqdn_or_wildcard(hostname) {
return Err(CellosError::InvalidSpec(format!(
"authority.dnsAuthority.hostnameAllowlist entry {hostname:?} is not a valid FQDN \
(single leading '*.' wildcard allowed; IPs are rejected)"
)));
}
}
if let Some(refresh) = &dns.refresh_policy {
if let (Some(min_ttl), Some(max_stale)) =
(refresh.min_ttl_seconds, refresh.max_stale_seconds)
{
if min_ttl > max_stale {
return Err(CellosError::InvalidSpec(format!(
"authority.dnsAuthority.refreshPolicy.minTtlSeconds ({min_ttl}) must be <= maxStaleSeconds ({max_stale})"
)));
}
}
}
Ok(())
}
fn validate_cdn_authority(cdn: &crate::CdnAuthority) -> Result<(), CellosError> {
let mut provider_ids: HashSet<&str> = HashSet::new();
for provider in &cdn.providers {
ensure_portable_identifier(
&provider.provider_id,
"authority.cdnAuthority.providers[].providerId",
)?;
if !provider_ids.insert(provider.provider_id.as_str()) {
return Err(CellosError::InvalidSpec(format!(
"authority.cdnAuthority.providers[].providerId duplicates value {:?}",
provider.provider_id
)));
}
if !is_fqdn_or_wildcard(&provider.hostname_pattern) {
return Err(CellosError::InvalidSpec(format!(
"authority.cdnAuthority.providers[].hostnamePattern {:?} is not a valid FQDN \
(single leading '*.' wildcard allowed; IPs are rejected)",
provider.hostname_pattern
)));
}
}
Ok(())
}
const MAX_ARGV_ENCODED_BYTES: usize = 3072;
pub(crate) fn check_argv_size_within_kernel_cmdline_limit(
argv: &[String],
) -> Result<(), CellosError> {
use base64::engine::general_purpose::STANDARD;
use base64::Engine as _;
let encoded_len = match serde_json::to_string(argv) {
Ok(json) => STANDARD.encode(json.as_bytes()).len(),
Err(_) => {
let json_upper = 2 + argv
.iter()
.map(|s| s.len().saturating_mul(2).saturating_add(3))
.sum::<usize>();
json_upper.div_ceil(3).saturating_mul(4)
}
};
if encoded_len > MAX_ARGV_ENCODED_BYTES {
return Err(CellosError::ArgvTooLarge {
encoded_bytes: encoded_len,
limit_bytes: MAX_ARGV_ENCODED_BYTES,
});
}
Ok(())
}
pub fn authority_derivation_signing_payload(
token: &crate::AuthorityDerivationToken,
) -> Result<Vec<u8>, crate::error::CellosError> {
let value = serde_json::json!({
"roleRoot": token.role_root.to_string(),
"leafCapability": &token.leaf_capability,
"parentRunId": token.parent_run_id,
});
serde_json::to_vec(&value).map_err(|e| {
crate::error::CellosError::InvalidSpec(format!(
"authority derivation signing payload encode failed: {e}"
))
})
}
pub fn verify_authority_derivation(
spec: &crate::ExecutionCellSpec,
token: &crate::AuthorityDerivationToken,
role_keys: &std::collections::HashMap<String, String>,
) -> Result<(), crate::error::CellosError> {
use base64::engine::general_purpose::STANDARD;
use base64::Engine as _;
use ed25519_dalek::{Signature, VerifyingKey};
verify_authority_derivation_structural(spec, token)?;
let role_id = token.role_root.to_string();
let verifying_key_b64 = role_keys.get(&role_id).ok_or_else(|| {
crate::error::CellosError::InvalidSpec(format!("unknown role: {role_id}"))
})?;
let verifying_key_bytes = STANDARD.decode(verifying_key_b64.as_bytes()).map_err(|e| {
crate::error::CellosError::InvalidSpec(format!(
"authority derivation verifying key for role {role_id} is not valid base64: {e}"
))
})?;
let verifying_key_array: [u8; 32] =
verifying_key_bytes.as_slice().try_into().map_err(|_| {
crate::error::CellosError::InvalidSpec(format!(
"authority derivation verifying key for role {role_id} must be 32 bytes (got {})",
verifying_key_bytes.len()
))
})?;
let verifying_key = VerifyingKey::from_bytes(&verifying_key_array).map_err(|e| {
crate::error::CellosError::InvalidSpec(format!(
"authority derivation verifying key for role {role_id} is not a valid ED25519 point: {e}"
))
})?;
let sig_bytes = STANDARD
.decode(token.grantor_signature.bytes.as_bytes())
.map_err(|e| {
crate::error::CellosError::InvalidSpec(format!(
"authority derivation grantor signature is not valid base64: {e}"
))
})?;
let sig_array: [u8; 64] = sig_bytes.as_slice().try_into().map_err(|_| {
crate::error::CellosError::InvalidSpec(format!(
"authority derivation grantor signature must be 64 bytes (got {})",
sig_bytes.len()
))
})?;
let signature = Signature::from_bytes(&sig_array);
let payload = authority_derivation_signing_payload(token)?;
verifying_key
.verify_strict(&payload, &signature)
.map_err(|_| {
crate::error::CellosError::InvalidSpec("authority derivation signature invalid".into())
})?;
tracing::debug!(
role_root = %token.role_root,
"authority derivation token verified (structural + signature)"
);
Ok(())
}
pub fn enforce_derivation_scope_policy(
token: &crate::AuthorityDerivationToken,
allow_universal: bool,
) -> Result<(), crate::error::CellosError> {
if token.parent_run_id.is_some() {
return Ok(());
}
if allow_universal {
tracing::warn!(
target: "cellos.supervisor.authority",
role_root = %token.role_root,
"authority derivation token has parentRunId: null — universal token accepted in permissive mode (unset CELLOS_REQUIRE_SCOPED_DERIVATION_TOKENS or set it to 1/true/yes/on to restore the strict-by-default posture)"
);
return Ok(());
}
Err(crate::error::CellosError::InvalidSpec(
"authority derivation token has parentRunId: null — universal tokens are rejected by the strict-by-default policy (set CELLOS_REQUIRE_SCOPED_DERIVATION_TOKENS to 0/false/no/off to opt into permissive mode, which emits an audit warning event instead)".into(),
))
}
pub fn verify_signed_trust_keyset_envelope(
envelope: &crate::types::SignedTrustKeysetEnvelope,
verifying_keys: &std::collections::HashMap<String, ed25519_dalek::VerifyingKey>,
now: std::time::SystemTime,
) -> Result<Vec<u8>, crate::error::CellosError> {
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine as _;
use ed25519_dalek::Signature;
use std::collections::HashSet;
if envelope.payload_type != "application/vnd.cellos.trust-keyset-v1+json" {
return Err(crate::error::CellosError::InvalidSpec(format!(
"signed trust keyset envelope payloadType must be application/vnd.cellos.trust-keyset-v1+json, got '{}'",
envelope.payload_type
)));
}
let payload_b64 = envelope.payload.trim_end_matches('=');
let payload_bytes = URL_SAFE_NO_PAD.decode(payload_b64).map_err(|e| {
crate::error::CellosError::InvalidSpec(format!(
"signed trust keyset envelope payload is not valid base64url: {e}"
))
})?;
let computed_digest = sha256_hex_prefixed(&payload_bytes);
if computed_digest != envelope.payload_digest {
return Err(crate::error::CellosError::InvalidSpec(format!(
"signed trust keyset envelope payload digest mismatch: declared={}, computed={}",
envelope.payload_digest, computed_digest
)));
}
if envelope.signatures.is_empty() {
return Err(crate::error::CellosError::InvalidSpec(
"signed trust keyset envelope has no signatures".into(),
));
}
let required = envelope.required_signer_count.unwrap_or(1).max(1);
let mut verified_signers: HashSet<&str> = HashSet::new();
for sig_entry in &envelope.signatures {
if sig_entry.algorithm != "ed25519" {
continue;
}
if verified_signers.contains(sig_entry.signer_kid.as_str()) {
continue;
}
let Some(verifying_key) = verifying_keys.get(&sig_entry.signer_kid) else {
continue;
};
if !signature_window_contains(
now,
sig_entry.not_before.as_deref(),
sig_entry.not_after.as_deref(),
)? {
continue;
}
let sig_b64 = sig_entry.signature.trim_end_matches('=');
let Ok(sig_bytes) = URL_SAFE_NO_PAD.decode(sig_b64) else {
continue;
};
let Ok(sig_array) = <[u8; 64]>::try_from(sig_bytes.as_slice()) else {
continue;
};
let signature = Signature::from_bytes(&sig_array);
if verifying_key
.verify_strict(&payload_bytes, &signature)
.is_ok()
{
verified_signers.insert(sig_entry.signer_kid.as_str());
}
}
if verified_signers.len() >= required as usize {
return Ok(payload_bytes);
}
if required == 1 {
Err(crate::error::CellosError::InvalidSpec(
"signed trust keyset envelope: no signature verified".into(),
))
} else {
Err(crate::error::CellosError::InvalidSpec(format!(
"signed trust keyset envelope: only {} distinct signers verified, need {}",
verified_signers.len(),
required
)))
}
}
pub fn verify_signed_trust_keyset_chain(
chain: &[crate::types::SignedTrustKeysetEnvelope],
verifying_keys: &std::collections::HashMap<String, ed25519_dalek::VerifyingKey>,
now: std::time::SystemTime,
) -> Result<Vec<u8>, crate::error::CellosError> {
if chain.is_empty() {
return Err(crate::error::CellosError::InvalidSpec(
"signed trust keyset chain: empty chain".into(),
));
}
let mut prev_payload_bytes: Option<Vec<u8>> = None;
for (idx, envelope) in chain.iter().enumerate() {
let payload_bytes = verify_signed_trust_keyset_envelope(envelope, verifying_keys, now)
.map_err(|e| {
crate::error::CellosError::InvalidSpec(format!(
"signed trust keyset chain: envelope at index {idx} failed verification: {e}"
))
})?;
if let Some(prev_bytes) = prev_payload_bytes.as_deref() {
let expected = sha256_hex_prefixed(prev_bytes);
match envelope.replaces_envelope_digest.as_deref() {
Some(actual) if actual == expected => {}
Some(actual) => {
return Err(crate::error::CellosError::InvalidSpec(format!(
"signed trust keyset chain: envelope at index {idx} replacesEnvelopeDigest mismatch: declared={actual}, expected={expected}"
)));
}
None => {
return Err(crate::error::CellosError::InvalidSpec(format!(
"signed trust keyset chain: envelope at index {idx} missing replacesEnvelopeDigest (only the genesis envelope at index 0 may omit it)"
)));
}
}
}
prev_payload_bytes = Some(payload_bytes);
}
Ok(prev_payload_bytes.expect("chain non-empty checked above"))
}
fn sha256_hex_prefixed(bytes: &[u8]) -> String {
use sha2::{Digest, Sha256};
use std::fmt::Write as _;
let out = Sha256::new().chain_update(bytes).finalize();
let mut hex = String::with_capacity(7 + 64);
hex.push_str("sha256:");
for b in out.iter() {
let _ = write!(hex, "{b:02x}");
}
hex
}
fn signature_window_contains(
now: std::time::SystemTime,
not_before: Option<&str>,
not_after: Option<&str>,
) -> Result<bool, crate::error::CellosError> {
use chrono::{DateTime, Utc};
let now_chrono: DateTime<Utc> = now.into();
if let Some(nb) = not_before {
let parsed = DateTime::parse_from_rfc3339(nb).map_err(|e| {
crate::error::CellosError::InvalidSpec(format!(
"signed trust keyset envelope notBefore '{nb}' is not RFC3339: {e}"
))
})?;
if now_chrono < parsed.with_timezone(&Utc) {
return Ok(false);
}
}
if let Some(na) = not_after {
let parsed = DateTime::parse_from_rfc3339(na).map_err(|e| {
crate::error::CellosError::InvalidSpec(format!(
"signed trust keyset envelope notAfter '{na}' is not RFC3339: {e}"
))
})?;
if now_chrono > parsed.with_timezone(&Utc) {
return Ok(false);
}
}
Ok(true)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
AuthorityBundle, EnvironmentSpec, ExecutionCellSpec, ExportArtifact, ExportChannels,
ExportTarget, HttpExportTarget, Lifetime, RunCpuMax, RunLimits, RunSpec, S3ExportTarget,
SecretDeliveryMode, WorkloadIdentity, WorkloadIdentityKind,
};
#[test]
fn rejects_empty_argv_token() {
let doc = ExecutionCellDocument {
api_version: "cellos.io/v1".into(),
kind: "ExecutionCell".into(),
spec: ExecutionCellSpec {
id: "x".into(),
correlation: None,
ingress: None,
environment: None,
placement: None,
policy: None,
identity: None,
run: Some(RunSpec {
argv: vec!["sh".into(), "".into()],
working_directory: None,
timeout_ms: None,
limits: None,
secret_delivery: SecretDeliveryMode::Env,
}),
authority: AuthorityBundle::default(),
lifetime: Lifetime { ttl_seconds: 1 },
export: None,
telemetry: None,
},
};
assert!(validate_execution_cell_document(&doc).is_err());
}
#[test]
fn admits_argv_under_kernel_cmdline_limit() {
let payload = "a".repeat(1024);
let doc = ExecutionCellDocument {
api_version: "cellos.io/v1".into(),
kind: "ExecutionCell".into(),
spec: ExecutionCellSpec {
id: "argv-fits".into(),
correlation: None,
ingress: None,
environment: None,
placement: None,
policy: None,
identity: None,
run: Some(RunSpec {
argv: vec!["sh".into(), payload],
working_directory: None,
timeout_ms: None,
limits: None,
secret_delivery: SecretDeliveryMode::Env,
}),
authority: AuthorityBundle::default(),
lifetime: Lifetime { ttl_seconds: 1 },
export: None,
telemetry: None,
},
};
validate_execution_cell_document(&doc).expect("1 KiB argv must pass admission");
}
#[test]
fn rejects_argv_exceeding_kernel_cmdline_limit() {
let payload = "a".repeat(5 * 1024);
let doc = ExecutionCellDocument {
api_version: "cellos.io/v1".into(),
kind: "ExecutionCell".into(),
spec: ExecutionCellSpec {
id: "argv-too-big".into(),
correlation: None,
ingress: None,
environment: None,
placement: None,
policy: None,
identity: None,
run: Some(RunSpec {
argv: vec!["sh".into(), payload],
working_directory: None,
timeout_ms: None,
limits: None,
secret_delivery: SecretDeliveryMode::Env,
}),
authority: AuthorityBundle::default(),
lifetime: Lifetime { ttl_seconds: 1 },
export: None,
telemetry: None,
},
};
let err = validate_execution_cell_document(&doc).expect_err("5 KiB argv must reject");
match err {
CellosError::ArgvTooLarge {
encoded_bytes,
limit_bytes,
} => {
assert!(
encoded_bytes > limit_bytes,
"encoded_bytes ({encoded_bytes}) must exceed limit_bytes ({limit_bytes})"
);
assert_eq!(limit_bytes, 3072, "FC-17 budget is 3 KiB");
}
other => panic!("expected CellosError::ArgvTooLarge, got: {other:?}"),
}
}
#[test]
fn rejects_identity_ttl_longer_than_cell_ttl() {
let doc = ExecutionCellDocument {
api_version: "cellos.io/v1".into(),
kind: "ExecutionCell".into(),
spec: ExecutionCellSpec {
id: "x".into(),
correlation: None,
ingress: None,
environment: None,
placement: None,
policy: None,
identity: Some(WorkloadIdentity {
kind: WorkloadIdentityKind::FederatedOidc,
provider: "github-actions".into(),
audience: "sts.amazonaws.com".into(),
subject: None,
ttl_seconds: Some(120),
secret_ref: "AWS_WEB_IDENTITY".into(),
}),
run: None,
authority: AuthorityBundle {
filesystem: None,
network: None,
egress_rules: None,
secret_refs: Some(vec!["AWS_WEB_IDENTITY".into()]),
authority_derivation: None,
dns_authority: None,
cdn_authority: None,
},
lifetime: Lifetime { ttl_seconds: 60 },
export: None,
telemetry: None,
},
};
assert!(validate_execution_cell_document(&doc).is_err());
}
#[test]
fn rejects_unknown_export_target_reference() {
let doc = ExecutionCellDocument {
api_version: "cellos.io/v1".into(),
kind: "ExecutionCell".into(),
spec: ExecutionCellSpec {
id: "x".into(),
correlation: None,
ingress: None,
environment: None,
placement: None,
policy: None,
identity: None,
run: None,
authority: AuthorityBundle {
filesystem: None,
network: None,
egress_rules: None,
secret_refs: Some(vec!["AWS_WEB_IDENTITY".into()]),
authority_derivation: None,
dns_authority: None,
cdn_authority: None,
},
lifetime: Lifetime { ttl_seconds: 60 },
export: Some(ExportChannels {
artifacts: Some(vec![ExportArtifact {
name: "junit".into(),
path: "/tmp/junit.xml".into(),
target: Some("missing".into()),
content_type: None,
}]),
targets: Some(vec![ExportTarget::S3(S3ExportTarget {
name: "artifacts".into(),
bucket: "cellos-artifacts".into(),
key_prefix: None,
region: None,
secret_ref: Some("AWS_WEB_IDENTITY".into()),
})]),
}),
telemetry: None,
},
};
assert!(validate_execution_cell_document(&doc).is_err());
}
#[test]
fn rejects_spec_id_with_path_traversal() {
let doc = ExecutionCellDocument {
api_version: "cellos.io/v1".into(),
kind: "ExecutionCell".into(),
spec: ExecutionCellSpec {
id: "../escape".into(),
correlation: None,
ingress: None,
environment: None,
placement: None,
policy: None,
identity: None,
run: None,
authority: AuthorityBundle::default(),
lifetime: Lifetime { ttl_seconds: 60 },
export: None,
telemetry: None,
},
};
assert!(validate_execution_cell_document(&doc).is_err());
}
#[test]
fn rejects_export_artifact_name_with_dotdot() {
let doc = ExecutionCellDocument {
api_version: "cellos.io/v1".into(),
kind: "ExecutionCell".into(),
spec: ExecutionCellSpec {
id: "safe-cell".into(),
correlation: None,
ingress: None,
environment: None,
placement: None,
policy: None,
identity: None,
run: None,
authority: AuthorityBundle::default(),
lifetime: Lifetime { ttl_seconds: 60 },
export: Some(ExportChannels {
artifacts: Some(vec![ExportArtifact {
name: "bad..name".into(),
path: "/tmp/junit.xml".into(),
target: None,
content_type: None,
}]),
targets: None,
}),
telemetry: None,
},
};
assert!(validate_execution_cell_document(&doc).is_err());
}
#[test]
fn rejects_secret_ref_with_separator() {
let doc = ExecutionCellDocument {
api_version: "cellos.io/v1".into(),
kind: "ExecutionCell".into(),
spec: ExecutionCellSpec {
id: "safe-cell".into(),
correlation: None,
ingress: None,
environment: None,
placement: None,
policy: None,
identity: None,
run: None,
authority: AuthorityBundle {
filesystem: None,
network: None,
egress_rules: None,
secret_refs: Some(vec!["bad/ref".into()]),
authority_derivation: None,
dns_authority: None,
cdn_authority: None,
},
lifetime: Lifetime { ttl_seconds: 60 },
export: None,
telemetry: None,
},
};
assert!(validate_execution_cell_document(&doc).is_err());
}
#[test]
fn rejects_http_export_target_with_non_http_base_url() {
let doc = ExecutionCellDocument {
api_version: "cellos.io/v1".into(),
kind: "ExecutionCell".into(),
spec: ExecutionCellSpec {
id: "safe-cell".into(),
correlation: None,
ingress: None,
environment: None,
placement: None,
policy: None,
identity: None,
run: None,
authority: AuthorityBundle {
filesystem: None,
network: None,
egress_rules: None,
secret_refs: Some(vec!["ARTIFACT_API_TOKEN".into()]),
authority_derivation: None,
dns_authority: None,
cdn_authority: None,
},
lifetime: Lifetime { ttl_seconds: 60 },
export: Some(ExportChannels {
artifacts: Some(vec![ExportArtifact {
name: "coverage-summary".into(),
path: "/tmp/coverage.txt".into(),
target: Some("artifact-api".into()),
content_type: Some("text/plain".into()),
}]),
targets: Some(vec![ExportTarget::Http(HttpExportTarget {
name: "artifact-api".into(),
base_url: "ftp://artifacts.example.invalid/upload".into(),
secret_ref: Some("ARTIFACT_API_TOKEN".into()),
})]),
}),
telemetry: None,
},
};
assert!(validate_execution_cell_document(&doc).is_err());
}
use super::is_portable_identifier;
use proptest::prelude::*;
fn minimal_doc_with_placement(placement: crate::PlacementSpec) -> ExecutionCellDocument {
ExecutionCellDocument {
api_version: "cellos.io/v1".into(),
kind: "ExecutionCell".into(),
spec: ExecutionCellSpec {
id: "placement-test-cell".into(),
correlation: None,
ingress: None,
environment: None,
placement: Some(placement),
policy: None,
identity: None,
run: None,
authority: AuthorityBundle::default(),
lifetime: Lifetime { ttl_seconds: 60 },
export: None,
telemetry: None,
},
}
}
#[test]
fn rejects_placement_pool_id_with_separator() {
let doc = minimal_doc_with_placement(crate::PlacementSpec {
pool_id: Some("pool/main".into()),
kubernetes_namespace: None,
queue_name: None,
});
let err = validate_execution_cell_document(&doc).unwrap_err();
assert!(err.to_string().contains("spec.placement.poolId"));
}
#[test]
fn rejects_empty_placement_hints() {
let doc = minimal_doc_with_placement(crate::PlacementSpec::default());
let err = validate_execution_cell_document(&doc).unwrap_err();
assert!(err
.to_string()
.contains("spec.placement must set at least one placement hint"));
}
#[test]
fn rejects_placement_queue_name_with_dotdot() {
let doc = minimal_doc_with_placement(crate::PlacementSpec {
pool_id: None,
kubernetes_namespace: None,
queue_name: Some("ci..high".into()),
});
let err = validate_execution_cell_document(&doc).unwrap_err();
assert!(err.to_string().contains("spec.placement.queueName"));
}
#[test]
fn rejects_placement_namespace_with_uppercase() {
let doc = minimal_doc_with_placement(crate::PlacementSpec {
pool_id: None,
kubernetes_namespace: Some("CellOS-Prod".into()),
queue_name: None,
});
let err = validate_execution_cell_document(&doc).unwrap_err();
assert!(err
.to_string()
.contains("spec.placement.kubernetesNamespace"));
}
#[test]
fn accepts_valid_placement_hints() {
let doc = minimal_doc_with_placement(crate::PlacementSpec {
pool_id: Some("runner-pool-amd64".into()),
kubernetes_namespace: Some("cellos-prod".into()),
queue_name: Some("ci-high".into()),
});
assert!(validate_execution_cell_document(&doc).is_ok());
}
proptest! {
#[test]
fn prop_rejects_slash_in_identifier(
prefix in "[A-Za-z0-9._-]{0,20}",
suffix in "[A-Za-z0-9._-]{0,20}",
) {
let with_slash = format!("{prefix}/{suffix}");
prop_assert!(!is_portable_identifier(&with_slash), "slash in {with_slash:?}");
}
#[test]
fn prop_rejects_backslash_in_identifier(
prefix in "[A-Za-z0-9._-]{0,20}",
suffix in "[A-Za-z0-9._-]{0,20}",
) {
let with_bs = format!("{prefix}\\{suffix}");
prop_assert!(!is_portable_identifier(&with_bs), "backslash in {with_bs:?}");
}
#[test]
fn prop_rejects_dotdot_in_identifier(
prefix in "[A-Za-z0-9._-]{0,20}",
suffix in "[A-Za-z0-9._-]{0,20}",
) {
let with_dotdot = format!("{prefix}..{suffix}");
prop_assert!(!is_portable_identifier(&with_dotdot), ".. in {with_dotdot:?}");
}
#[test]
fn prop_rejects_non_alphanum_first_char(
first in "[._\\-]",
rest in "[A-Za-z0-9._-]{0,20}",
) {
let s = format!("{first}{rest}");
prop_assert!(!is_portable_identifier(&s), "starts with non-alphanum: {s:?}");
}
#[test]
fn prop_accepts_valid_identifiers(
first in "[A-Za-z0-9]",
rest in "[A-Za-z0-9._-]{0,60}",
) {
let s = format!("{first}{rest}");
prop_assume!(!s.contains(".."));
prop_assert!(is_portable_identifier(&s), "should accept {s:?}");
}
#[test]
fn prop_invalid_spec_id_rejected_by_validate(
prefix in "[A-Za-z0-9._-]{0,10}",
suffix in "[A-Za-z0-9._-]{0,10}",
) {
let bad_id = format!("{prefix}/{suffix}");
let doc = ExecutionCellDocument {
api_version: "cellos.io/v1".into(),
kind: "ExecutionCell".into(),
spec: ExecutionCellSpec {
id: bad_id.clone(),
correlation: None,
ingress: None,
environment: None,
placement: None,
policy: None,
identity: None,
run: None,
authority: AuthorityBundle::default(),
lifetime: Lifetime { ttl_seconds: 60 },
export: None,
telemetry: None,
},
};
prop_assert!(
validate_execution_cell_document(&doc).is_err(),
"expected error for id {bad_id:?}"
);
}
#[test]
fn prop_empty_argv_token_rejected(
prefix_args in prop::collection::vec("[A-Za-z0-9/_-]{1,20}", 0..5),
suffix_args in prop::collection::vec("[A-Za-z0-9/_-]{1,20}", 0..5),
) {
let mut argv: Vec<String> = prefix_args;
argv.push(String::new()); argv.extend(suffix_args);
let doc = ExecutionCellDocument {
api_version: "cellos.io/v1".into(),
kind: "ExecutionCell".into(),
spec: ExecutionCellSpec {
id: "valid-cell".into(),
correlation: None,
ingress: None,
environment: None,
placement: None,
policy: None,
identity: None,
run: Some(RunSpec {
argv,
working_directory: None,
timeout_ms: None,
limits: None,
secret_delivery: SecretDeliveryMode::Env,
}),
authority: AuthorityBundle::default(),
lifetime: Lifetime { ttl_seconds: 60 },
export: None,
telemetry: None,
},
};
prop_assert!(
validate_execution_cell_document(&doc).is_err(),
"empty argv token should be rejected"
);
}
}
#[test]
fn rejects_http_export_target_missing_egress_rule() {
let doc = ExecutionCellDocument {
api_version: "cellos.io/v1".into(),
kind: "ExecutionCell".into(),
spec: ExecutionCellSpec {
id: "safe-cell".into(),
correlation: None,
ingress: None,
environment: None,
placement: None,
policy: None,
identity: None,
run: None,
authority: AuthorityBundle {
filesystem: None,
network: None,
egress_rules: Some(vec![crate::EgressRule {
host: "api.github.com".into(),
port: 443,
protocol: Some("tls".into()),
dns_egress_justification: None,
}]),
secret_refs: Some(vec!["ARTIFACT_API_TOKEN".into()]),
authority_derivation: None,
dns_authority: None,
cdn_authority: None,
},
lifetime: Lifetime { ttl_seconds: 60 },
export: Some(ExportChannels {
artifacts: Some(vec![ExportArtifact {
name: "coverage-summary".into(),
path: "/tmp/coverage.txt".into(),
target: Some("artifact-api".into()),
content_type: Some("text/plain".into()),
}]),
targets: Some(vec![ExportTarget::Http(HttpExportTarget {
name: "artifact-api".into(),
base_url: "https://artifacts.acme.internal/upload".into(),
secret_ref: Some("ARTIFACT_API_TOKEN".into()),
})]),
}),
telemetry: None,
},
};
assert!(validate_execution_cell_document(&doc).is_err());
}
#[test]
fn rejects_run_timeout_longer_than_lifetime() {
let doc = ExecutionCellDocument {
api_version: "cellos.io/v1".into(),
kind: "ExecutionCell".into(),
spec: ExecutionCellSpec {
id: "safe-cell".into(),
correlation: None,
ingress: None,
environment: None,
placement: None,
policy: None,
identity: None,
run: Some(RunSpec {
argv: vec!["/usr/bin/true".into()],
working_directory: None,
timeout_ms: Some(61_000),
limits: None,
secret_delivery: SecretDeliveryMode::Env,
}),
authority: AuthorityBundle::default(),
lifetime: Lifetime { ttl_seconds: 60 },
export: None,
telemetry: None,
},
};
assert!(validate_execution_cell_document(&doc).is_err());
}
#[test]
fn accepts_run_timeout_equal_to_lifetime() {
let doc = ExecutionCellDocument {
api_version: "cellos.io/v1".into(),
kind: "ExecutionCell".into(),
spec: ExecutionCellSpec {
id: "safe-cell-boundary".into(),
correlation: None,
ingress: None,
environment: None,
placement: None,
policy: None,
identity: None,
run: Some(RunSpec {
argv: vec!["/usr/bin/true".into()],
working_directory: None,
timeout_ms: Some(60_000),
limits: None,
secret_delivery: SecretDeliveryMode::Env,
}),
authority: AuthorityBundle::default(),
lifetime: Lifetime { ttl_seconds: 60 },
export: None,
telemetry: None,
},
};
assert!(
validate_execution_cell_document(&doc).is_ok(),
"timeout_ms == ttl_seconds * 1000 must be accepted (boundary)"
);
}
#[test]
fn rejects_run_timeout_one_ms_over_lifetime() {
let doc = ExecutionCellDocument {
api_version: "cellos.io/v1".into(),
kind: "ExecutionCell".into(),
spec: ExecutionCellSpec {
id: "safe-cell-overshoot".into(),
correlation: None,
ingress: None,
environment: None,
placement: None,
policy: None,
identity: None,
run: Some(RunSpec {
argv: vec!["/usr/bin/true".into()],
working_directory: None,
timeout_ms: Some(60_001),
limits: None,
secret_delivery: SecretDeliveryMode::Env,
}),
authority: AuthorityBundle::default(),
lifetime: Lifetime { ttl_seconds: 60 },
export: None,
telemetry: None,
},
};
let err = validate_execution_cell_document(&doc).expect_err("must reject");
let msg = format!("{err}");
assert!(
msg.contains("timeoutMs"),
"error must mention timeoutMs; got {msg:?}"
);
assert!(
msg.contains("ttlSeconds"),
"error must mention ttlSeconds; got {msg:?}"
);
}
#[test]
fn accepts_run_limits_and_timeout_within_lifetime() {
let doc = ExecutionCellDocument {
api_version: "cellos.io/v1".into(),
kind: "ExecutionCell".into(),
spec: ExecutionCellSpec {
id: "safe-cell".into(),
correlation: None,
ingress: None,
environment: None,
placement: None,
policy: None,
identity: None,
run: Some(RunSpec {
argv: vec!["/usr/bin/true".into()],
working_directory: None,
timeout_ms: Some(5_000),
limits: Some(RunLimits {
memory_max_bytes: Some(268_435_456),
cpu_max: Some(RunCpuMax {
quota_micros: 50_000,
period_micros: Some(100_000),
}),
graceful_shutdown_seconds: None,
}),
secret_delivery: SecretDeliveryMode::Env,
}),
authority: AuthorityBundle::default(),
lifetime: Lifetime { ttl_seconds: 60 },
export: None,
telemetry: None,
},
};
assert!(validate_execution_cell_document(&doc).is_ok());
}
fn minimal_doc_with_env(env: EnvironmentSpec) -> ExecutionCellDocument {
ExecutionCellDocument {
api_version: "cellos.io/v1".into(),
kind: "ExecutionCell".into(),
spec: ExecutionCellSpec {
id: "env-test-cell".into(),
correlation: None,
ingress: None,
environment: Some(env),
placement: None,
policy: None,
identity: None,
run: None,
authority: AuthorityBundle::default(),
lifetime: Lifetime { ttl_seconds: 60 },
export: None,
telemetry: None,
},
}
}
#[test]
fn accepts_environment_with_reference_only() {
let doc = minimal_doc_with_env(EnvironmentSpec {
image_reference: "ubuntu:24.04".into(),
image_digest: None,
template_id: None,
});
assert!(validate_execution_cell_document(&doc).is_ok());
}
#[test]
fn accepts_environment_with_digest_and_template() {
let doc = minimal_doc_with_env(EnvironmentSpec {
image_reference: "ubuntu:24.04".into(),
image_digest: Some(
"sha256:a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2".into(),
),
template_id: Some("ubuntu-24-04-build".into()),
});
assert!(validate_execution_cell_document(&doc).is_ok());
}
#[test]
fn rejects_environment_with_empty_image_reference() {
let doc = minimal_doc_with_env(EnvironmentSpec {
image_reference: "".into(),
image_digest: None,
template_id: None,
});
let err = validate_execution_cell_document(&doc).unwrap_err();
assert!(
err.to_string().contains("imageReference"),
"expected imageReference in error, got: {err}"
);
}
#[test]
fn rejects_environment_with_bad_digest_missing_prefix() {
let doc = minimal_doc_with_env(EnvironmentSpec {
image_reference: "ubuntu:24.04".into(),
image_digest: Some(
"a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2".into(),
),
template_id: None,
});
let err = validate_execution_cell_document(&doc).unwrap_err();
assert!(err.to_string().contains("imageDigest"), "{err}");
}
#[test]
fn rejects_environment_with_digest_wrong_length() {
let doc = minimal_doc_with_env(EnvironmentSpec {
image_reference: "ubuntu:24.04".into(),
image_digest: Some("sha256:deadbeef".into()),
template_id: None,
});
let err = validate_execution_cell_document(&doc).unwrap_err();
assert!(err.to_string().contains("imageDigest"), "{err}");
}
#[test]
fn rejects_environment_with_uppercase_digest() {
let doc = minimal_doc_with_env(EnvironmentSpec {
image_reference: "ubuntu:24.04".into(),
image_digest: Some(
"sha256:A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2".into(),
),
template_id: None,
});
let err = validate_execution_cell_document(&doc).unwrap_err();
assert!(err.to_string().contains("imageDigest"), "{err}");
}
#[test]
fn rejects_environment_with_invalid_template_id() {
let doc = minimal_doc_with_env(EnvironmentSpec {
image_reference: "ubuntu:24.04".into(),
image_digest: None,
template_id: Some("../escape".into()),
});
let err = validate_execution_cell_document(&doc).unwrap_err();
assert!(err.to_string().contains("templateId"), "{err}");
}
#[test]
fn is_sha256_digest_accepts_valid() {
assert!(is_sha256_digest(
"sha256:a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
));
}
#[test]
fn is_sha256_digest_rejects_short() {
assert!(!is_sha256_digest("sha256:deadbeef"));
}
#[test]
fn is_sha256_digest_rejects_no_prefix() {
assert!(!is_sha256_digest(
"a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
));
}
#[test]
fn is_sha256_digest_rejects_uppercase() {
assert!(!is_sha256_digest(
"sha256:A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2"
));
}
fn derivation_spec_with_authority(
egress: Vec<crate::EgressRule>,
secret_refs: Vec<String>,
) -> ExecutionCellSpec {
ExecutionCellSpec {
id: "deriv-test".into(),
correlation: None,
ingress: None,
environment: None,
placement: None,
policy: None,
identity: None,
run: None,
authority: AuthorityBundle {
filesystem: None,
network: None,
egress_rules: Some(egress),
secret_refs: Some(secret_refs),
authority_derivation: None,
dns_authority: None,
cdn_authority: None,
},
lifetime: Lifetime { ttl_seconds: 60 },
export: None,
telemetry: None,
}
}
fn token_with_leaf(leaf: crate::AuthorityCapability) -> crate::AuthorityDerivationToken {
crate::AuthorityDerivationToken {
role_root: crate::RoleId("role-test".into()),
parent_run_id: None,
derivation_steps: vec![],
leaf_capability: leaf,
grantor_signature: crate::AuthoritySignature {
algorithm: "ed25519".into(),
bytes: "AA==".into(),
},
}
}
fn sign_token(
signing_key: &ed25519_dalek::SigningKey,
mut token: crate::AuthorityDerivationToken,
) -> crate::AuthorityDerivationToken {
use base64::engine::general_purpose::STANDARD;
use base64::Engine as _;
use ed25519_dalek::Signer as _;
let payload = authority_derivation_signing_payload(&token).expect("payload encode");
let signature = signing_key.sign(&payload);
token.grantor_signature.bytes = STANDARD.encode(signature.to_bytes());
token
}
fn test_signing_key(seed: u8) -> ed25519_dalek::SigningKey {
ed25519_dalek::SigningKey::from_bytes(&[seed; 32])
}
fn verifying_key_b64(signing_key: &ed25519_dalek::SigningKey) -> String {
use base64::engine::general_purpose::STANDARD;
use base64::Engine as _;
STANDARD.encode(signing_key.verifying_key().to_bytes())
}
#[test]
fn verify_authority_derivation_child_within_parent_passes() {
let spec = derivation_spec_with_authority(
vec![crate::EgressRule {
host: "api.example.com".into(),
port: 443,
protocol: Some("https".into()),
dns_egress_justification: None,
}],
vec!["api-key".into()],
);
let signing_key = test_signing_key(0x42);
let token = sign_token(
&signing_key,
token_with_leaf(crate::AuthorityCapability {
egress_rules: vec![crate::EgressRule {
host: "api.example.com".into(),
port: 443,
protocol: Some("https".into()),
dns_egress_justification: None,
}],
secret_refs: vec!["api-key".into()],
}),
);
let mut keys = std::collections::HashMap::new();
keys.insert("role-test".to_string(), verifying_key_b64(&signing_key));
assert!(verify_authority_derivation(&spec, &token, &keys).is_ok());
}
#[test]
fn verify_authority_derivation_child_exceeding_parent_fails_egress() {
let spec = derivation_spec_with_authority(
vec![crate::EgressRule {
host: "api.example.com".into(),
port: 443,
protocol: Some("https".into()),
dns_egress_justification: None,
}],
vec![],
);
let token = token_with_leaf(crate::AuthorityCapability {
egress_rules: vec![crate::EgressRule {
host: "evil.example.com".into(),
port: 443,
protocol: Some("https".into()),
dns_egress_justification: None,
}],
secret_refs: vec![],
});
let keys = std::collections::HashMap::new();
let err = verify_authority_derivation(&spec, &token, &keys).unwrap_err();
assert!(err.to_string().contains("leafCapability"), "{err}");
}
#[test]
fn verify_authority_derivation_child_exceeding_parent_fails_secrets() {
let spec = derivation_spec_with_authority(vec![], vec!["api-key".into()]);
let token = token_with_leaf(crate::AuthorityCapability {
egress_rules: vec![],
secret_refs: vec!["root-token".into()],
});
let keys = std::collections::HashMap::new();
let err = verify_authority_derivation(&spec, &token, &keys).unwrap_err();
assert!(err.to_string().contains("leafCapability"), "{err}");
}
#[test]
fn validate_doc_rejects_spec_with_exceeding_derivation_token() {
let mut spec = derivation_spec_with_authority(
vec![crate::EgressRule {
host: "api.example.com".into(),
port: 443,
protocol: Some("https".into()),
dns_egress_justification: None,
}],
vec![],
);
spec.authority.authority_derivation = Some(token_with_leaf(crate::AuthorityCapability {
egress_rules: vec![crate::EgressRule {
host: "evil.example.com".into(),
port: 443,
protocol: Some("https".into()),
dns_egress_justification: None,
}],
secret_refs: vec![],
}));
let doc = ExecutionCellDocument {
api_version: "cellos.io/v1".into(),
kind: "ExecutionCell".into(),
spec,
};
let err = validate_execution_cell_document(&doc).unwrap_err();
assert!(err.to_string().contains("leafCapability"), "{err}");
}
#[test]
fn validate_doc_accepts_spec_with_valid_derivation_token() {
let mut spec = derivation_spec_with_authority(
vec![crate::EgressRule {
host: "api.example.com".into(),
port: 443,
protocol: Some("https".into()),
dns_egress_justification: None,
}],
vec!["api-key".into()],
);
spec.authority.authority_derivation = Some(token_with_leaf(crate::AuthorityCapability {
egress_rules: vec![crate::EgressRule {
host: "api.example.com".into(),
port: 443,
protocol: Some("https".into()),
dns_egress_justification: None,
}],
secret_refs: vec!["api-key".into()],
}));
let doc = ExecutionCellDocument {
api_version: "cellos.io/v1".into(),
kind: "ExecutionCell".into(),
spec,
};
assert!(validate_execution_cell_document(&doc).is_ok());
}
fn signed_spec_and_token() -> (
crate::ExecutionCellSpec,
crate::AuthorityDerivationToken,
ed25519_dalek::SigningKey,
) {
let spec = derivation_spec_with_authority(
vec![crate::EgressRule {
host: "api.example.com".into(),
port: 443,
protocol: Some("https".into()),
dns_egress_justification: None,
}],
vec!["api-key".into()],
);
let signing_key = test_signing_key(0x11);
let token = sign_token(
&signing_key,
token_with_leaf(crate::AuthorityCapability {
egress_rules: vec![crate::EgressRule {
host: "api.example.com".into(),
port: 443,
protocol: Some("https".into()),
dns_egress_justification: None,
}],
secret_refs: vec!["api-key".into()],
}),
);
(spec, token, signing_key)
}
#[test]
fn test_good_signature_passes() {
let (spec, token, signing_key) = signed_spec_and_token();
let mut keys = std::collections::HashMap::new();
keys.insert("role-test".to_string(), verifying_key_b64(&signing_key));
assert!(verify_authority_derivation(&spec, &token, &keys).is_ok());
}
#[test]
fn test_bad_signature_rejected() {
let (spec, mut token, signing_key) = signed_spec_and_token();
use base64::engine::general_purpose::STANDARD;
use base64::Engine as _;
let mut sig_bytes = STANDARD
.decode(token.grantor_signature.bytes.as_bytes())
.expect("decode original signature");
let last = sig_bytes.len() - 1;
sig_bytes[last] ^= 0x01;
token.grantor_signature.bytes = STANDARD.encode(&sig_bytes);
let mut keys = std::collections::HashMap::new();
keys.insert("role-test".to_string(), verifying_key_b64(&signing_key));
let err = verify_authority_derivation(&spec, &token, &keys).unwrap_err();
match err {
crate::error::CellosError::InvalidSpec(msg) => {
assert!(
msg.contains("authority derivation signature invalid"),
"unexpected error message: {msg}"
);
}
other => panic!("expected InvalidSpec, got {other:?}"),
}
}
#[test]
fn test_unknown_role_rejected() {
let (spec, token, _signing_key) = signed_spec_and_token();
let other_key = test_signing_key(0x22);
let mut keys = std::collections::HashMap::new();
keys.insert("role-other".to_string(), verifying_key_b64(&other_key));
let err = verify_authority_derivation(&spec, &token, &keys).unwrap_err();
match err {
crate::error::CellosError::InvalidSpec(msg) => {
assert!(msg.contains("unknown role"), "unexpected message: {msg}");
assert!(
msg.contains("role-test"),
"expected role id in message: {msg}"
);
}
other => panic!("expected InvalidSpec, got {other:?}"),
}
}
#[test]
fn test_no_token_passes() {
let spec = derivation_spec_with_authority(
vec![crate::EgressRule {
host: "api.example.com".into(),
port: 443,
protocol: Some("https".into()),
dns_egress_justification: None,
}],
vec!["api-key".into()],
);
assert!(spec.authority.authority_derivation.is_none());
let doc = ExecutionCellDocument {
api_version: "cellos.io/v1".into(),
kind: "ExecutionCell".into(),
spec,
};
assert!(validate_execution_cell_document(&doc).is_ok());
}
#[test]
fn test_empty_keys_with_token_fails() {
let (spec, token, _signing_key) = signed_spec_and_token();
let keys: std::collections::HashMap<String, String> = std::collections::HashMap::new();
let err = verify_authority_derivation(&spec, &token, &keys).unwrap_err();
match err {
crate::error::CellosError::InvalidSpec(msg) => {
assert!(msg.contains("unknown role"), "unexpected message: {msg}");
}
other => panic!("expected InvalidSpec, got {other:?}"),
}
}
#[test]
fn enforce_scope_policy_universal_allowed_when_permissive() {
let (_spec, token, _signing_key) = signed_spec_and_token();
assert!(token.parent_run_id.is_none());
assert!(enforce_derivation_scope_policy(&token, true).is_ok());
}
#[test]
fn enforce_scope_policy_universal_rejected_when_strict() {
let (_spec, token, _signing_key) = signed_spec_and_token();
assert!(token.parent_run_id.is_none());
let err = enforce_derivation_scope_policy(&token, false).unwrap_err();
match err {
crate::error::CellosError::InvalidSpec(msg) => {
assert!(
msg.contains("parentRunId: null")
&& msg.contains("CELLOS_REQUIRE_SCOPED_DERIVATION_TOKENS"),
"unexpected message: {msg}"
);
}
other => panic!("expected InvalidSpec, got {other:?}"),
}
}
#[test]
fn enforce_scope_policy_scoped_token_passes_in_either_mode() {
let (_spec, mut token, _signing_key) = signed_spec_and_token();
token.parent_run_id = Some("run-2026-04-25-abc".into());
assert!(enforce_derivation_scope_policy(&token, true).is_ok());
assert!(enforce_derivation_scope_policy(&token, false).is_ok());
}
#[test]
#[ignore]
fn gen_signed_ci_runner_fixture() {
let signing_key = test_signing_key(0x42);
let leaf = crate::AuthorityCapability {
egress_rules: vec![
crate::EgressRule {
host: "api.github.com".into(),
port: 443,
protocol: Some("tls".into()),
dns_egress_justification: None,
},
crate::EgressRule {
host: "ghcr.io".into(),
port: 443,
protocol: Some("tls".into()),
dns_egress_justification: None,
},
crate::EgressRule {
host: "artifacts.internal".into(),
port: 443,
protocol: Some("tls".into()),
dns_egress_justification: None,
},
crate::EgressRule {
host: "dns.internal".into(),
port: 53,
protocol: Some("dns-acknowledged".into()),
dns_egress_justification: Some(
"Internal resolver at dns.internal required for artifacts.internal hostname resolution; nameserver is operator-controlled and air-gapped from public internet.".into(),
),
},
],
secret_refs: vec!["NPM_TOKEN".into(), "GITHUB_TOKEN".into()],
};
let token = crate::AuthorityDerivationToken {
role_root: crate::RoleId("role-ci-runner".into()),
parent_run_id: Some("run-demo-ref-001".into()),
derivation_steps: vec![],
leaf_capability: leaf,
grantor_signature: crate::AuthoritySignature {
algorithm: "ed25519".into(),
bytes: "AA==".into(),
},
};
let signed = sign_token(&signing_key, token);
eprintln!("FIXTURE seed=0x42 role=role-ci-runner parentRunId=run-demo-ref-001");
eprintln!(
"FIXTURE verifyingKey(b64): {}",
verifying_key_b64(&signing_key)
);
eprintln!(
"FIXTURE grantorSignature(b64): {}",
signed.grantor_signature.bytes
);
}
fn doc_with_authority(authority: AuthorityBundle) -> ExecutionCellDocument {
ExecutionCellDocument {
api_version: "cellos.io/v1".into(),
kind: "ExecutionCell".into(),
spec: ExecutionCellSpec {
id: "dns-cdn-test-cell".into(),
correlation: None,
ingress: None,
environment: None,
placement: None,
policy: None,
identity: None,
run: None,
authority,
lifetime: Lifetime { ttl_seconds: 60 },
export: None,
telemetry: None,
},
}
}
fn full_dns_authority() -> crate::DnsAuthority {
crate::DnsAuthority {
resolvers: vec![crate::DnsResolver {
resolver_id: "internal-doh".into(),
endpoint: "https://1.1.1.1/dns-query".into(),
protocol: crate::DnsResolverProtocol::Doh,
trust_kid: Some("resolver-kid-2026q2".into()),
dnssec: None,
}],
allowed_query_types: vec![crate::DnsQueryType::A, crate::DnsQueryType::AAAA],
hostname_allowlist: vec!["api.example.com".into(), "*.cdn.example.com".into()],
refresh_policy: Some(crate::DnsRefreshPolicy {
min_ttl_seconds: Some(30),
max_stale_seconds: Some(300),
strategy: Some(crate::DnsRefreshStrategy::TtlHonor),
}),
rebinding_policy: None,
block_direct_workload_dns: true,
block_udp_doq: false,
block_udp_http3: false,
}
}
#[test]
fn t13_accepts_full_dns_and_cdn_authority() {
let authority = AuthorityBundle {
filesystem: None,
network: None,
egress_rules: None,
secret_refs: None,
authority_derivation: None,
dns_authority: Some(full_dns_authority()),
cdn_authority: Some(crate::CdnAuthority {
providers: vec![crate::CdnProvider {
provider_id: "cloudfront".into(),
hostname_pattern: "*.cdn.example.com".into(),
accept_fronting: false,
}],
}),
};
let doc = doc_with_authority(authority);
validate_execution_cell_document(&doc).expect("full T13 authority should validate");
}
#[test]
fn t13_rejects_dns_hostname_allowlist_with_ip_literal() {
let mut dns = full_dns_authority();
dns.hostname_allowlist = vec!["10.0.0.1".into()];
let authority = AuthorityBundle {
dns_authority: Some(dns),
..AuthorityBundle::default()
};
let err = validate_execution_cell_document(&doc_with_authority(authority)).unwrap_err();
assert!(
err.to_string()
.contains("authority.dnsAuthority.hostnameAllowlist"),
"{err}"
);
}
#[test]
fn t13_rejects_dns_hostname_allowlist_with_internal_wildcard() {
let mut dns = full_dns_authority();
dns.hostname_allowlist = vec!["api.*.example.com".into()];
let authority = AuthorityBundle {
dns_authority: Some(dns),
..AuthorityBundle::default()
};
let err = validate_execution_cell_document(&doc_with_authority(authority)).unwrap_err();
assert!(
err.to_string()
.contains("authority.dnsAuthority.hostnameAllowlist"),
"{err}"
);
}
#[test]
fn t13_rejects_dns_resolver_with_invalid_resolver_id() {
let mut dns = full_dns_authority();
dns.resolvers[0].resolver_id = "../escape".into();
let authority = AuthorityBundle {
dns_authority: Some(dns),
..AuthorityBundle::default()
};
let err = validate_execution_cell_document(&doc_with_authority(authority)).unwrap_err();
assert!(
err.to_string()
.contains("authority.dnsAuthority.resolvers[].resolverId"),
"{err}"
);
}
#[test]
fn t13_rejects_duplicate_dns_resolver_ids() {
let mut dns = full_dns_authority();
dns.resolvers.push(crate::DnsResolver {
resolver_id: "internal-doh".into(),
endpoint: "https://1.0.0.1/dns-query".into(),
protocol: crate::DnsResolverProtocol::Doh,
trust_kid: None,
dnssec: None,
});
let authority = AuthorityBundle {
dns_authority: Some(dns),
..AuthorityBundle::default()
};
let err = validate_execution_cell_document(&doc_with_authority(authority)).unwrap_err();
assert!(
err.to_string().contains("duplicates value"),
"expected duplicate resolverId rejection, got: {err}"
);
}
#[test]
fn t13_rejects_refresh_policy_min_ttl_greater_than_max_stale() {
let mut dns = full_dns_authority();
dns.refresh_policy = Some(crate::DnsRefreshPolicy {
min_ttl_seconds: Some(600),
max_stale_seconds: Some(60),
strategy: Some(crate::DnsRefreshStrategy::TtlHonor),
});
let authority = AuthorityBundle {
dns_authority: Some(dns),
..AuthorityBundle::default()
};
let err = validate_execution_cell_document(&doc_with_authority(authority)).unwrap_err();
assert!(
err.to_string().contains("minTtlSeconds")
&& err.to_string().contains("maxStaleSeconds"),
"{err}"
);
}
#[test]
fn t13_rejects_cdn_provider_with_ip_literal_pattern() {
let authority = AuthorityBundle {
cdn_authority: Some(crate::CdnAuthority {
providers: vec![crate::CdnProvider {
provider_id: "fastly".into(),
hostname_pattern: "192.168.1.1".into(),
accept_fronting: false,
}],
}),
..AuthorityBundle::default()
};
let err = validate_execution_cell_document(&doc_with_authority(authority)).unwrap_err();
assert!(
err.to_string()
.contains("authority.cdnAuthority.providers[].hostnamePattern"),
"{err}"
);
}
#[test]
fn t13_rejects_duplicate_cdn_provider_ids() {
let authority = AuthorityBundle {
cdn_authority: Some(crate::CdnAuthority {
providers: vec![
crate::CdnProvider {
provider_id: "cloudfront".into(),
hostname_pattern: "a.cdn.example.com".into(),
accept_fronting: false,
},
crate::CdnProvider {
provider_id: "cloudfront".into(),
hostname_pattern: "b.cdn.example.com".into(),
accept_fronting: true,
},
],
}),
..AuthorityBundle::default()
};
let err = validate_execution_cell_document(&doc_with_authority(authority)).unwrap_err();
assert!(err.to_string().contains("duplicates value"), "{err}");
}
#[test]
fn t13_dns_resolver_protocol_serialises_kebab_case() {
let resolver = crate::DnsResolver {
resolver_id: "p1".into(),
endpoint: "1.1.1.1:53".into(),
protocol: crate::DnsResolverProtocol::Do53Udp,
trust_kid: None,
dnssec: None,
};
let v = serde_json::to_value(&resolver).unwrap();
assert_eq!(v.get("protocol"), Some(&serde_json::json!("do53-udp")));
}
#[test]
fn t13_dns_authority_roundtrips_through_full_document() {
let raw = r#"{
"apiVersion": "cellos.io/v1",
"kind": "ExecutionCell",
"spec": {
"id": "t13-roundtrip",
"authority": {
"dnsAuthority": {
"resolvers": [{
"resolverId": "internal-doh",
"endpoint": "https://1.1.1.1/dns-query",
"protocol": "doh",
"trustKid": "resolver-kid-2026q2"
}],
"allowedQueryTypes": ["A", "AAAA", "HTTPS"],
"hostnameAllowlist": ["api.example.com", "*.cdn.example.com"],
"refreshPolicy": {
"minTtlSeconds": 30,
"maxStaleSeconds": 300,
"strategy": "ttl-honor"
},
"blockDirectWorkloadDns": true
},
"cdnAuthority": {
"providers": [{
"providerId": "cloudfront",
"hostnamePattern": "*.cdn.example.com",
"acceptFronting": false
}]
}
},
"lifetime": { "ttlSeconds": 60 }
}
}"#;
let doc: ExecutionCellDocument =
serde_json::from_str(raw).expect("parse T13 example document");
validate_execution_cell_document(&doc).expect("validation must pass");
let dns = doc.spec.authority.dns_authority.as_ref().unwrap();
assert_eq!(dns.resolvers.len(), 1);
assert!(matches!(
dns.resolvers[0].protocol,
crate::DnsResolverProtocol::Doh
));
assert!(dns.block_direct_workload_dns);
let cdn = doc.spec.authority.cdn_authority.as_ref().unwrap();
assert_eq!(cdn.providers.len(), 1);
assert!(!cdn.providers[0].accept_fronting);
let serialised = serde_json::to_string(&doc).expect("re-serialise");
let doc2: ExecutionCellDocument =
serde_json::from_str(&serialised).expect("re-parse roundtrip");
assert_eq!(
doc2.spec.authority.dns_authority,
doc.spec.authority.dns_authority
);
assert_eq!(
doc2.spec.authority.cdn_authority,
doc.spec.authority.cdn_authority
);
}
#[test]
fn t13_is_fqdn_or_wildcard_unit_cases() {
assert!(super::is_fqdn_or_wildcard("api.example.com"));
assert!(super::is_fqdn_or_wildcard("*.example.com"));
assert!(super::is_fqdn_or_wildcard("a.b.c.d.example.com"));
assert!(!super::is_fqdn_or_wildcard(""));
assert!(!super::is_fqdn_or_wildcard("example")); assert!(!super::is_fqdn_or_wildcard("10.0.0.1")); assert!(!super::is_fqdn_or_wildcard("2001:db8::1")); assert!(!super::is_fqdn_or_wildcard("api.*.example.com")); assert!(!super::is_fqdn_or_wildcard("-bad.example.com")); assert!(!super::is_fqdn_or_wildcard("under_score.example.com")); }
fn telemetry_doc(
events: Vec<String>,
agent_version: &str,
egress: Vec<crate::EgressRule>,
) -> ExecutionCellDocument {
let authority = AuthorityBundle {
egress_rules: if egress.is_empty() {
None
} else {
Some(egress)
},
..AuthorityBundle::default()
};
ExecutionCellDocument {
api_version: "cellos.io/v1".into(),
kind: "ExecutionCell".into(),
spec: ExecutionCellSpec {
id: "telemetry-test-cell".into(),
correlation: None,
ingress: None,
environment: None,
placement: None,
policy: None,
identity: None,
run: None,
authority,
lifetime: Lifetime { ttl_seconds: 60 },
export: None,
telemetry: Some(crate::TelemetrySpec {
channel: crate::TelemetryChannel::VsockCbor,
events,
rate_limits: None,
host_vs_guest_fields: None,
agent_version: agent_version.into(),
}),
},
}
}
#[test]
fn telemetry_rejects_empty_events() {
let doc = telemetry_doc(vec![], "1.0.0", vec![]);
let err = validate_execution_cell_document(&doc).unwrap_err();
assert!(
err.to_string().contains("spec.telemetry.events"),
"got: {err}"
);
}
#[test]
fn telemetry_rejects_bad_semver() {
let doc = telemetry_doc(vec!["process.spawn".into()], "v1", vec![]);
let err = validate_execution_cell_document(&doc).unwrap_err();
assert!(err.to_string().contains("agentVersion"), "got: {err}");
}
#[test]
fn telemetry_rejects_net_event_without_egress() {
let doc = telemetry_doc(vec!["net.connect.attempt".into()], "1.0.0", vec![]);
let err = validate_execution_cell_document(&doc).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("telemetry_without_egress"), "got: {msg}");
}
#[test]
fn telemetry_accepts_net_event_when_egress_declared() {
let doc = telemetry_doc(
vec!["net.connect.attempt".into()],
"1.0.0",
vec![crate::EgressRule {
host: "api.example.com".into(),
port: 443,
protocol: Some("https".into()),
dns_egress_justification: None,
}],
);
validate_execution_cell_document(&doc).expect("valid telemetry+egress spec");
}
#[test]
fn telemetry_accepts_non_net_event_without_egress() {
let doc = telemetry_doc(vec!["process.spawn".into()], "1.0.0", vec![]);
validate_execution_cell_document(&doc).expect("non-net.* event must not require egress");
}
#[test]
fn telemetry_accepts_prerelease_semver() {
let doc = telemetry_doc(vec!["process.spawn".into()], "1.2.3-rc.1", vec![]);
validate_execution_cell_document(&doc).expect("prerelease semver accepted");
}
}
#[cfg(test)]
mod sec25_envelope_tests {
use super::{
sha256_hex_prefixed, verify_signed_trust_keyset_chain, verify_signed_trust_keyset_envelope,
};
use crate::types::{SignedTrustKeysetEnvelope, TrustKeysetSignature};
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine as _;
use ed25519_dalek::{Signer as _, SigningKey, VerifyingKey};
use std::collections::HashMap;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
fn signing_key(seed: u8) -> SigningKey {
SigningKey::from_bytes(&[seed; 32])
}
fn make_envelope(
payload_bytes: &[u8],
signer: &SigningKey,
signer_kid: &str,
not_before: Option<String>,
not_after: Option<String>,
) -> (SignedTrustKeysetEnvelope, HashMap<String, VerifyingKey>) {
let signature = signer.sign(payload_bytes);
let envelope = SignedTrustKeysetEnvelope {
schema_version: "1.0.0".into(),
payload_type: "application/vnd.cellos.trust-keyset-v1+json".into(),
payload: URL_SAFE_NO_PAD.encode(payload_bytes),
signatures: vec![TrustKeysetSignature {
signer_kid: signer_kid.into(),
algorithm: "ed25519".into(),
signature: URL_SAFE_NO_PAD.encode(signature.to_bytes()),
not_before,
not_after,
}],
payload_digest: sha256_hex_prefixed(payload_bytes),
produced_at: "2026-05-01T00:00:00Z".into(),
replaces_envelope_digest: None,
required_signer_count: None,
};
let mut keys = HashMap::new();
keys.insert(signer_kid.to_string(), signer.verifying_key());
(envelope, keys)
}
#[test]
fn verifies_well_formed_envelope_with_synthetic_key() {
let signer = signing_key(7);
let payload = br#"{"schemaVersion":"1.0.0","keysetId":"ks-7","keys":[]}"#;
let (env, keys) = make_envelope(payload, &signer, "kid-active-7", None, None);
let returned = verify_signed_trust_keyset_envelope(&env, &keys, SystemTime::now())
.expect("envelope should verify");
assert_eq!(returned, payload, "verifier must return raw payload bytes");
}
#[test]
fn rejects_payload_digest_mismatch() {
let signer = signing_key(11);
let payload = b"hello-payload";
let (mut env, keys) = make_envelope(payload, &signer, "kid-active-11", None, None);
let last_idx = env.payload_digest.len() - 1;
let last_char = env.payload_digest.chars().last().unwrap();
let new_char = if last_char == '0' { '1' } else { '0' };
env.payload_digest
.replace_range(last_idx.., &new_char.to_string());
let err = verify_signed_trust_keyset_envelope(&env, &keys, SystemTime::now())
.expect_err("digest mismatch must fail");
assert!(
format!("{err}").contains("payload digest mismatch"),
"expected digest-mismatch error, got: {err}"
);
}
#[test]
fn rejects_unknown_signer_kid() {
let signer = signing_key(13);
let payload = b"unknown-kid-payload";
let (env, _keys) = make_envelope(payload, &signer, "kid-active-13", None, None);
let empty_keys: HashMap<String, VerifyingKey> = HashMap::new();
let err = verify_signed_trust_keyset_envelope(&env, &empty_keys, SystemTime::now())
.expect_err("unknown kid must fail");
assert!(
format!("{err}").contains("no signature verified"),
"expected no-signature-verified error, got: {err}"
);
}
#[test]
fn rejects_signature_outside_not_after_window() {
let signer = signing_key(17);
let payload = b"window-payload";
let (env, keys) = make_envelope(
payload,
&signer,
"kid-active-17",
None,
Some("2000-01-01T00:00:00Z".into()),
);
let err = verify_signed_trust_keyset_envelope(&env, &keys, SystemTime::now())
.expect_err("expired window must fail");
assert!(
format!("{err}").contains("no signature verified"),
"expected no-signature-verified error, got: {err}"
);
}
#[test]
fn rejects_tampered_payload() {
let signer = signing_key(19);
let original = b"original-trust-keyset-payload-bytes";
let (mut env, keys) = make_envelope(original, &signer, "kid-active-19", None, None);
let mut tampered = original.to_vec();
tampered[0] ^= 0x01;
env.payload = URL_SAFE_NO_PAD.encode(&tampered);
env.payload_digest = sha256_hex_prefixed(&tampered);
let err = verify_signed_trust_keyset_envelope(&env, &keys, SystemTime::now())
.expect_err("tampered payload must fail signature check");
assert!(
format!("{err}").contains("no signature verified"),
"expected signature failure, got: {err}"
);
}
#[test]
fn accepts_when_at_least_one_signature_verifies() {
let signer_good = signing_key(23);
let signer_other = signing_key(29);
let payload = b"multi-sig-payload";
let bad_sig = signer_other.sign(payload);
let good_sig = signer_good.sign(payload);
let env = SignedTrustKeysetEnvelope {
schema_version: "1.0.0".into(),
payload_type: "application/vnd.cellos.trust-keyset-v1+json".into(),
payload: URL_SAFE_NO_PAD.encode(payload),
signatures: vec![
TrustKeysetSignature {
signer_kid: "kid-unknown-29".into(),
algorithm: "ed25519".into(),
signature: URL_SAFE_NO_PAD.encode(bad_sig.to_bytes()),
not_before: None,
not_after: None,
},
TrustKeysetSignature {
signer_kid: "kid-active-23".into(),
algorithm: "ed25519".into(),
signature: URL_SAFE_NO_PAD.encode(good_sig.to_bytes()),
not_before: None,
not_after: None,
},
],
payload_digest: sha256_hex_prefixed(payload),
produced_at: "2026-05-01T00:00:00Z".into(),
replaces_envelope_digest: None,
required_signer_count: None,
};
let mut keys = HashMap::new();
keys.insert("kid-active-23".to_string(), signer_good.verifying_key());
let returned = verify_signed_trust_keyset_envelope(
&env,
&keys,
UNIX_EPOCH + Duration::from_secs(1_800_000_000),
)
.expect("at least one signature should verify");
assert_eq!(returned, payload);
}
fn make_multisig_envelope(
payload_bytes: &[u8],
signers: &[(&str, &SigningKey)],
required_signer_count: Option<u32>,
) -> (SignedTrustKeysetEnvelope, HashMap<String, VerifyingKey>) {
let signatures = signers
.iter()
.map(|(kid, signer)| {
let sig = signer.sign(payload_bytes);
TrustKeysetSignature {
signer_kid: (*kid).to_string(),
algorithm: "ed25519".into(),
signature: URL_SAFE_NO_PAD.encode(sig.to_bytes()),
not_before: None,
not_after: None,
}
})
.collect();
let envelope = SignedTrustKeysetEnvelope {
schema_version: "1.0.0".into(),
payload_type: "application/vnd.cellos.trust-keyset-v1+json".into(),
payload: URL_SAFE_NO_PAD.encode(payload_bytes),
signatures,
payload_digest: sha256_hex_prefixed(payload_bytes),
produced_at: "2026-05-01T00:00:00Z".into(),
replaces_envelope_digest: None,
required_signer_count,
};
let mut keys = HashMap::new();
for (kid, signer) in signers {
keys.insert((*kid).to_string(), signer.verifying_key());
}
(envelope, keys)
}
#[test]
fn verifies_with_two_distinct_signers_when_threshold_2() {
let signer_a = signing_key(41);
let signer_b = signing_key(43);
let payload = b"phase3-multisig-payload-2of2";
let (env, keys) = make_multisig_envelope(
payload,
&[("kid-ops-a", &signer_a), ("kid-ops-b", &signer_b)],
Some(2),
);
let returned = verify_signed_trust_keyset_envelope(&env, &keys, SystemTime::now())
.expect("two distinct signers + threshold 2 should verify");
assert_eq!(returned, payload);
}
#[test]
fn rejects_when_threshold_2_but_only_one_verifies() {
let signer_a = signing_key(47);
let signer_b = signing_key(53);
let payload = b"phase3-only-one-verifies";
let (env, _full_keys) = make_multisig_envelope(
payload,
&[("kid-ops-a", &signer_a), ("kid-ops-b", &signer_b)],
Some(2),
);
let mut sparse_keys: HashMap<String, VerifyingKey> = HashMap::new();
sparse_keys.insert("kid-ops-a".into(), signer_a.verifying_key());
let err = verify_signed_trust_keyset_envelope(&env, &sparse_keys, SystemTime::now())
.expect_err("threshold 2 with one verifier present must fail");
let msg = format!("{err}");
assert!(
msg.contains("only 1 distinct signers verified, need 2"),
"expected threshold-shortfall error, got: {msg}"
);
}
#[test]
fn counts_distinct_kids_only_duplicate_kid_does_not_inflate() {
let signer = signing_key(59);
let payload = b"phase3-duplicate-kid-payload";
let sig_a = signer.sign(payload);
let sig_b = signer.sign(payload);
let env = SignedTrustKeysetEnvelope {
schema_version: "1.0.0".into(),
payload_type: "application/vnd.cellos.trust-keyset-v1+json".into(),
payload: URL_SAFE_NO_PAD.encode(payload),
signatures: vec![
TrustKeysetSignature {
signer_kid: "kid-ops-only".into(),
algorithm: "ed25519".into(),
signature: URL_SAFE_NO_PAD.encode(sig_a.to_bytes()),
not_before: None,
not_after: None,
},
TrustKeysetSignature {
signer_kid: "kid-ops-only".into(),
algorithm: "ed25519".into(),
signature: URL_SAFE_NO_PAD.encode(sig_b.to_bytes()),
not_before: None,
not_after: None,
},
],
payload_digest: sha256_hex_prefixed(payload),
produced_at: "2026-05-01T00:00:00Z".into(),
replaces_envelope_digest: None,
required_signer_count: Some(2),
};
let mut keys = HashMap::new();
keys.insert("kid-ops-only".into(), signer.verifying_key());
let err = verify_signed_trust_keyset_envelope(&env, &keys, SystemTime::now())
.expect_err("duplicate kid must not satisfy threshold 2");
let msg = format!("{err}");
assert!(
msg.contains("only 1 distinct signers verified, need 2"),
"expected duplicate-kid threshold failure, got: {msg}"
);
}
#[test]
fn default_threshold_1_preserves_phase1_behavior() {
let signer = signing_key(61);
let payload = b"phase1-default-threshold";
for explicit in [None, Some(1)] {
let (env, keys) =
make_multisig_envelope(payload, &[("kid-default-61", &signer)], explicit);
let returned = verify_signed_trust_keyset_envelope(&env, &keys, SystemTime::now())
.expect("default / explicit-1 threshold should verify");
assert_eq!(
returned, payload,
"raw payload bytes returned (explicit={explicit:?})"
);
}
}
#[test]
fn rejects_when_required_zero_treated_as_one() {
let signer = signing_key(67);
let payload = b"phase3-zero-clamp-payload";
let (env, _keys) = make_multisig_envelope(
payload,
&[("kid-active-67", &signer)],
Some(0), );
let empty: HashMap<String, VerifyingKey> = HashMap::new();
let err = verify_signed_trust_keyset_envelope(&env, &empty, SystemTime::now())
.expect_err("zero threshold must clamp to 1");
let msg = format!("{err}");
assert!(
msg.contains("no signature verified"),
"expected legacy single-signer error after clamping to 1, got: {msg}"
);
}
fn make_chain_envelope(
payload_bytes: &[u8],
signer: &SigningKey,
signer_kid: &str,
replaces: Option<String>,
) -> SignedTrustKeysetEnvelope {
let signature = signer.sign(payload_bytes);
SignedTrustKeysetEnvelope {
schema_version: "1.0.0".into(),
payload_type: "application/vnd.cellos.trust-keyset-v1+json".into(),
payload: URL_SAFE_NO_PAD.encode(payload_bytes),
signatures: vec![TrustKeysetSignature {
signer_kid: signer_kid.into(),
algorithm: "ed25519".into(),
signature: URL_SAFE_NO_PAD.encode(signature.to_bytes()),
not_before: None,
not_after: None,
}],
payload_digest: sha256_hex_prefixed(payload_bytes),
produced_at: "2026-05-01T00:00:00Z".into(),
replaces_envelope_digest: replaces,
required_signer_count: None,
}
}
#[test]
fn chain_with_correct_replaces_envelope_digest_succeeds() {
let signer = signing_key(71);
let payload_old = b"phase3-chain-old-payload";
let payload_new = b"phase3-chain-new-payload";
let prev = make_chain_envelope(payload_old, &signer, "kid-active-71", None);
let next = make_chain_envelope(
payload_new,
&signer,
"kid-active-71",
Some(sha256_hex_prefixed(payload_old)),
);
let mut keys = HashMap::new();
keys.insert("kid-active-71".into(), signer.verifying_key());
let head_payload =
verify_signed_trust_keyset_chain(&[prev, next], &keys, SystemTime::now())
.expect("correctly-chained envelopes should verify");
assert_eq!(head_payload, payload_new);
}
#[test]
fn chain_with_mismatched_replaces_envelope_digest_rejected() {
let signer = signing_key(73);
let payload_old = b"phase3-chain-mismatch-old";
let payload_new = b"phase3-chain-mismatch-new";
let prev = make_chain_envelope(payload_old, &signer, "kid-active-73", None);
let bogus_digest = sha256_hex_prefixed(b"this-is-not-the-prior-payload");
let next = make_chain_envelope(payload_new, &signer, "kid-active-73", Some(bogus_digest));
let mut keys = HashMap::new();
keys.insert("kid-active-73".into(), signer.verifying_key());
let err = verify_signed_trust_keyset_chain(&[prev, next], &keys, SystemTime::now())
.expect_err("mismatched replacesEnvelopeDigest must fail");
let msg = format!("{err}");
assert!(
msg.contains("replacesEnvelopeDigest mismatch"),
"expected chain link mismatch error, got: {msg}"
);
assert!(
msg.contains("index 1"),
"expected index 1 (the bad link) in error, got: {msg}"
);
}
#[test]
fn chain_first_envelope_without_replaces_accepted_as_genesis() {
let signer = signing_key(79);
let payload = b"phase3-chain-genesis-only";
let genesis = make_chain_envelope(payload, &signer, "kid-active-79", None);
let mut keys = HashMap::new();
keys.insert("kid-active-79".into(), signer.verifying_key());
let head_payload = verify_signed_trust_keyset_chain(&[genesis], &keys, SystemTime::now())
.expect("genesis-only chain should verify");
assert_eq!(head_payload, payload);
}
#[test]
fn chain_returns_head_payload_bytes() {
let signer = signing_key(83);
let p0 = b"phase3-head-bytes-genesis";
let p1 = b"phase3-head-bytes-middle";
let p2 = b"phase3-head-bytes-HEAD-distinct";
let env0 = make_chain_envelope(p0, &signer, "kid-active-83", None);
let env1 = make_chain_envelope(p1, &signer, "kid-active-83", Some(sha256_hex_prefixed(p0)));
let env2 = make_chain_envelope(p2, &signer, "kid-active-83", Some(sha256_hex_prefixed(p1)));
let mut keys = HashMap::new();
keys.insert("kid-active-83".into(), signer.verifying_key());
let head_payload =
verify_signed_trust_keyset_chain(&[env0, env1, env2], &keys, SystemTime::now())
.expect("3-envelope chain should verify");
assert_eq!(
head_payload, p2,
"verifier must return HEAD payload, not earlier links"
);
}
#[test]
fn chain_empty_rejected() {
let keys: HashMap<String, VerifyingKey> = HashMap::new();
let err = verify_signed_trust_keyset_chain(&[], &keys, SystemTime::now())
.expect_err("empty chain must be rejected");
let msg = format!("{err}");
assert!(
msg.contains("empty chain"),
"expected empty-chain error, got: {msg}"
);
}
#[test]
fn chain_propagates_threshold_failure_per_envelope() {
let signer_a = signing_key(89);
let signer_b = signing_key(97);
let p0 = b"phase3-threshold-prop-genesis";
let p1 = b"phase3-threshold-prop-head";
let env0 = make_chain_envelope(p0, &signer_a, "kid-ops-a", None);
let mut env1 =
make_chain_envelope(p1, &signer_a, "kid-ops-a", Some(sha256_hex_prefixed(p0)));
env1.required_signer_count = Some(2);
let mut keys = HashMap::new();
keys.insert("kid-ops-a".into(), signer_a.verifying_key());
keys.insert("kid-ops-b".into(), signer_b.verifying_key());
let err = verify_signed_trust_keyset_chain(&[env0, env1], &keys, SystemTime::now())
.expect_err("env1 threshold-2 with one signer must fail");
let msg = format!("{err}");
assert!(
msg.contains("envelope at index 1 failed verification"),
"expected indexed envelope failure, got: {msg}"
);
assert!(
msg.contains("only 1 distinct signers verified, need 2"),
"expected per-envelope threshold message in chain error, got: {msg}"
);
}
}