use rand::RngCore;
use serde::{Deserialize, Serialize};
use zeroize::Zeroizing;
use crate::clock::Clock;
use crate::confirm::{ConfirmOutcome, ConfirmRequest, Confirmer};
use crate::error::CoreError;
use crate::keypair;
use crate::policy;
use crate::record::SecretRecord;
use crate::secret::SecretValue;
pub const PACKAGE_SCHEMA_VERSION: u32 = 1;
pub const PACKAGE_MAGIC: &[u8; 4] = b"KVPK";
const TOKEN_SECRET_LEN: usize = 32;
const HEADER_LEN: usize = 4 + 4 + 8;
#[derive(Debug, Serialize, Deserialize)]
pub struct PackagePayload {
pub schema_version: u32,
pub environment: String,
pub created: String,
pub expires_at: u64,
pub token_commitment: String,
pub entries: Vec<SecretRecord>,
}
impl PackagePayload {
pub fn new(
environment: impl Into<String>,
created: impl Into<String>,
expires_at: u64,
entries: Vec<SecretRecord>,
) -> Self {
Self {
schema_version: PACKAGE_SCHEMA_VERSION,
environment: environment.into(),
created: created.into(),
expires_at,
token_commitment: String::new(),
entries,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Package {
pub version: u32,
pub expires_at: u64,
sealed: Vec<u8>,
}
impl Package {
fn new(version: u32, expires_at: u64, sealed: Vec<u8>) -> Self {
Self {
version,
expires_at,
sealed,
}
}
pub fn fingerprint(&self) -> String {
blake3::hash(&self.sealed).to_hex().to_string()
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(HEADER_LEN + self.sealed.len());
out.extend_from_slice(PACKAGE_MAGIC);
out.extend_from_slice(&self.version.to_le_bytes());
out.extend_from_slice(&self.expires_at.to_le_bytes());
out.extend_from_slice(&self.sealed);
out
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, CoreError> {
if bytes.len() < HEADER_LEN || &bytes[..4] != PACKAGE_MAGIC {
return Err(CoreError::Package("not a kovra package frame".to_string()));
}
let version = u32::from_le_bytes(bytes[4..8].try_into().expect("checked length"));
if version != PACKAGE_SCHEMA_VERSION {
return Err(CoreError::Package(format!(
"unsupported package version {version}"
)));
}
let expires_at = u64::from_le_bytes(bytes[8..16].try_into().expect("checked length"));
Ok(Self::new(version, expires_at, bytes[HEADER_LEN..].to_vec()))
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AccessToken {
pub version: u32,
pub package_fingerprint: String,
pub expires_at: u64,
pub secret: SecretValue,
}
impl AccessToken {
pub fn to_bytes(&self) -> Result<Vec<u8>, CoreError> {
serde_json::to_vec(self).map_err(|e| CoreError::Serialization(e.to_string()))
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, CoreError> {
serde_json::from_slice(bytes).map_err(|e| CoreError::Serialization(e.to_string()))
}
}
pub fn seal(
mut payload: PackagePayload,
recipient_public_openssh: &str,
) -> Result<(Package, AccessToken), CoreError> {
for entry in &payload.entries {
if policy::prod_not_packageable(entry.environment()) {
return Err(CoreError::Package(format!(
"refusing to package prod secret `{}` (I4a: prod is never packaged)",
entry.canonical_path()
)));
}
}
let mut secret = Zeroizing::new(vec![0u8; TOKEN_SECRET_LEN]);
rand::rngs::OsRng.fill_bytes(&mut secret);
payload.token_commitment = blake3::hash(&secret).to_hex().to_string();
payload.schema_version = PACKAGE_SCHEMA_VERSION;
let expires_at = payload.expires_at;
let plaintext = Zeroizing::new(
serde_json::to_vec(&payload).map_err(|e| CoreError::Serialization(e.to_string()))?,
);
let sealed = keypair::encrypt_to(recipient_public_openssh, &plaintext)?;
let package = Package::new(PACKAGE_SCHEMA_VERSION, expires_at, sealed);
let token = AccessToken {
version: PACKAGE_SCHEMA_VERSION,
package_fingerprint: package.fingerprint(),
expires_at,
secret: SecretValue::new(secret.to_vec()),
};
Ok((package, token))
}
pub fn open_attended(
package: &Package,
recipient_private_openssh: &str,
clock: &dyn Clock,
) -> Result<PackagePayload, CoreError> {
if clock.unix_secs() > package.expires_at {
return Err(CoreError::Package("package has expired".to_string()));
}
let plaintext = keypair::decrypt(recipient_private_openssh, &package.sealed)?;
let payload: PackagePayload =
serde_json::from_slice(&plaintext).map_err(|e| CoreError::Serialization(e.to_string()))?;
Ok(payload)
}
pub fn open_unattended(
package: &Package,
token: &AccessToken,
recipient_private_openssh: &str,
clock: &dyn Clock,
) -> Result<PackagePayload, CoreError> {
let payload = open_attended(package, recipient_private_openssh, clock)?;
verify_token(package, &payload, token, clock)?;
enforce_no_prod_unattended(&payload)?;
Ok(payload)
}
pub fn verify_token(
package: &Package,
payload: &PackagePayload,
token: &AccessToken,
clock: &dyn Clock,
) -> Result<(), CoreError> {
if clock.unix_secs() > token.expires_at {
return Err(CoreError::Package("access token has expired".to_string()));
}
if token.package_fingerprint != package.fingerprint() {
return Err(CoreError::Package(
"access token does not match this package".to_string(),
));
}
let presented = blake3::hash(token.secret.expose()).to_hex().to_string();
if presented != payload.token_commitment {
return Err(CoreError::Package(
"access token secret is not valid for this package".to_string(),
));
}
Ok(())
}
pub fn enforce_no_prod_unattended(payload: &PackagePayload) -> Result<(), CoreError> {
for entry in &payload.entries {
if policy::prod_blocks_unattended(entry.environment()) {
return Err(CoreError::Package(format!(
"prod secret `{}` cannot be delivered unattended (I4b)",
entry.canonical_path()
)));
}
}
Ok(())
}
pub struct TokenConfirmer {
approved: bool,
}
impl TokenConfirmer {
pub fn new(
package: &Package,
payload: &PackagePayload,
token: &AccessToken,
clock: &dyn Clock,
) -> Self {
Self {
approved: verify_token(package, payload, token, clock).is_ok(),
}
}
}
impl Confirmer for TokenConfirmer {
fn confirm(&self, _req: &ConfirmRequest, _timeout: std::time::Duration) -> ConfirmOutcome {
if self.approved {
ConfirmOutcome::Approved
} else {
ConfirmOutcome::Denied
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::clock::MockClock;
use crate::keypair::{KeyAlgorithm, generate};
use crate::sensitivity::Sensitivity;
const HOUR: u64 = 3600;
fn now() -> u64 {
MockClock::default().unix_secs()
}
fn literal(env: &str, key: &str, value: &str) -> SecretRecord {
SecretRecord::Literal {
value: SecretValue::from(value),
sensitivity: Sensitivity::Medium,
revealable: false,
environment: env.to_string(),
component: "app".to_string(),
key: key.to_string(),
description: None,
created: "2026-05-30T00:00:00Z".to_string(),
updated: "2026-05-30T00:00:00Z".to_string(),
}
}
fn reference(env: &str, key: &str, uri: &str) -> SecretRecord {
SecretRecord::Reference {
reference: uri.to_string(),
sensitivity: Sensitivity::Medium,
revealable: false,
environment: env.to_string(),
component: "app".to_string(),
key: key.to_string(),
description: None,
created: "2026-05-30T00:00:00Z".to_string(),
updated: "2026-05-30T00:00:00Z".to_string(),
}
}
fn payload(entries: Vec<SecretRecord>) -> PackagePayload {
PackagePayload::new("dev", "2026-05-30T00:00:00Z", now() + HOUR, entries)
}
#[test]
fn seal_open_round_trips_and_wrong_identity_fails() {
let recipient = generate(KeyAlgorithm::Ed25519).unwrap();
let clock = MockClock::default();
let (package, _token) = seal(
payload(vec![literal("dev", "token", "s3cr3t-dev-value")]),
&recipient.public_openssh,
)
.unwrap();
let opened = open_attended(&package, &recipient.private_openssh, &clock).unwrap();
match &opened.entries[0] {
SecretRecord::Literal { value, .. } => assert_eq!(value.expose(), b"s3cr3t-dev-value"),
other => panic!("expected literal, got {other:?}"),
}
let other = generate(KeyAlgorithm::Ed25519).unwrap();
assert!(open_attended(&package, &other.private_openssh, &clock).is_err());
}
#[test]
fn all_modalities_round_trip() {
let recipient = generate(KeyAlgorithm::Ed25519).unwrap();
let clock = MockClock::default();
let shared = generate(KeyAlgorithm::Ed25519).unwrap();
let entries = vec![
literal("dev", "db", "db-pass"),
reference("dev", "api", "azure-kv://corp-kv/api-key"),
SecretRecord::Keypair {
algorithm: KeyAlgorithm::Ed25519,
private: Some(SecretValue::from(shared.private_openssh.as_str())),
public: shared.public_openssh.clone(),
sensitivity: Sensitivity::High,
revealable: false,
environment: "dev".to_string(),
component: "ssh".to_string(),
key: "deploy".to_string(),
description: None,
created: "2026-05-30T00:00:00Z".to_string(),
updated: "2026-05-30T00:00:00Z".to_string(),
},
SecretRecord::Totp {
seed: SecretValue::from("totp-seed-bytes"),
algorithm: crate::totp::TotpAlgorithm::Sha1,
digits: 6,
period: 30,
sensitivity: Sensitivity::High,
revealable: false,
environment: "dev".to_string(),
component: "auth".to_string(),
key: "mfa".to_string(),
description: None,
created: "2026-05-30T00:00:00Z".to_string(),
updated: "2026-05-30T00:00:00Z".to_string(),
},
];
let (package, _token) = seal(payload(entries), &recipient.public_openssh).unwrap();
let opened = open_attended(&package, &recipient.private_openssh, &clock).unwrap();
assert_eq!(opened.entries.len(), 4);
match &opened.entries[2] {
SecretRecord::Keypair { private, .. } => {
assert_eq!(
private.as_ref().unwrap().expose(),
shared.private_openssh.as_bytes()
);
}
other => panic!("expected keypair, got {other:?}"),
}
match &opened.entries[3] {
SecretRecord::Totp { seed, .. } => assert_eq!(seed.expose(), b"totp-seed-bytes"),
other => panic!("expected totp, got {other:?}"),
}
}
#[test]
fn i4a_packaging_a_prod_secret_is_refused() {
let recipient = generate(KeyAlgorithm::Ed25519).unwrap();
let entries = vec![
literal("dev", "ok", "fine"),
literal("prod", "db", "prod-only-value"),
];
let err = seal(payload(entries), &recipient.public_openssh).unwrap_err();
match err {
CoreError::Package(msg) => {
assert!(msg.contains("prod/app/db"), "names the coordinate: {msg}");
assert!(msg.contains("I4a"));
assert!(
!msg.contains("prod-only-value"),
"error must not carry the value"
);
}
other => panic!("expected Package error, got {other:?}"),
}
}
#[test]
fn i4b_prod_entry_refused_under_a_valid_token() {
let recipient = generate(KeyAlgorithm::Ed25519).unwrap();
let clock = MockClock::default();
let (package, token) =
seal_forged_with_prod(&recipient.public_openssh, now() + HOUR).unwrap();
let payload = open_attended(&package, &recipient.private_openssh, &clock).unwrap();
assert!(verify_token(&package, &payload, &token, &clock).is_ok());
let err =
open_unattended(&package, &token, &recipient.private_openssh, &clock).unwrap_err();
match err {
CoreError::Package(msg) => {
assert!(msg.contains("I4b"), "I4b denial: {msg}");
assert!(msg.contains("prod/app/secret"));
}
other => panic!("expected Package error, got {other:?}"),
}
}
fn seal_forged_with_prod(
recipient_public_openssh: &str,
expires_at: u64,
) -> Result<(Package, AccessToken), CoreError> {
let mut p = PackagePayload::new(
"prod",
"2026-05-30T00:00:00Z",
expires_at,
vec![literal("prod", "secret", "forged-prod-value")],
);
let mut secret = vec![0u8; TOKEN_SECRET_LEN];
rand::rngs::OsRng.fill_bytes(&mut secret);
p.token_commitment = blake3::hash(&secret).to_hex().to_string();
let plaintext = serde_json::to_vec(&p).unwrap();
let sealed = keypair::encrypt_to(recipient_public_openssh, &plaintext)?;
let package = Package::new(PACKAGE_SCHEMA_VERSION, expires_at, sealed);
let token = AccessToken {
version: PACKAGE_SCHEMA_VERSION,
package_fingerprint: package.fingerprint(),
expires_at,
secret: SecretValue::new(secret),
};
Ok((package, token))
}
#[test]
fn i8_reference_travels_as_pointer_only() {
let recipient = generate(KeyAlgorithm::Ed25519).unwrap();
let clock = MockClock::default();
let (package, _token) = seal(
payload(vec![reference("dev", "api", "azure-kv://corp-kv/api-key")]),
&recipient.public_openssh,
)
.unwrap();
let opened = open_attended(&package, &recipient.private_openssh, &clock).unwrap();
match &opened.entries[0] {
SecretRecord::Reference { reference, .. } => {
assert_eq!(reference, "azure-kv://corp-kv/api-key");
}
other => panic!("expected reference, got {other:?}"),
}
assert_eq!(
opened.entries[0].reference(),
Some("azure-kv://corp-kv/api-key")
);
}
#[test]
fn token_ttl_and_fingerprint_binding() {
let recipient = generate(KeyAlgorithm::Ed25519).unwrap();
let (package, token) = seal(
payload(vec![literal("dev", "k", "v")]),
&recipient.public_openssh,
)
.unwrap();
let early = MockClock::default();
let payload = open_attended(&package, &recipient.private_openssh, &early).unwrap();
assert!(verify_token(&package, &payload, &token, &early).is_ok());
let late = MockClock::at(now() + 2 * HOUR);
assert!(open_attended(&package, &recipient.private_openssh, &late).is_err());
assert!(verify_token(&package, &payload, &token, &late).is_err());
let (_other_pkg, other_token) =
seal(payload_for_other(), &recipient.public_openssh).unwrap();
assert!(verify_token(&package, &payload, &other_token, &early).is_err());
}
fn payload_for_other() -> PackagePayload {
PackagePayload::new(
"dev",
"2026-05-30T00:00:00Z",
now() + HOUR,
vec![literal("dev", "other", "other")],
)
}
#[test]
fn token_confirmer_is_two_factor() {
let recipient = generate(KeyAlgorithm::Ed25519).unwrap();
let clock = MockClock::default();
let (package, token) = seal(
payload(vec![literal("dev", "k", "v")]),
&recipient.public_openssh,
)
.unwrap();
let payload = open_attended(&package, &recipient.private_openssh, &clock).unwrap();
let good = TokenConfirmer::new(&package, &payload, &token, &clock);
assert!(
good.confirm(
&ConfirmRequest::new(
"dev/app/k",
Sensitivity::High,
"dev",
crate::scope::Origin::Human
),
std::time::Duration::ZERO
)
.is_approved()
);
let forged = AccessToken {
version: PACKAGE_SCHEMA_VERSION,
package_fingerprint: package.fingerprint(),
expires_at: token.expires_at,
secret: SecretValue::from("not-the-real-secret"),
};
let bad = TokenConfirmer::new(&package, &payload, &forged, &clock);
assert_eq!(
bad.confirm(
&ConfirmRequest::new(
"dev/app/k",
Sensitivity::High,
"dev",
crate::scope::Origin::Human
),
std::time::Duration::ZERO
),
ConfirmOutcome::Denied
);
}
#[test]
fn tampered_package_fails_to_open() {
let recipient = generate(KeyAlgorithm::Ed25519).unwrap();
let clock = MockClock::default();
let (package, _token) = seal(
payload(vec![literal("dev", "k", "v")]),
&recipient.public_openssh,
)
.unwrap();
let mut bytes = package.to_bytes();
let last = bytes.len() - 1;
bytes[last] ^= 0xff;
let tampered = Package::from_bytes(&bytes).unwrap();
assert!(open_attended(&tampered, &recipient.private_openssh, &clock).is_err());
}
#[test]
fn debug_is_redacted_and_frame_round_trips() {
let recipient = generate(KeyAlgorithm::Ed25519).unwrap();
let (package, token) = seal(
payload(vec![literal("dev", "k", "top-secret-literal")]),
&recipient.public_openssh,
)
.unwrap();
let opened = {
let clock = MockClock::default();
open_attended(&package, &recipient.private_openssh, &clock).unwrap()
};
let dbg = format!("{opened:?}");
assert!(dbg.contains("REDACTED"));
assert!(!dbg.contains("top-secret-literal"));
let token_dbg = format!("{token:?}");
assert!(token_dbg.contains("REDACTED"));
let back = Package::from_bytes(&package.to_bytes()).unwrap();
assert_eq!(back, package);
let token2 = AccessToken::from_bytes(&token.to_bytes().unwrap()).unwrap();
assert_eq!(token2.secret.expose(), token.secret.expose());
assert_eq!(token2.package_fingerprint, token.package_fingerprint);
}
#[test]
fn foreign_frame_is_rejected() {
assert!(Package::from_bytes(b"not a package").is_err());
}
}