pub const SECRET_LEN: usize = 64;
pub const PUBLIC_LEN: usize = 32;
#[derive(Debug, thiserror::Error)]
pub enum KeyError {
#[error(
"{path} holds {got} hex character(s); a varve signing key is {want} — a 32-byte \
ed25519 seed followed by its 32-byte public key. Mint one with `varve keygen`."
)]
WrongLength {
path: String,
got: usize,
want: usize,
},
#[error("{path} is not hex: {reason}")]
NotHex { path: String, reason: String },
#[error(
"{path} is not a consistent keypair: the public half it carries is not the one its \
seed derives. Signing with it produces layers NO trust root can verify. Mint a \
fresh key with `varve keygen`."
)]
Mismatched { path: String },
}
pub fn generate() -> (String, String) {
let (sk, pk) = crate::verify::generate_root_keypair();
(hex_encode(&sk), hex_encode(&pk))
}
pub fn public_from_secret(secret_hex: &str, path: &str) -> Result<String, KeyError> {
let bytes = decode_secret(secret_hex, path)?;
check_derived(&bytes, path)?;
Ok(hex_encode(&bytes[32..]))
}
pub fn check_keypair(secret_hex: &str, path: &str) -> Result<Vec<u8>, KeyError> {
let bytes = decode_secret(secret_hex, path)?;
check_derived(&bytes, path)?;
Ok(bytes)
}
fn decode_secret(secret_hex: &str, path: &str) -> Result<Vec<u8>, KeyError> {
let trimmed = secret_hex.trim();
if trimmed.len() != SECRET_LEN * 2 {
return Err(KeyError::WrongLength {
path: path.to_string(),
got: trimmed.len(),
want: SECRET_LEN * 2,
});
}
hex_decode(trimmed).map_err(|reason| KeyError::NotHex {
path: path.to_string(),
reason,
})
}
fn check_derived(bytes: &[u8], path: &str) -> Result<(), KeyError> {
let mismatched = || KeyError::Mismatched {
path: path.to_string(),
};
let probe = br#"{"varve":"keypair-probe"}"#;
let envelope = crate::verify::dsse_sign_typed(probe, PROBE_TYPE, bytes, "probe")
.map_err(|_| mismatched())?;
let verified = crate::verify::dsse_verify_typed(envelope.as_bytes(), PROBE_TYPE, &bytes[32..])
.map_err(|_| mismatched())?;
if verified != probe {
return Err(mismatched());
}
Ok(())
}
const PROBE_TYPE: &str = "application/vnd.pulseengine.varve.keypair-probe.v1+json";
fn hex_encode(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
fn hex_decode(s: &str) -> Result<Vec<u8>, String> {
if !s.len().is_multiple_of(2) {
return Err("odd number of hex digits".into());
}
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(|e| e.to_string()))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_minted_key_yields_the_public_half_a_realm_pins() {
let (secret, public) = generate();
assert_eq!(secret.len(), SECRET_LEN * 2, "128 hex characters");
assert_eq!(public.len(), PUBLIC_LEN * 2, "64 hex characters");
assert_eq!(public_from_secret(&secret, "k").unwrap(), public);
assert!(public.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn a_key_that_signs_unverifiably_is_refused_before_it_signs() {
let entropy = "ab".repeat(SECRET_LEN);
match check_keypair(&entropy, "random.key") {
Err(KeyError::Mismatched { .. }) => {}
other => panic!("entropy must be refused as a keypair, got {other:?}"),
}
let (secret, _) = generate();
let mut bad = secret.clone();
bad.replace_range(64..66, if &secret[64..66] == "aa" { "bb" } else { "aa" });
assert!(matches!(
check_keypair(&bad, "tampered.key"),
Err(KeyError::Mismatched { .. })
));
assert!(check_keypair(&secret, "good.key").is_ok());
}
#[test]
fn the_wrong_length_says_what_it_wanted() {
let thirty_two = "ab".repeat(32);
let err = check_keypair(&thirty_two, "root.key").unwrap_err();
let msg = err.to_string();
assert!(msg.contains("64"), "must name what it got: {msg}");
assert!(msg.contains("128"), "must name what it needs: {msg}");
assert!(msg.contains("varve keygen"), "must carry its fix: {msg}");
}
#[test]
fn non_hex_is_its_own_error_not_a_length_complaint() {
let not_hex = "z".repeat(SECRET_LEN * 2);
assert!(matches!(
check_keypair(¬_hex, "k"),
Err(KeyError::NotHex { .. })
));
}
#[test]
fn a_minted_key_actually_signs_and_verifies_end_to_end() {
let (secret, public) = generate();
let sk = check_keypair(&secret, "k").unwrap();
let payload = crate::manifest::fixtures::manifest_with_tools(
"2026.08.0",
"qualified",
1,
"2026-08-01T00:00:00Z",
&[("synth", "sha256:aa")],
);
let envelope = crate::verify::sign_layer_manifest(&payload, &sk, "test-root").unwrap();
let pk = hex_decode(&public).unwrap();
let back = crate::verify::dsse_verify_typed(
envelope.as_bytes(),
crate::verify::LAYER_PAYLOAD_TYPE,
&pk,
)
.unwrap();
assert_eq!(
back, payload,
"the minted key round-trips through a real layer"
);
}
}