use chrono::{DateTime, Utc};
use ring::signature::{Ed25519KeyPair, KeyPair, UnparsedPublicKey, ED25519};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::{BTreeMap, BTreeSet};
const ARTIFACT_DIGEST_DOMAIN: &[u8] = b"recursiveintell:artifact-envelope:digest:v1\0";
const SIGNATURE_DOMAIN: &[u8] = b"recursiveintell:artifact-envelope:signature:v1\0";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PolicyAdmission {
pub policy_id: String,
pub admitted: bool,
}
impl PolicyAdmission {
pub fn admitted(policy_id: impl Into<String>) -> Self {
Self {
policy_id: policy_id.into(),
admitted: true,
}
}
pub fn rejected(policy_id: impl Into<String>) -> Self {
Self {
policy_id: policy_id.into(),
admitted: false,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ArtifactEnvelopeV1 {
pub artifact_digest: String,
pub signature: Option<Vec<u8>>,
pub signer_id: String,
pub trusted_timestamp: DateTime<Utc>,
pub policy_admission: PolicyAdmission,
pub signer_public_key: Option<[u8; 32]>,
}
impl ArtifactEnvelopeV1 {
pub fn unsigned(
artifact: &[u8],
signer_id: impl Into<String>,
trusted_timestamp: DateTime<Utc>,
policy_admission: PolicyAdmission,
) -> Self {
Self {
artifact_digest: artifact_digest(artifact),
signature: None,
signer_id: signer_id.into(),
trusted_timestamp,
policy_admission,
signer_public_key: None,
}
}
pub fn sign_ed25519(&mut self, secret_key: &[u8; 32]) -> Result<(), EnvelopeError> {
let signing_key = Ed25519KeyPair::from_seed_unchecked(secret_key)
.map_err(|_| EnvelopeError::InvalidSigningKey)?;
let preimage = self.signature_preimage()?;
self.signature = Some(signing_key.sign(&preimage).as_ref().to_vec());
self.signer_public_key = Some(
signing_key
.public_key()
.as_ref()
.try_into()
.map_err(|_| EnvelopeError::InvalidSigningKey)?,
);
Ok(())
}
pub fn signer_public_key(&self) -> Option<[u8; 32]> {
self.signer_public_key
}
pub fn verify(
&self,
artifact: &[u8],
context: &EnvelopeVerificationContext,
) -> EnvelopeVerificationReport {
let digest_valid = self.artifact_digest == artifact_digest(artifact);
if !digest_valid {
return EnvelopeVerificationReport::at(
false,
false,
false,
false,
false,
EnvelopeVerificationStatus::DigestInvalid,
);
}
let signature_present = self.signature.is_some();
let signature_valid = self.verify_signature(context);
if !signature_present {
return EnvelopeVerificationReport::at(
true,
false,
false,
false,
false,
EnvelopeVerificationStatus::DigestValidOnly,
);
}
if !signature_valid {
return EnvelopeVerificationReport::at(
true,
false,
false,
false,
false,
EnvelopeVerificationStatus::SignatureInvalid,
);
}
let signer_authorized = context.authorized_signers.contains(&self.signer_id);
if !signer_authorized {
return EnvelopeVerificationReport::at(
true,
true,
false,
false,
false,
EnvelopeVerificationStatus::SignatureValidSignerUnauthorized,
);
}
let time_valid = self.trusted_timestamp >= context.not_before
&& self.trusted_timestamp <= context.not_after;
if !time_valid {
return EnvelopeVerificationReport::at(
true,
true,
true,
false,
false,
EnvelopeVerificationStatus::SignerAuthorizedTimeInvalid,
);
}
let policy_admitted = self.policy_admission.admitted
&& context
.admitted_policies
.contains(&self.policy_admission.policy_id);
let status = if policy_admitted {
EnvelopeVerificationStatus::FullyVerified
} else {
EnvelopeVerificationStatus::TimeValidPolicyRejected
};
EnvelopeVerificationReport::at(true, true, true, true, policy_admitted, status)
}
fn verify_signature(&self, context: &EnvelopeVerificationContext) -> bool {
let Some(signature_bytes) = self.signature.as_deref() else {
return false;
};
let Some(key_bytes) = context.signer_keys.get(&self.signer_id) else {
return false;
};
let Ok(preimage) = self.signature_preimage() else {
return false;
};
UnparsedPublicKey::new(&ED25519, key_bytes)
.verify(&preimage, signature_bytes)
.is_ok()
}
fn signature_preimage(&self) -> Result<Vec<u8>, EnvelopeError> {
let timestamp = self
.trusted_timestamp
.timestamp_nanos_opt()
.ok_or(EnvelopeError::TimestampOutOfRange)?;
let mut preimage = SIGNATURE_DOMAIN.to_vec();
for field in [
self.artifact_digest.as_bytes(),
self.signer_id.as_bytes(),
×tamp.to_be_bytes(),
self.policy_admission.policy_id.as_bytes(),
&[u8::from(self.policy_admission.admitted)],
] {
preimage.extend_from_slice(&(field.len() as u64).to_be_bytes());
preimage.extend_from_slice(field);
}
Ok(preimage)
}
}
fn artifact_digest(artifact: &[u8]) -> String {
let mut digest = Sha256::new();
digest.update(ARTIFACT_DIGEST_DOMAIN);
digest.update((artifact.len() as u64).to_be_bytes());
digest.update(artifact);
hex::encode(digest.finalize())
}
#[derive(Debug, Clone)]
pub struct EnvelopeVerificationContext {
signer_keys: BTreeMap<String, [u8; 32]>,
authorized_signers: BTreeSet<String>,
admitted_policies: BTreeSet<String>,
not_before: DateTime<Utc>,
not_after: DateTime<Utc>,
}
impl EnvelopeVerificationContext {
pub fn new(not_before: DateTime<Utc>, not_after: DateTime<Utc>) -> Self {
Self {
signer_keys: BTreeMap::new(),
authorized_signers: BTreeSet::new(),
admitted_policies: BTreeSet::new(),
not_before,
not_after,
}
}
pub fn with_signer_key(mut self, signer_id: impl Into<String>, key: [u8; 32]) -> Self {
self.signer_keys.insert(signer_id.into(), key);
self
}
pub fn authorize_signer(mut self, signer_id: impl Into<String>) -> Self {
self.authorized_signers.insert(signer_id.into());
self
}
pub fn admit_policy(mut self, policy_id: impl Into<String>) -> Self {
self.admitted_policies.insert(policy_id.into());
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EnvelopeVerificationStatus {
DigestInvalid,
DigestValidOnly,
SignatureInvalid,
SignatureValidSignerUnauthorized,
SignerAuthorizedTimeInvalid,
TimeValidPolicyRejected,
FullyVerified,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EnvelopeVerificationReport {
pub digest_valid: bool,
pub signature_valid: bool,
pub signer_authorized: bool,
pub time_valid: bool,
pub policy_admitted: bool,
pub status: EnvelopeVerificationStatus,
}
impl EnvelopeVerificationReport {
fn at(
digest_valid: bool,
signature_valid: bool,
signer_authorized: bool,
time_valid: bool,
policy_admitted: bool,
status: EnvelopeVerificationStatus,
) -> Self {
Self {
digest_valid,
signature_valid,
signer_authorized,
time_valid,
policy_admitted,
status,
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum EnvelopeError {
#[error("trusted timestamp is outside the nanosecond range")]
TimestampOutOfRange,
#[error("invalid Ed25519 signing key")]
InvalidSigningKey,
}