1pub const SECRET_LEN: usize = 64;
25pub const PUBLIC_LEN: usize = 32;
27
28#[derive(Debug, thiserror::Error)]
29pub enum KeyError {
30 #[error(
31 "{path} holds {got} hex character(s); a varve signing key is {want} — a 32-byte \
32 ed25519 seed followed by its 32-byte public key. Mint one with `varve keygen`."
33 )]
34 WrongLength {
35 path: String,
36 got: usize,
37 want: usize,
38 },
39 #[error("{path} is not hex: {reason}")]
40 NotHex { path: String, reason: String },
41 #[error(
42 "{path} is not a consistent keypair: the public half it carries is not the one its \
43 seed derives. Signing with it produces layers NO trust root can verify. Mint a \
44 fresh key with `varve keygen`."
45 )]
46 Mismatched { path: String },
47}
48
49pub fn generate() -> (String, String) {
52 let (sk, pk) = crate::verify::generate_root_keypair();
53 (hex_encode(&sk), hex_encode(&pk))
54}
55
56pub fn public_from_secret(secret_hex: &str, path: &str) -> Result<String, KeyError> {
60 let bytes = decode_secret(secret_hex, path)?;
61 check_derived(&bytes, path)?;
62 Ok(hex_encode(&bytes[32..]))
63}
64
65pub fn check_keypair(secret_hex: &str, path: &str) -> Result<Vec<u8>, KeyError> {
68 let bytes = decode_secret(secret_hex, path)?;
69 check_derived(&bytes, path)?;
70 Ok(bytes)
71}
72
73fn decode_secret(secret_hex: &str, path: &str) -> Result<Vec<u8>, KeyError> {
74 let trimmed = secret_hex.trim();
75 if trimmed.len() != SECRET_LEN * 2 {
76 return Err(KeyError::WrongLength {
77 path: path.to_string(),
78 got: trimmed.len(),
79 want: SECRET_LEN * 2,
80 });
81 }
82 hex_decode(trimmed).map_err(|reason| KeyError::NotHex {
83 path: path.to_string(),
84 reason,
85 })
86}
87
88fn check_derived(bytes: &[u8], path: &str) -> Result<(), KeyError> {
95 let mismatched = || KeyError::Mismatched {
96 path: path.to_string(),
97 };
98 let probe = br#"{"varve":"keypair-probe"}"#;
99 let envelope = crate::verify::dsse_sign_typed(probe, PROBE_TYPE, bytes, "probe")
100 .map_err(|_| mismatched())?;
101 let verified = crate::verify::dsse_verify_typed(envelope.as_bytes(), PROBE_TYPE, &bytes[32..])
102 .map_err(|_| mismatched())?;
103 if verified != probe {
104 return Err(mismatched());
105 }
106 Ok(())
107}
108
109const PROBE_TYPE: &str = "application/vnd.pulseengine.varve.keypair-probe.v1+json";
112
113fn hex_encode(bytes: &[u8]) -> String {
114 bytes.iter().map(|b| format!("{b:02x}")).collect()
115}
116
117fn hex_decode(s: &str) -> Result<Vec<u8>, String> {
118 if !s.len().is_multiple_of(2) {
119 return Err("odd number of hex digits".into());
120 }
121 (0..s.len())
122 .step_by(2)
123 .map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(|e| e.to_string()))
124 .collect()
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 #[test]
133 fn a_minted_key_yields_the_public_half_a_realm_pins() {
134 let (secret, public) = generate();
135 assert_eq!(secret.len(), SECRET_LEN * 2, "128 hex characters");
136 assert_eq!(public.len(), PUBLIC_LEN * 2, "64 hex characters");
137 assert_eq!(public_from_secret(&secret, "k").unwrap(), public);
139 assert!(public.chars().all(|c| c.is_ascii_hexdigit()));
141 }
142
143 #[test]
145 fn a_key_that_signs_unverifiably_is_refused_before_it_signs() {
146 let entropy = "ab".repeat(SECRET_LEN);
149 match check_keypair(&entropy, "random.key") {
150 Err(KeyError::Mismatched { .. }) => {}
151 other => panic!("entropy must be refused as a keypair, got {other:?}"),
152 }
153 let (secret, _) = generate();
155 let mut bad = secret.clone();
156 bad.replace_range(64..66, if &secret[64..66] == "aa" { "bb" } else { "aa" });
157 assert!(matches!(
158 check_keypair(&bad, "tampered.key"),
159 Err(KeyError::Mismatched { .. })
160 ));
161 assert!(check_keypair(&secret, "good.key").is_ok());
163 }
164
165 #[test]
167 fn the_wrong_length_says_what_it_wanted() {
168 let thirty_two = "ab".repeat(32);
172 let err = check_keypair(&thirty_two, "root.key").unwrap_err();
173 let msg = err.to_string();
174 assert!(msg.contains("64"), "must name what it got: {msg}");
175 assert!(msg.contains("128"), "must name what it needs: {msg}");
176 assert!(msg.contains("varve keygen"), "must carry its fix: {msg}");
177 }
178
179 #[test]
181 fn non_hex_is_its_own_error_not_a_length_complaint() {
182 let not_hex = "z".repeat(SECRET_LEN * 2);
183 assert!(matches!(
184 check_keypair(¬_hex, "k"),
185 Err(KeyError::NotHex { .. })
186 ));
187 }
188
189 #[test]
191 fn a_minted_key_actually_signs_and_verifies_end_to_end() {
192 let (secret, public) = generate();
196 let sk = check_keypair(&secret, "k").unwrap();
197 let payload = crate::manifest::fixtures::manifest_with_tools(
198 "2026.08.0",
199 "qualified",
200 1,
201 "2026-08-01T00:00:00Z",
202 &[("synth", "sha256:aa")],
203 );
204 let envelope = crate::verify::sign_layer_manifest(&payload, &sk, "test-root").unwrap();
205 let pk = hex_decode(&public).unwrap();
206 let back = crate::verify::dsse_verify_typed(
207 envelope.as_bytes(),
208 crate::verify::LAYER_PAYLOAD_TYPE,
209 &pk,
210 )
211 .unwrap();
212 assert_eq!(
213 back, payload,
214 "the minted key round-trips through a real layer"
215 );
216 }
217}