1use std::path::Path;
28
29use mkit_attest::{Algorithm, ExternalSigner, Signer};
30use mkit_keystore::{KeyRef, KeySelector, open_backend};
31use zeroize::Zeroizing;
32
33use crate::config::Config;
34
35#[derive(Debug)]
37pub enum FactoryError {
38 UnknownAlgorithm(String),
40 UnknownSignerKind(String),
42 MissingKeyFile { algorithm: Algorithm, path: String },
45 MissingKeystoreKey {
48 algorithm: Algorithm,
49 backend: String,
50 reason: String,
51 },
52 InvalidKeyFile { path: String, reason: String },
54 ExternalSignerPath(String),
56 Signer(String),
59 Keystore(String),
61}
62
63impl std::fmt::Display for FactoryError {
64 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65 match self {
66 Self::UnknownAlgorithm(s) => write!(
67 f,
68 "unknown algorithm '{s}' — expected one of: ed25519, secp256k1, p256"
69 ),
70 Self::UnknownSignerKind(s) => write!(
71 f,
72 "unknown signer '{s}' — expected one of: repo-key, external, keystore"
73 ),
74 Self::MissingKeyFile { algorithm, path } => write!(
75 f,
76 "{algorithm} key file not found at '{path}' — run `mkit keygen --algorithm {algorithm}` first"
77 ),
78 Self::MissingKeystoreKey {
79 algorithm,
80 backend,
81 reason,
82 } => write!(
83 f,
84 "missing keystore signing key for algorithm {algorithm} — run `mkit key generate --backend {backend} --algorithm {algorithm} --label <label>` first: {reason}"
85 ),
86 Self::InvalidKeyFile { path, reason } => {
87 write!(f, "invalid key file '{path}': {reason}")
88 }
89 Self::ExternalSignerPath(s) => {
90 write!(f, "attest.external_signer_path: {s}")
91 }
92 Self::Signer(s) => write!(f, "signer: {s}"),
93 Self::Keystore(s) => write!(f, "keystore: {s}"),
94 }
95 }
96}
97
98impl std::error::Error for FactoryError {}
99
100pub fn parse_algorithm(s: &str) -> Result<Algorithm, FactoryError> {
102 s.parse::<Algorithm>()
103 .map_err(|_| FactoryError::UnknownAlgorithm(s.to_owned()))
104}
105
106pub fn build_signer(
115 root: &Path,
116 algorithm: Algorithm,
117 signer_kind: &str,
118 cfg: &Config,
119) -> Result<Box<dyn Signer>, FactoryError> {
120 match signer_kind {
121 "repo-key" => build_repo_key_signer(root, algorithm, cfg),
122 "external" => build_external_signer(algorithm, &cfg.attest),
123 "keystore" => build_keystore_signer(algorithm, cfg),
124 other => Err(FactoryError::UnknownSignerKind(other.to_owned())),
125 }
126}
127
128fn build_keystore_signer(
129 algorithm: Algorithm,
130 cfg: &Config,
131) -> Result<Box<dyn Signer>, FactoryError> {
132 let key_ref = configured_key_ref(cfg, algorithm)
133 .parse::<KeyRef>()
134 .map_err(|error| FactoryError::Keystore(format!("key ref: {error}")))?;
135 let store = open_backend(key_ref.backend())
136 .map_err(|error| FactoryError::Keystore(error.to_string()))?;
137 let keystore_algorithm = to_keystore_algorithm(algorithm)?;
138 let backend = key_ref.backend().to_string();
139 let label = key_ref.label().to_owned();
140 let selector = KeySelector::new(label.clone(), Some(keystore_algorithm))
141 .map_err(|error| FactoryError::Keystore(error.to_string()))?;
142 let opener = store
143 .opener()
144 .ok_or_else(|| FactoryError::Keystore(format!("backend {backend} cannot open keys")))?;
145 let signer = opener.open(&selector).map_err(|error| match error {
146 mkit_keystore::Error::KeyNotFound(_) => FactoryError::MissingKeystoreKey {
147 algorithm,
148 backend,
149 reason: error.to_string(),
150 },
151 other => FactoryError::Keystore(other.to_string()),
152 })?;
153 Ok(Box::new(KeystoreAttestSigner { algorithm, signer }))
154}
155
156fn configured_key_ref(cfg: &Config, algorithm: Algorithm) -> &str {
157 match algorithm {
158 Algorithm::Ed25519 => cfg.key.ed25519_ref_or_fallback(),
159 Algorithm::Secp256k1 => cfg.key.secp256k1_ref_or_fallback(),
160 Algorithm::P256 => cfg.key.p256_ref_or_fallback(),
161 #[cfg(feature = "bls-threshold")]
162 Algorithm::Bls12381Threshold => "",
163 }
164}
165
166#[allow(clippy::unnecessary_wraps)]
169fn to_keystore_algorithm(algorithm: Algorithm) -> Result<mkit_keystore::Algorithm, FactoryError> {
170 match algorithm {
171 Algorithm::Ed25519 => Ok(mkit_keystore::Algorithm::Ed25519),
172 Algorithm::Secp256k1 => Ok(mkit_keystore::Algorithm::Secp256k1),
173 Algorithm::P256 => Ok(mkit_keystore::Algorithm::P256),
174 #[cfg(feature = "bls-threshold")]
175 Algorithm::Bls12381Threshold => Err(FactoryError::UnknownAlgorithm(
176 "bls12381-thr keystore backend is Phase 2 of issue #160".to_owned(),
177 )),
178 }
179}
180
181struct KeystoreAttestSigner {
182 algorithm: Algorithm,
183 signer: Box<dyn mkit_keystore::KeySigner>,
184}
185
186impl std::fmt::Debug for KeystoreAttestSigner {
187 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188 f.debug_struct("KeystoreAttestSigner")
189 .field("algorithm", &self.algorithm)
190 .field("signer", &"<keystore>")
191 .finish()
192 }
193}
194
195impl Signer for KeystoreAttestSigner {
196 fn algorithm(&self) -> Algorithm {
197 self.algorithm
198 }
199
200 fn keyid(&self) -> Result<String, mkit_attest::Error> {
201 self.signer
202 .keyid()
203 .map(mkit_keystore::KeyId::into_string)
204 .map_err(|error| mkit_attest::Error::ExternalSignerBadResponse(error.to_string()))
205 }
206
207 fn sign(&mut self, pae: &[u8]) -> Result<Vec<u8>, mkit_attest::Error> {
208 self.signer
209 .sign(pae)
210 .map_err(|error| mkit_attest::Error::ExternalSignerBadResponse(error.to_string()))
211 }
212}
213
214fn build_repo_key_signer(
215 root: &Path,
216 algorithm: Algorithm,
217 cfg: &Config,
218) -> Result<Box<dyn Signer>, FactoryError> {
219 match algorithm {
220 Algorithm::Ed25519 => {
221 let rel = cfg.signing_key.as_str();
229 let path = crate::config::resolve_key_path(root, rel).map_err(|e| {
230 FactoryError::InvalidKeyFile {
231 path: rel.to_owned(),
232 reason: e.to_string(),
233 }
234 })?;
235 if !path.exists() {
236 return Err(FactoryError::MissingKeyFile {
237 algorithm,
238 path: path.display().to_string(),
239 });
240 }
241 let kp =
242 mkit_core::sign::load_key(&path).map_err(|e| FactoryError::InvalidKeyFile {
243 path: path.display().to_string(),
244 reason: e.to_string(),
245 })?;
246 Ok(Box::new(mkit_attest::RepoKeySigner::new(kp)))
247 }
248 Algorithm::Secp256k1 => {
249 let rel = cfg.attest.secp256k1_key_path_or_default();
250 let secret = load_raw_secret(root, rel, algorithm)?;
251 let signer = mkit_attest::signer_k256::Secp256k1Signer::from_seed_zeroizing(&secret)
254 .map_err(|e| FactoryError::Signer(e.to_string()))?;
255 Ok(Box::new(signer))
256 }
257 Algorithm::P256 => {
258 let rel = cfg.attest.p256_key_path_or_default();
259 let secret = load_raw_secret(root, rel, algorithm)?;
260 let signer = mkit_attest::signer_p256::P256Signer::from_seed_zeroizing(&secret)
262 .map_err(|e| FactoryError::Signer(e.to_string()))?;
263 Ok(Box::new(signer))
264 }
265 #[cfg(feature = "bls-threshold")]
266 Algorithm::Bls12381Threshold => Err(FactoryError::UnknownAlgorithm(
267 "bls12381-thr repo-key signer is Phase 3 of issue #160 (release-party CLI)".to_owned(),
268 )),
269 }
270}
271
272fn build_external_signer(
273 algorithm: Algorithm,
274 config: &crate::config::AttestConfig,
275) -> Result<Box<dyn Signer>, FactoryError> {
276 if config.external_signer_path.is_empty() {
277 return Err(FactoryError::ExternalSignerPath(
278 "empty — set `attest.external_signer_path` in user-scoped \
279 config ($XDG_CONFIG_HOME/mkit/config). Per-repo .mkit/config \
280 cannot set this key (security)."
281 .into(),
282 ));
283 }
284 let mut ext = ExternalSigner::with_algorithm(&config.external_signer_path, algorithm)
285 .map_err(|e| FactoryError::ExternalSignerPath(e.to_string()))?
286 .with_args(config.external_signer_args.clone());
287 if let Some(secs) = config.external_signer_timeout_secs {
291 ext = ext.with_timeout(std::time::Duration::from_secs(secs));
292 }
293 Ok(Box::new(ext))
294}
295
296fn load_raw_secret(
297 root: &Path,
298 rel_path: &str,
299 algorithm: Algorithm,
300) -> Result<Zeroizing<[u8; 32]>, FactoryError> {
301 let path = crate::config::resolve_key_path(root, rel_path).map_err(|e| {
302 FactoryError::InvalidKeyFile {
303 path: rel_path.to_owned(),
304 reason: e.to_string(),
305 }
306 })?;
307 if !path.exists() {
308 return Err(FactoryError::MissingKeyFile {
309 algorithm,
310 path: rel_path.to_owned(),
311 });
312 }
313 mkit_core::sign::load_raw_32(&path).map_err(|e| FactoryError::InvalidKeyFile {
314 path: rel_path.to_owned(),
315 reason: e.to_string(),
316 })
317}
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322 use std::fs;
323 use std::os::unix::fs::PermissionsExt;
324
325 fn write_ed25519_key(path: &Path, bytes: &[u8; 32]) {
328 if let Some(parent) = path.parent() {
329 fs::create_dir_all(parent).unwrap();
330 let mut p = fs::metadata(parent).unwrap().permissions();
331 p.set_mode(0o700);
332 fs::set_permissions(parent, p).unwrap();
333 }
334 fs::write(path, bytes).unwrap();
335 let mut perm = fs::metadata(path).unwrap().permissions();
336 perm.set_mode(0o600);
337 fs::set_permissions(path, perm).unwrap();
338 }
339
340 #[test]
341 fn parse_algorithm_round_trip() {
342 assert_eq!(parse_algorithm("ed25519").unwrap(), Algorithm::Ed25519);
343 assert_eq!(parse_algorithm("secp256k1").unwrap(), Algorithm::Secp256k1);
344 assert_eq!(parse_algorithm("p256").unwrap(), Algorithm::P256);
345 }
346
347 #[test]
348 fn parse_algorithm_rejects_unknown() {
349 match parse_algorithm("rsa") {
350 Err(FactoryError::UnknownAlgorithm(s)) => assert_eq!(s, "rsa"),
351 Err(other) => panic!("unexpected error: {other}"),
352 Ok(_) => panic!("unexpected success"),
353 }
354 }
355
356 #[test]
357 fn unknown_signer_kind_errors() {
358 let td = tempfile::tempdir().unwrap();
359 let cfg = Config::with_defaults();
360 match build_signer(td.path(), Algorithm::Ed25519, "sigstore", &cfg) {
361 Err(FactoryError::UnknownSignerKind(s)) => assert_eq!(s, "sigstore"),
362 Err(other) => panic!("unexpected error: {other}"),
363 Ok(_) => panic!("unexpected success"),
364 }
365 }
366
367 #[test]
371 fn repo_key_ed25519_missing_key_errors_with_keygen_hint() {
372 let td = tempfile::tempdir().unwrap();
373 let cfg = Config::with_defaults();
374 match build_signer(td.path(), Algorithm::Ed25519, "repo-key", &cfg) {
375 Err(FactoryError::MissingKeyFile { algorithm, path }) => {
376 assert_eq!(algorithm, Algorithm::Ed25519);
377 assert!(path.contains("default.key"), "{path}");
378 }
379 Err(other) => panic!("unexpected error: {other}"),
380 Ok(_) => panic!("unexpected success"),
381 }
382 assert!(
383 !td.path().join(".mkit/keys/default.key").exists(),
384 "factory must not silently create the key file"
385 );
386 }
387
388 #[test]
389 fn repo_key_ed25519_loads_existing_key() {
390 let td = tempfile::tempdir().unwrap();
391 let key_path = td.path().join(".mkit/keys/default.key");
392 write_ed25519_key(&key_path, &[0xCDu8; 32]);
393 let cfg = Config::with_defaults();
394 let signer = build_signer(td.path(), Algorithm::Ed25519, "repo-key", &cfg)
395 .expect("ed25519 repo-key should load existing key");
396 assert_eq!(signer.algorithm(), Algorithm::Ed25519);
397 }
398
399 #[test]
402 fn repo_key_ed25519_honours_signing_key_config() {
403 let td = tempfile::tempdir().unwrap();
404 let key_path = td.path().join(".mkit/keys/custom-global.key");
405 write_ed25519_key(&key_path, &[0xEFu8; 32]);
406 let mut cfg = Config::with_defaults();
407 cfg.signing_key = ".mkit/keys/custom-global.key".into();
408 let signer = build_signer(td.path(), Algorithm::Ed25519, "repo-key", &cfg)
409 .expect("custom signing_key path should load");
410 assert_eq!(signer.algorithm(), Algorithm::Ed25519);
411 }
412
413 #[test]
414 fn repo_key_secp256k1_missing_key_errors_with_keygen_hint() {
415 let td = tempfile::tempdir().unwrap();
416 let cfg = Config::with_defaults();
417 match build_signer(td.path(), Algorithm::Secp256k1, "repo-key", &cfg) {
418 Err(FactoryError::MissingKeyFile { algorithm, path }) => {
419 assert_eq!(algorithm, Algorithm::Secp256k1);
420 assert!(path.contains("secp256k1"));
421 }
422 Err(other) => panic!("unexpected error: {other}"),
423 Ok(_) => panic!("unexpected success"),
424 }
425 }
426
427 #[test]
428 fn repo_key_p256_loads_existing_raw_secret() {
429 let td = tempfile::tempdir().unwrap();
430 fs::create_dir_all(td.path().join(".mkit/keys")).unwrap();
431 let mut secret = [0u8; 32];
432 secret[31] = 3;
433 fs::write(td.path().join(".mkit/keys/p256.key"), secret).unwrap();
434 let mut perm = fs::metadata(td.path().join(".mkit/keys/p256.key"))
435 .unwrap()
436 .permissions();
437 perm.set_mode(0o600);
438 fs::set_permissions(td.path().join(".mkit/keys/p256.key"), perm).unwrap();
439 let mut dperm = fs::metadata(td.path().join(".mkit/keys"))
440 .unwrap()
441 .permissions();
442 dperm.set_mode(0o700);
443 fs::set_permissions(td.path().join(".mkit/keys"), dperm).unwrap();
444
445 let cfg = Config::with_defaults();
446 let signer = build_signer(td.path(), Algorithm::P256, "repo-key", &cfg)
447 .expect("p256 repo-key should load raw secret");
448 assert_eq!(signer.algorithm(), Algorithm::P256);
449 }
450
451 #[test]
452 fn repo_key_wrong_length_key_errors() {
453 let td = tempfile::tempdir().unwrap();
454 fs::create_dir_all(td.path().join(".mkit/keys")).unwrap();
455 fs::write(td.path().join(".mkit/keys/secp256k1.key"), b"short").unwrap();
456 let mut perm = fs::metadata(td.path().join(".mkit/keys/secp256k1.key"))
457 .unwrap()
458 .permissions();
459 perm.set_mode(0o600);
460 fs::set_permissions(td.path().join(".mkit/keys/secp256k1.key"), perm).unwrap();
461 let mut dperm = fs::metadata(td.path().join(".mkit/keys"))
462 .unwrap()
463 .permissions();
464 dperm.set_mode(0o700);
465 fs::set_permissions(td.path().join(".mkit/keys"), dperm).unwrap();
466
467 let cfg = Config::with_defaults();
468 match build_signer(td.path(), Algorithm::Secp256k1, "repo-key", &cfg) {
469 Err(FactoryError::InvalidKeyFile { reason, .. }) => {
470 assert!(reason.contains("32 bytes"), "{reason}");
471 }
472 Err(other) => panic!("unexpected error: {other}"),
473 Ok(_) => panic!("unexpected success"),
474 }
475 }
476
477 #[test]
478 fn external_signer_requires_path() {
479 let td = tempfile::tempdir().unwrap();
480 let cfg = Config::with_defaults();
481 match build_signer(td.path(), Algorithm::Ed25519, "external", &cfg) {
482 Err(FactoryError::ExternalSignerPath(_)) => {}
483 Err(other) => panic!("unexpected error: {other}"),
484 Ok(_) => panic!("unexpected success"),
485 }
486 }
487}