use std::path::Path;
use ed25519_dalek::pkcs8::DecodePrivateKey;
use ed25519_dalek::SigningKey;
use crate::aee_seal_envelope::KeyRole;
pub const SEAL_KEY_SCHEMA: &str = "assay.aee_seal_key.v0";
const ROLE_SUBSTRATE_OBSERVATION: &str = "substrate-observation";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SealKeyError {
NotStrictJson(String),
NotAnObject,
UnknownSchema { found: String },
MissingMember { member: &'static str },
UnknownMember { member: String },
NotAnObservationRole { found: String },
KeyUnreadable { detail: String },
KeyNotEd25519Pkcs8 { detail: String },
KeyIdNotDerivable { detail: String },
}
impl std::fmt::Display for SealKeyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotStrictJson(d) => write!(f, "seal key descriptor is not strict JSON: {d}"),
Self::NotAnObject => write!(f, "seal key descriptor is not a JSON object"),
Self::UnknownSchema { found } => write!(
f,
"seal key descriptor declares schema {found:?}, this build implements {SEAL_KEY_SCHEMA:?}"
),
Self::MissingMember { member } => {
write!(f, "seal key descriptor has no {member:?}")
}
Self::UnknownMember { member } => write!(
f,
"seal key descriptor carries an unknown member {member:?}; the key id is derived from the key material and cannot be declared"
),
Self::NotAnObservationRole { found } => write!(
f,
"seal key descriptor declares role {found:?}; only {ROLE_SUBSTRATE_OBSERVATION:?} may sign a substrate observation"
),
Self::KeyUnreadable { detail } => write!(f, "seal key file unreadable: {detail}"),
Self::KeyNotEd25519Pkcs8 { detail } => {
write!(f, "seal key is not a PKCS#8 Ed25519 private key: {detail}")
}
Self::KeyIdNotDerivable { detail } => {
write!(f, "seal key id could not be derived: {detail}")
}
}
}
}
impl std::error::Error for SealKeyError {}
pub struct SealSigningKey {
signing_key: SigningKey,
keyid: String,
}
impl std::fmt::Debug for SealSigningKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SealSigningKey")
.field("keyid", &self.keyid)
.field("signing_key", &"<redacted>")
.finish()
}
}
impl SealSigningKey {
pub fn keyid(&self) -> &str {
&self.keyid
}
pub fn signing_key(&self) -> &SigningKey {
&self.signing_key
}
pub fn role(&self) -> KeyRole {
KeyRole::SubstrateObservation
}
}
pub fn load(descriptor_path: &Path, raw: &str) -> Result<SealSigningKey, SealKeyError> {
let value = assay_canonical::parse_strict(raw)
.map_err(|e| SealKeyError::NotStrictJson(e.to_string()))?;
let object = value.as_object().ok_or(SealKeyError::NotAnObject)?;
match object.get("schema").and_then(|s| s.as_str()) {
Some(SEAL_KEY_SCHEMA) => {}
other => {
return Err(SealKeyError::UnknownSchema {
found: other.unwrap_or("<absent>").to_string(),
})
}
}
const KNOWN: [&str; 3] = ["schema", "role", "private_key_path"];
for key in object.keys() {
if !KNOWN.contains(&key.as_str()) {
return Err(SealKeyError::UnknownMember {
member: key.clone(),
});
}
}
let role = object
.get("role")
.and_then(|r| r.as_str())
.ok_or(SealKeyError::MissingMember { member: "role" })?;
if role != ROLE_SUBSTRATE_OBSERVATION {
return Err(SealKeyError::NotAnObservationRole {
found: role.to_string(),
});
}
let rel = object
.get("private_key_path")
.and_then(|p| p.as_str())
.ok_or(SealKeyError::MissingMember {
member: "private_key_path",
})?;
let key_path = descriptor_path
.parent()
.unwrap_or_else(|| Path::new("."))
.join(rel);
let pem = std::fs::read_to_string(&key_path).map_err(|e| SealKeyError::KeyUnreadable {
detail: format!("{}: {e}", key_path.display()),
})?;
let signing_key =
SigningKey::from_pkcs8_pem(&pem).map_err(|e| SealKeyError::KeyNotEd25519Pkcs8 {
detail: e.to_string(),
})?;
let keyid = assay_evidence::mandate::signing::compute_key_id_from_verifying_key(
&signing_key.verifying_key(),
)
.map_err(|e| SealKeyError::KeyIdNotDerivable {
detail: e.to_string(),
})?;
Ok(SealSigningKey { signing_key, keyid })
}
#[cfg(test)]
mod tests {
use super::*;
use ed25519_dalek::pkcs8::EncodePrivateKey;
fn write_key(dir: &Path, name: &str, seed: [u8; 32]) -> SigningKey {
let key = SigningKey::from_bytes(&seed);
let pem = key
.to_pkcs8_pem(ed25519_dalek::pkcs8::spki::der::pem::LineEnding::LF)
.expect("encode");
std::fs::write(dir.join(name), pem.as_bytes()).expect("write");
key
}
fn descriptor(role: &str, key_name: &str) -> String {
serde_json::json!({
"schema": SEAL_KEY_SCHEMA,
"role": role,
"private_key_path": key_name,
})
.to_string()
}
fn load_in(dir: &Path, raw: &str) -> Result<SealSigningKey, SealKeyError> {
load(&dir.join("key.json"), raw)
}
#[test]
fn a_substrate_observation_key_loads_and_derives_its_own_id() {
let dir = tempfile::tempdir().expect("tmp");
let key = write_key(dir.path(), "k.pem", [3u8; 32]);
let loaded =
load_in(dir.path(), &descriptor(ROLE_SUBSTRATE_OBSERVATION, "k.pem")).expect("load");
assert_eq!(loaded.role(), KeyRole::SubstrateObservation);
assert_eq!(
loaded.signing_key().verifying_key(),
key.verifying_key(),
"loaded a different key than was written"
);
let expected = assay_evidence::mandate::signing::compute_key_id_from_verifying_key(
&key.verifying_key(),
)
.expect("derive");
assert_eq!(loaded.keyid(), expected);
assert!(loaded.keyid().starts_with("sha256:"), "{}", loaded.keyid());
}
#[test]
fn different_key_material_yields_different_ids() {
let dir = tempfile::tempdir().expect("tmp");
write_key(dir.path(), "a.pem", [3u8; 32]);
write_key(dir.path(), "b.pem", [4u8; 32]);
let a = load_in(dir.path(), &descriptor(ROLE_SUBSTRATE_OBSERVATION, "a.pem")).expect("a");
let b = load_in(dir.path(), &descriptor(ROLE_SUBSTRATE_OBSERVATION, "b.pem")).expect("b");
assert_ne!(a.keyid(), b.keyid());
}
#[test]
fn a_policy_decision_role_is_refused() {
let dir = tempfile::tempdir().expect("tmp");
write_key(dir.path(), "k.pem", [3u8; 32]);
assert_eq!(
load_in(dir.path(), &descriptor("policy-decision", "k.pem")).unwrap_err(),
SealKeyError::NotAnObservationRole {
found: "policy-decision".to_string()
}
);
}
#[test]
fn a_declared_keyid_is_refused_not_ignored() {
let dir = tempfile::tempdir().expect("tmp");
write_key(dir.path(), "k.pem", [3u8; 32]);
let raw = serde_json::json!({
"schema": SEAL_KEY_SCHEMA,
"role": ROLE_SUBSTRATE_OBSERVATION,
"private_key_path": "k.pem",
"keyid": "assay-aee-spike-fixture-key-v0",
})
.to_string();
assert_eq!(
load_in(dir.path(), &raw).unwrap_err(),
SealKeyError::UnknownMember {
member: "keyid".to_string()
}
);
}
#[test]
fn the_fixture_signing_secret_is_not_loadable_as_a_key() {
let dir = tempfile::tempdir().expect("tmp");
std::fs::write(
dir.path().join("k.pem"),
b"assay-aee-landlock-seal-fixture-key-not-production",
)
.expect("write");
assert!(matches!(
load_in(dir.path(), &descriptor(ROLE_SUBSTRATE_OBSERVATION, "k.pem")),
Err(SealKeyError::KeyNotEd25519Pkcs8 { .. })
));
}
#[test]
fn a_duplicate_key_in_the_descriptor_is_refused() {
let dir = tempfile::tempdir().expect("tmp");
write_key(dir.path(), "k.pem", [3u8; 32]);
let raw = format!(
"{{\"schema\":\"{SEAL_KEY_SCHEMA}\",\"role\":\"{ROLE_SUBSTRATE_OBSERVATION}\",\"private_key_path\":\"a.pem\",\"private_key_path\":\"k.pem\"}}"
);
assert!(serde_json::from_str::<serde_json::Value>(&raw).is_ok());
assert!(matches!(
load_in(dir.path(), &raw),
Err(SealKeyError::NotStrictJson(_))
));
}
#[test]
fn a_missing_member_or_schema_is_refused() {
let dir = tempfile::tempdir().expect("tmp");
write_key(dir.path(), "k.pem", [3u8; 32]);
let no_role =
serde_json::json!({"schema": SEAL_KEY_SCHEMA, "private_key_path": "k.pem"}).to_string();
assert_eq!(
load_in(dir.path(), &no_role).unwrap_err(),
SealKeyError::MissingMember { member: "role" }
);
let no_path =
serde_json::json!({"schema": SEAL_KEY_SCHEMA, "role": ROLE_SUBSTRATE_OBSERVATION})
.to_string();
assert_eq!(
load_in(dir.path(), &no_path).unwrap_err(),
SealKeyError::MissingMember {
member: "private_key_path"
}
);
let wrong_schema = serde_json::json!({
"schema": "assay.aee_seal_key.v1",
"role": ROLE_SUBSTRATE_OBSERVATION,
"private_key_path": "k.pem"
})
.to_string();
assert!(matches!(
load_in(dir.path(), &wrong_schema),
Err(SealKeyError::UnknownSchema { .. })
));
}
#[test]
fn the_key_path_resolves_relative_to_the_descriptor() {
let dir = tempfile::tempdir().expect("tmp");
let nested = dir.path().join("nested");
std::fs::create_dir(&nested).expect("mkdir");
write_key(&nested, "k.pem", [3u8; 32]);
assert!(load(
&nested.join("key.json"),
&descriptor(ROLE_SUBSTRATE_OBSERVATION, "k.pem")
)
.is_ok());
assert!(matches!(
load_in(dir.path(), &descriptor(ROLE_SUBSTRATE_OBSERVATION, "k.pem")),
Err(SealKeyError::KeyUnreadable { .. })
));
}
#[test]
fn debug_does_not_print_the_private_key() {
let dir = tempfile::tempdir().expect("tmp");
write_key(dir.path(), "k.pem", [3u8; 32]);
let loaded =
load_in(dir.path(), &descriptor(ROLE_SUBSTRATE_OBSERVATION, "k.pem")).expect("load");
let rendered = format!("{loaded:?}");
assert!(rendered.contains("<redacted>"), "{rendered}");
assert!(!rendered.contains("030303"), "{rendered}");
}
}