use assay_canonical::{jcs, parse_strict};
use assay_common::dsse::build_pae;
use base64::engine::general_purpose::STANDARD as BASE64;
use base64::Engine as _;
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use serde::{Deserialize, Serialize};
use crate::aee_seal::SealPayload;
pub const PAYLOAD_TYPE: &str = "application/vnd.assay.aee-landlock-seal.v1+json";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeyRole {
SubstrateObservation,
PolicyDecision,
}
#[derive(Debug, Clone)]
pub struct TrustedObservationKey {
pub keyid: String,
pub role: KeyRole,
pub verifying_key: VerifyingKey,
pub collection_paths: Vec<String>,
pub substrate: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SealEnvelope {
pub payload: String,
#[serde(rename = "payloadType")]
pub payload_type: String,
pub signatures: Vec<EnvelopeSignature>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EnvelopeSignature {
pub keyid: String,
pub sig: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SealVerifyError {
UnsupportedPayloadType(String),
SignatureCount(usize),
PayloadNotBase64,
PayloadNotStrictJson(String),
SignatureNotBase64,
SignatureMalformed,
SignatureInvalid,
KeyNotTrusted {
keyid: String,
},
WrongKeyRole,
CollectionPathOutOfScope {
path: String,
},
SubstrateOutOfScope {
substrate: String,
},
PayloadNotASeal(String),
}
impl SealVerifyError {
pub fn code(&self) -> &'static str {
match self {
Self::UnsupportedPayloadType(_) => "seal-envelope-unsupported-payload-type",
Self::SignatureCount(_) => "seal-envelope-signature-count",
Self::PayloadNotBase64 => "seal-envelope-payload-not-base64",
Self::PayloadNotStrictJson(_) => "seal-envelope-payload-not-strict-json",
Self::SignatureNotBase64 => "seal-envelope-signature-not-base64",
Self::SignatureMalformed => "seal-envelope-signature-malformed",
Self::SignatureInvalid => "seal-envelope-signature-invalid",
Self::KeyNotTrusted { .. } => "seal-envelope-key-not-trusted",
Self::WrongKeyRole => "seal-envelope-wrong-key-role",
Self::CollectionPathOutOfScope { .. } => "seal-envelope-collection-path-out-of-scope",
Self::SubstrateOutOfScope { .. } => "seal-envelope-substrate-out-of-scope",
Self::PayloadNotASeal(_) => "seal-envelope-payload-not-a-seal",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SealSignError {
NotCanonicalizable(String),
WrongKeyRole,
}
impl std::fmt::Display for SealSignError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotCanonicalizable(detail) => {
write!(f, "seal payload does not canonicalize: {detail}")
}
Self::WrongKeyRole => write!(
f,
"the signing key is not a substrate observation key; ADR-045 forbids a policy-decision key from signing a substrate observation"
),
}
}
}
impl std::error::Error for SealSignError {}
fn canonical_payload_bytes(payload: &SealPayload) -> Result<Vec<u8>, SealSignError> {
jcs::to_vec(payload).map_err(|e| SealSignError::NotCanonicalizable(e.to_string()))
}
pub fn sign_seal(
payload: &SealPayload,
signing_key: &SigningKey,
keyid: &str,
role: KeyRole,
) -> Result<SealEnvelope, SealSignError> {
if role != KeyRole::SubstrateObservation {
return Err(SealSignError::WrongKeyRole);
}
let bytes = canonical_payload_bytes(payload)?;
let signature = signing_key.sign(&build_pae(PAYLOAD_TYPE, &bytes));
Ok(SealEnvelope {
payload: BASE64.encode(&bytes),
payload_type: PAYLOAD_TYPE.to_string(),
signatures: vec![EnvelopeSignature {
keyid: keyid.to_string(),
sig: BASE64.encode(signature.to_bytes()),
}],
})
}
pub fn verify_seal(
envelope: &SealEnvelope,
trusted: &TrustedObservationKey,
) -> Result<SealPayload, SealVerifyError> {
if envelope.payload_type != PAYLOAD_TYPE {
return Err(SealVerifyError::UnsupportedPayloadType(
envelope.payload_type.clone(),
));
}
let entry = match envelope.signatures.as_slice() {
[only] => only,
other => return Err(SealVerifyError::SignatureCount(other.len())),
};
let bytes = BASE64
.decode(envelope.payload.as_bytes())
.map_err(|_| SealVerifyError::PayloadNotBase64)?;
let value = parse_strict(
std::str::from_utf8(&bytes)
.map_err(|e| SealVerifyError::PayloadNotStrictJson(e.to_string()))?,
)
.map_err(|e| SealVerifyError::PayloadNotStrictJson(e.to_string()))?;
if entry.keyid != trusted.keyid {
return Err(SealVerifyError::KeyNotTrusted {
keyid: entry.keyid.clone(),
});
}
if trusted.role != KeyRole::SubstrateObservation {
return Err(SealVerifyError::WrongKeyRole);
}
let sig_bytes = BASE64
.decode(entry.sig.as_bytes())
.map_err(|_| SealVerifyError::SignatureNotBase64)?;
let sig_array: [u8; 64] = sig_bytes
.as_slice()
.try_into()
.map_err(|_| SealVerifyError::SignatureMalformed)?;
let signature = Signature::from_bytes(&sig_array);
trusted
.verifying_key
.verify(&build_pae(PAYLOAD_TYPE, &bytes), &signature)
.map_err(|_| SealVerifyError::SignatureInvalid)?;
let path = value
.get("assayCollectionPath")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string();
if !trusted.collection_paths.contains(&path) {
return Err(SealVerifyError::CollectionPathOutOfScope { path });
}
let payload: SealPayload = serde_json::from_value(value)
.map_err(|e| SealVerifyError::PayloadNotASeal(e.to_string()))?;
Ok(payload)
}
pub fn check_substrate_scope(
trusted: &TrustedObservationKey,
statement_substrate: &str,
) -> Result<(), SealVerifyError> {
if trusted.substrate != statement_substrate {
return Err(SealVerifyError::SubstrateOutOfScope {
substrate: statement_substrate.to_string(),
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::aee_seal::{build_sealed_run, DropAccounting, ObservationEnvironment, Vantage};
use crate::enforcement_health_v1::{EnforcementHealthV1, Probe};
const PARITY: &str = include_str!(
"../../../scripts/experiments/fixtures/aee-landlock-seal/derivation-parity.json"
);
fn env_from_parity() -> ObservationEnvironment {
let p: serde_json::Value = serde_json::from_str(PARITY).expect("parity vectors parse");
let e = &p["environment"];
let g = |k: &str| e[k].as_str().expect("digest").to_string();
ObservationEnvironment {
subject_digest: g("subject"),
substrate_digest: g("substrate"),
corpus_digest: g("corpus"),
catch_policy_digest: g("catchPolicy"),
observation_vocabulary_digest: g("observationVocabulary"),
run_entropy_digest: g("runEntropy"),
network_posture: e["networkPosture"].clone(),
}
}
fn sealed_payload() -> SealPayload {
let health = EnforcementHealthV1::landlock_active(
4,
vec![443],
Some(Probe {
kind: "real_block".into(),
transport: "ipv4".into(),
blocked_action: "tcp_connect".into(),
blocked_port: 4444,
blocked_errno: "EACCES".into(),
listener_reached: false,
}),
Some("restrictions_held".to_string()),
);
build_sealed_run(
Vantage::Landlock(&health),
&env_from_parity(),
&[],
"2026-08-05T00:00:00Z",
&DropAccounting::SynchronousProbe,
crate::aee_seal::COLLECTION_PATH_LANDLOCK_TCP_CONNECT,
)
.expect("seal-eligible")
.seal
}
fn key() -> SigningKey {
SigningKey::from_bytes(&[7u8; 32])
}
fn trusted(k: &SigningKey) -> TrustedObservationKey {
TrustedObservationKey {
keyid: "observer-1".into(),
role: KeyRole::SubstrateObservation,
verifying_key: k.verifying_key(),
collection_paths: vec![crate::aee_seal::COLLECTION_PATH_LANDLOCK_TCP_CONNECT.into()],
substrate: "assay-landlock-substrate".into(),
}
}
fn signed() -> (SealEnvelope, SigningKey) {
let k = key();
let env = sign_seal(
&sealed_payload(),
&k,
"observer-1",
KeyRole::SubstrateObservation,
)
.expect("signs");
(env, k)
}
#[test]
fn a_signed_seal_round_trips() {
let (envelope, k) = signed();
let payload = verify_seal(&envelope, &trusted(&k)).expect("verifies");
assert_eq!(payload, sealed_payload());
}
#[test]
fn the_same_payload_encoded_differently_does_not_verify() {
let (mut envelope, k) = signed();
let bytes = BASE64.decode(envelope.payload.as_bytes()).unwrap();
let value: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
let pretty = serde_json::to_vec_pretty(&value).unwrap();
assert_ne!(pretty, bytes, "the re-encoding must actually differ");
assert_eq!(
serde_json::from_slice::<serde_json::Value>(&pretty).unwrap(),
value,
"and must still mean the same thing"
);
envelope.payload = BASE64.encode(&pretty);
let err = verify_seal(&envelope, &trusted(&k)).expect_err("must refuse");
assert_eq!(err.code(), "seal-envelope-signature-invalid");
}
#[test]
fn a_tampered_payload_does_not_verify() {
let (mut envelope, k) = signed();
let bytes = BASE64.decode(envelope.payload.as_bytes()).unwrap();
let mut value: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
value["aeeStillArmed"] = serde_json::json!(false);
envelope.payload = BASE64.encode(jcs::to_vec(&value).unwrap());
let err = verify_seal(&envelope, &trusted(&k)).expect_err("must refuse");
assert_eq!(err.code(), "seal-envelope-signature-invalid");
}
#[test]
fn a_duplicate_member_is_refused() {
let (mut envelope, k) = signed();
let bytes = BASE64.decode(envelope.payload.as_bytes()).unwrap();
let text = String::from_utf8(bytes).unwrap();
let doubled = text.replacen('{', r#"{"aeeStillArmed":false,"#, 1);
envelope.payload = BASE64.encode(doubled.as_bytes());
let err = verify_seal(&envelope, &trusted(&k)).expect_err("must refuse");
assert_eq!(err.code(), "seal-envelope-payload-not-strict-json");
}
#[test]
fn a_fixture_envelope_is_refused_by_type() {
let (mut envelope, k) = signed();
envelope.payload_type = "application/vnd.assay.aee-landlock-seal.fixture.v0+json".into();
let err = verify_seal(&envelope, &trusted(&k)).expect_err("must refuse");
assert_eq!(err.code(), "seal-envelope-unsupported-payload-type");
}
#[test]
fn a_policy_decision_key_may_not_sign() {
let err = sign_seal(
&sealed_payload(),
&key(),
"policy-1",
KeyRole::PolicyDecision,
)
.expect_err("must refuse");
assert_eq!(err, SealSignError::WrongKeyRole);
}
#[test]
fn a_policy_decision_key_may_not_be_credited() {
let (envelope, k) = signed();
let mut t = trusted(&k);
t.role = KeyRole::PolicyDecision;
let err = verify_seal(&envelope, &t).expect_err("must refuse");
assert_eq!(err.code(), "seal-envelope-wrong-key-role");
}
#[test]
fn an_untrusted_key_is_not_credited() {
let (envelope, k) = signed();
let mut t = trusted(&k);
t.keyid = "someone-else".into();
let err = verify_seal(&envelope, &t).expect_err("must refuse");
assert_eq!(err.code(), "seal-envelope-key-not-trusted");
}
#[test]
fn a_key_outside_its_collection_path_is_not_credited() {
let (envelope, k) = signed();
let mut t = trusted(&k);
t.collection_paths = vec!["landlock-udp-send".into()];
let err = verify_seal(&envelope, &t).expect_err("must refuse");
assert_eq!(err.code(), "seal-envelope-collection-path-out-of-scope");
}
#[test]
fn a_key_outside_its_substrate_is_not_credited() {
let k = key();
let err =
check_substrate_scope(&trusted(&k), "some-other-substrate").expect_err("must refuse");
assert_eq!(err.code(), "seal-envelope-substrate-out-of-scope");
check_substrate_scope(&trusted(&k), "assay-landlock-substrate").expect("in scope");
}
#[test]
fn an_envelope_without_exactly_one_signature_is_refused() {
let (mut envelope, k) = signed();
let only = envelope.signatures[0].clone();
envelope.signatures = vec![only.clone(), only];
assert_eq!(
verify_seal(&envelope, &trusted(&k))
.expect_err("must refuse")
.code(),
"seal-envelope-signature-count"
);
envelope.signatures.clear();
assert_eq!(
verify_seal(&envelope, &trusted(&k))
.expect_err("must refuse")
.code(),
"seal-envelope-signature-count"
);
}
#[test]
fn the_signed_bytes_are_rfc8785_canonical() {
let payload = sealed_payload();
let bytes = canonical_payload_bytes(&payload).expect("canonicalizes");
assert_eq!(bytes, jcs::to_vec(&payload).unwrap());
let (envelope, _) = signed();
assert_eq!(BASE64.decode(envelope.payload.as_bytes()).unwrap(), bytes);
}
#[test]
fn no_refusal_returns_a_payload() {
let (envelope, k) = signed();
let mut wrong_key = trusted(&k);
wrong_key.verifying_key = SigningKey::from_bytes(&[9u8; 32]).verifying_key();
assert!(verify_seal(&envelope, &wrong_key).is_err());
let mut wrong_type = envelope.clone();
wrong_type.payload_type = "text/plain".into();
assert!(verify_seal(&wrong_type, &trusted(&k)).is_err());
let mut bad_b64 = envelope.clone();
bad_b64.payload = "!!!".into();
assert!(verify_seal(&bad_b64, &trusted(&k)).is_err());
}
}