1use mkit_attest::{Algorithm, ExternalSigner, Signer};
28use mkit_core::layout::RepoLayout;
29use mkit_keystore::{KeyRef, KeySelector, open_backend};
30use zeroize::Zeroizing;
31
32use crate::config::Config;
33
34#[derive(Debug)]
36pub enum FactoryError {
37 UnknownAlgorithm(String),
39 UnknownSignerKind(String),
41 MissingKeyFile { algorithm: Algorithm, path: String },
44 MissingKeystoreKey {
47 algorithm: Algorithm,
48 backend: String,
49 reason: String,
50 },
51 InvalidKeyFile { path: String, reason: String },
53 ExternalSignerPath(String),
55 Signer(String),
58 Keystore(String),
60}
61
62impl std::fmt::Display for FactoryError {
63 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 match self {
65 Self::UnknownAlgorithm(s) => write!(
66 f,
67 "unknown algorithm '{s}' — expected one of: ed25519, secp256k1, p256"
68 ),
69 Self::UnknownSignerKind(s) => write!(
70 f,
71 "unknown signer '{s}' — expected one of: repo-key, external, keystore"
72 ),
73 Self::MissingKeyFile { algorithm, path } => write!(
74 f,
75 "{algorithm} key file not found at '{path}' — run `mkit keygen --algorithm {algorithm}` first"
76 ),
77 Self::MissingKeystoreKey {
78 algorithm,
79 backend,
80 reason,
81 } => write!(
82 f,
83 "missing keystore signing key for algorithm {algorithm} — run `mkit key generate --backend {backend} --algorithm {algorithm} --label <label>` first: {reason}"
84 ),
85 Self::InvalidKeyFile { path, reason } => {
86 write!(f, "invalid key file '{path}': {reason}")
87 }
88 Self::ExternalSignerPath(s) => {
89 write!(f, "attest.external_signer_path: {s}")
90 }
91 Self::Signer(s) => write!(f, "signer: {s}"),
92 Self::Keystore(s) => write!(f, "keystore: {s}"),
93 }
94 }
95}
96
97impl std::error::Error for FactoryError {}
98
99pub fn parse_algorithm(s: &str) -> Result<Algorithm, FactoryError> {
101 s.parse::<Algorithm>()
102 .map_err(|_| FactoryError::UnknownAlgorithm(s.to_owned()))
103}
104
105pub fn build_signer(
115 layout: &RepoLayout,
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(layout, 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 (issue #160) is not yet wired into the factory"
177 .to_owned(),
178 )),
179 }
180}
181
182struct KeystoreAttestSigner {
183 algorithm: Algorithm,
184 signer: Box<dyn mkit_keystore::KeySigner>,
185}
186
187impl std::fmt::Debug for KeystoreAttestSigner {
188 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189 f.debug_struct("KeystoreAttestSigner")
190 .field("algorithm", &self.algorithm)
191 .field("signer", &"<keystore>")
192 .finish()
193 }
194}
195
196impl Signer for KeystoreAttestSigner {
197 fn algorithm(&self) -> Algorithm {
198 self.algorithm
199 }
200
201 fn keyid(&self) -> Result<String, mkit_attest::Error> {
202 self.signer
203 .keyid()
204 .map(mkit_keystore::KeyId::into_string)
205 .map_err(|error| mkit_attest::Error::ExternalSignerBadResponse(error.to_string()))
206 }
207
208 fn sign(&mut self, pae: &[u8]) -> Result<Vec<u8>, mkit_attest::Error> {
209 self.signer
210 .sign(pae)
211 .map_err(|error| mkit_attest::Error::ExternalSignerBadResponse(error.to_string()))
212 }
213}
214
215fn build_repo_key_signer(
216 layout: &RepoLayout,
217 algorithm: Algorithm,
218 cfg: &Config,
219) -> Result<Box<dyn Signer>, FactoryError> {
220 match algorithm {
221 Algorithm::Ed25519 => {
222 let rel = cfg.signing_key.as_str();
230 let path = crate::config::resolve_key_path(layout, rel).map_err(|e| {
231 FactoryError::InvalidKeyFile {
232 path: rel.to_owned(),
233 reason: e.to_string(),
234 }
235 })?;
236 if !path.exists() {
237 return Err(FactoryError::MissingKeyFile {
238 algorithm,
239 path: path.display().to_string(),
240 });
241 }
242 let kp =
243 mkit_core::sign::load_key(&path).map_err(|e| FactoryError::InvalidKeyFile {
244 path: path.display().to_string(),
245 reason: e.to_string(),
246 })?;
247 Ok(Box::new(mkit_attest::RepoKeySigner::new(kp)))
248 }
249 Algorithm::Secp256k1 => {
250 let rel = cfg.attest.secp256k1_key_path_or_default();
251 let secret = load_raw_secret(layout, rel, algorithm)?;
252 let signer = mkit_attest::signer_k256::Secp256k1Signer::from_seed_zeroizing(&secret)
255 .map_err(|e| FactoryError::Signer(e.to_string()))?;
256 Ok(Box::new(signer))
257 }
258 Algorithm::P256 => {
259 let rel = cfg.attest.p256_key_path_or_default();
260 let secret = load_raw_secret(layout, rel, algorithm)?;
261 let signer = mkit_attest::signer_p256::P256Signer::from_seed_zeroizing(&secret)
263 .map_err(|e| FactoryError::Signer(e.to_string()))?;
264 Ok(Box::new(signer))
265 }
266 #[cfg(feature = "bls-threshold")]
267 Algorithm::Bls12381Threshold => Err(FactoryError::UnknownAlgorithm(
268 "bls12381-thr repo-key signer (issue #160) awaits the release-party CLI".to_owned(),
269 )),
270 }
271}
272
273fn build_external_signer(
274 algorithm: Algorithm,
275 config: &crate::config::AttestConfig,
276) -> Result<Box<dyn Signer>, FactoryError> {
277 if config.external_signer_path.is_empty() {
278 return Err(FactoryError::ExternalSignerPath(
279 "empty — set `attest.external_signer_path` in user-scoped \
280 config ($XDG_CONFIG_HOME/mkit/config). Per-repo .mkit/config \
281 cannot set this key (security)."
282 .into(),
283 ));
284 }
285 let mut ext = ExternalSigner::with_algorithm(&config.external_signer_path, algorithm)
286 .map_err(|e| FactoryError::ExternalSignerPath(e.to_string()))?
287 .with_args(config.external_signer_args.clone());
288 if let Some(secs) = config.external_signer_timeout_secs {
292 ext = ext.with_timeout(std::time::Duration::from_secs(secs));
293 }
294 Ok(Box::new(ext))
295}
296
297fn load_raw_secret(
298 layout: &RepoLayout,
299 rel_path: &str,
300 algorithm: Algorithm,
301) -> Result<Zeroizing<[u8; 32]>, FactoryError> {
302 let path = crate::config::resolve_key_path(layout, rel_path).map_err(|e| {
303 FactoryError::InvalidKeyFile {
304 path: rel_path.to_owned(),
305 reason: e.to_string(),
306 }
307 })?;
308 if !path.exists() {
309 return Err(FactoryError::MissingKeyFile {
312 algorithm,
313 path: path.display().to_string(),
314 });
315 }
316 mkit_core::sign::load_raw_32(&path).map_err(|e| FactoryError::InvalidKeyFile {
317 path: rel_path.to_owned(),
318 reason: e.to_string(),
319 })
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325 use std::fs;
326 use std::path::Path;
327
328 #[cfg(unix)]
334 fn set_secure_mode(path: &Path, mode: u32) {
335 use std::os::unix::fs::PermissionsExt;
336 let mut perm = fs::metadata(path).unwrap().permissions();
337 perm.set_mode(mode);
338 fs::set_permissions(path, perm).unwrap();
339 }
340
341 #[cfg(not(unix))]
342 fn set_secure_mode(_path: &Path, _mode: u32) {}
343
344 fn write_ed25519_key(path: &Path, bytes: &[u8; 32]) {
347 if let Some(parent) = path.parent() {
348 fs::create_dir_all(parent).unwrap();
349 set_secure_mode(parent, 0o700);
350 }
351 fs::write(path, bytes).unwrap();
352 set_secure_mode(path, 0o600);
353 }
354
355 #[test]
356 fn parse_algorithm_round_trip() {
357 assert_eq!(parse_algorithm("ed25519").unwrap(), Algorithm::Ed25519);
358 assert_eq!(parse_algorithm("secp256k1").unwrap(), Algorithm::Secp256k1);
359 assert_eq!(parse_algorithm("p256").unwrap(), Algorithm::P256);
360 }
361
362 #[test]
363 fn parse_algorithm_rejects_unknown() {
364 match parse_algorithm("rsa") {
365 Err(FactoryError::UnknownAlgorithm(s)) => assert_eq!(s, "rsa"),
366 Err(other) => panic!("unexpected error: {other}"),
367 Ok(_) => panic!("unexpected success"),
368 }
369 }
370
371 #[test]
372 fn unknown_signer_kind_errors() {
373 let td = tempfile::tempdir().unwrap();
374 let cfg = Config::with_defaults();
375 match build_signer(
376 &RepoLayout::single(td.path()),
377 Algorithm::Ed25519,
378 "sigstore",
379 &cfg,
380 ) {
381 Err(FactoryError::UnknownSignerKind(s)) => assert_eq!(s, "sigstore"),
382 Err(other) => panic!("unexpected error: {other}"),
383 Ok(_) => panic!("unexpected success"),
384 }
385 }
386
387 #[test]
391 fn repo_key_ed25519_missing_key_errors_with_keygen_hint() {
392 let td = tempfile::tempdir().unwrap();
393 let cfg = Config::with_defaults();
394 match build_signer(
395 &RepoLayout::single(td.path()),
396 Algorithm::Ed25519,
397 "repo-key",
398 &cfg,
399 ) {
400 Err(FactoryError::MissingKeyFile { algorithm, path }) => {
401 assert_eq!(algorithm, Algorithm::Ed25519);
402 assert!(path.contains("default.key"), "{path}");
403 }
404 Err(other) => panic!("unexpected error: {other}"),
405 Ok(_) => panic!("unexpected success"),
406 }
407 assert!(
408 !td.path().join(".mkit/keys/default.key").exists(),
409 "factory must not silently create the key file"
410 );
411 }
412
413 #[test]
414 fn repo_key_ed25519_loads_existing_key() {
415 let td = tempfile::tempdir().unwrap();
416 let key_path = td.path().join(".mkit/keys/default.key");
417 write_ed25519_key(&key_path, &[0xCDu8; 32]);
418 let cfg = Config::with_defaults();
419 let signer = build_signer(
420 &RepoLayout::single(td.path()),
421 Algorithm::Ed25519,
422 "repo-key",
423 &cfg,
424 )
425 .expect("ed25519 repo-key should load existing key");
426 assert_eq!(signer.algorithm(), Algorithm::Ed25519);
427 }
428
429 #[test]
432 fn repo_key_ed25519_honours_signing_key_config() {
433 let td = tempfile::tempdir().unwrap();
434 let key_path = td.path().join(".mkit/keys/custom-global.key");
435 write_ed25519_key(&key_path, &[0xEFu8; 32]);
436 let mut cfg = Config::with_defaults();
437 cfg.signing_key = ".mkit/keys/custom-global.key".into();
438 let signer = build_signer(
439 &RepoLayout::single(td.path()),
440 Algorithm::Ed25519,
441 "repo-key",
442 &cfg,
443 )
444 .expect("custom signing_key path should load");
445 assert_eq!(signer.algorithm(), Algorithm::Ed25519);
446 }
447
448 #[test]
449 fn repo_key_secp256k1_missing_key_errors_with_keygen_hint() {
450 let td = tempfile::tempdir().unwrap();
451 let cfg = Config::with_defaults();
452 match build_signer(
453 &RepoLayout::single(td.path()),
454 Algorithm::Secp256k1,
455 "repo-key",
456 &cfg,
457 ) {
458 Err(FactoryError::MissingKeyFile { algorithm, path }) => {
459 assert_eq!(algorithm, Algorithm::Secp256k1);
460 assert!(path.contains("secp256k1"));
461 }
462 Err(other) => panic!("unexpected error: {other}"),
463 Ok(_) => panic!("unexpected success"),
464 }
465 }
466
467 #[test]
468 fn repo_key_p256_loads_existing_raw_secret() {
469 let td = tempfile::tempdir().unwrap();
470 fs::create_dir_all(td.path().join(".mkit/keys")).unwrap();
471 let mut secret = [0u8; 32];
472 secret[31] = 3;
473 fs::write(td.path().join(".mkit/keys/p256.key"), secret).unwrap();
474 set_secure_mode(&td.path().join(".mkit/keys/p256.key"), 0o600);
475 set_secure_mode(&td.path().join(".mkit/keys"), 0o700);
476
477 let cfg = Config::with_defaults();
478 let signer = build_signer(
479 &RepoLayout::single(td.path()),
480 Algorithm::P256,
481 "repo-key",
482 &cfg,
483 )
484 .expect("p256 repo-key should load raw secret");
485 assert_eq!(signer.algorithm(), Algorithm::P256);
486 }
487
488 #[test]
489 fn repo_key_wrong_length_key_errors() {
490 let td = tempfile::tempdir().unwrap();
491 fs::create_dir_all(td.path().join(".mkit/keys")).unwrap();
492 fs::write(td.path().join(".mkit/keys/secp256k1.key"), b"short").unwrap();
493 set_secure_mode(&td.path().join(".mkit/keys/secp256k1.key"), 0o600);
494 set_secure_mode(&td.path().join(".mkit/keys"), 0o700);
495
496 let cfg = Config::with_defaults();
497 match build_signer(
498 &RepoLayout::single(td.path()),
499 Algorithm::Secp256k1,
500 "repo-key",
501 &cfg,
502 ) {
503 Err(FactoryError::InvalidKeyFile { reason, .. }) => {
504 assert!(reason.contains("32 bytes"), "{reason}");
505 }
506 Err(other) => panic!("unexpected error: {other}"),
507 Ok(_) => panic!("unexpected success"),
508 }
509 }
510
511 #[test]
512 fn external_signer_requires_path() {
513 let td = tempfile::tempdir().unwrap();
514 let cfg = Config::with_defaults();
515 match build_signer(
516 &RepoLayout::single(td.path()),
517 Algorithm::Ed25519,
518 "external",
519 &cfg,
520 ) {
521 Err(FactoryError::ExternalSignerPath(_)) => {}
522 Err(other) => panic!("unexpected error: {other}"),
523 Ok(_) => panic!("unexpected success"),
524 }
525 }
526}