use mkit_attest::{Algorithm, ExternalSigner, Signer};
use mkit_core::layout::RepoLayout;
use mkit_keystore::{KeyRef, KeySelector, open_backend};
use zeroize::Zeroizing;
use crate::config::Config;
#[derive(Debug)]
pub enum FactoryError {
UnknownAlgorithm(String),
UnknownSignerKind(String),
MissingKeyFile { algorithm: Algorithm, path: String },
MissingKeystoreKey {
algorithm: Algorithm,
backend: String,
reason: String,
},
InvalidKeyFile { path: String, reason: String },
ExternalSignerPath(String),
Signer(String),
Keystore(String),
}
impl std::fmt::Display for FactoryError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnknownAlgorithm(s) => write!(
f,
"unknown algorithm '{s}' — expected one of: ed25519, secp256k1, p256"
),
Self::UnknownSignerKind(s) => write!(
f,
"unknown signer '{s}' — expected one of: repo-key, external, keystore"
),
Self::MissingKeyFile { algorithm, path } => write!(
f,
"{algorithm} key file not found at '{path}' — run `mkit keygen --algorithm {algorithm}` first"
),
Self::MissingKeystoreKey {
algorithm,
backend,
reason,
} => write!(
f,
"missing keystore signing key for algorithm {algorithm} — run `mkit key generate --backend {backend} --algorithm {algorithm} --label <label>` first: {reason}"
),
Self::InvalidKeyFile { path, reason } => {
write!(f, "invalid key file '{path}': {reason}")
}
Self::ExternalSignerPath(s) => {
write!(f, "attest.external_signer_path: {s}")
}
Self::Signer(s) => write!(f, "signer: {s}"),
Self::Keystore(s) => write!(f, "keystore: {s}"),
}
}
}
impl std::error::Error for FactoryError {}
pub fn parse_algorithm(s: &str) -> Result<Algorithm, FactoryError> {
s.parse::<Algorithm>()
.map_err(|_| FactoryError::UnknownAlgorithm(s.to_owned()))
}
pub fn build_signer(
layout: &RepoLayout,
algorithm: Algorithm,
signer_kind: &str,
cfg: &Config,
) -> Result<Box<dyn Signer>, FactoryError> {
match signer_kind {
"repo-key" => build_repo_key_signer(layout, algorithm, cfg),
"external" => build_external_signer(algorithm, &cfg.attest),
"keystore" => build_keystore_signer(algorithm, cfg),
other => Err(FactoryError::UnknownSignerKind(other.to_owned())),
}
}
fn build_keystore_signer(
algorithm: Algorithm,
cfg: &Config,
) -> Result<Box<dyn Signer>, FactoryError> {
let key_ref = configured_key_ref(cfg, algorithm)
.parse::<KeyRef>()
.map_err(|error| FactoryError::Keystore(format!("key ref: {error}")))?;
let store = open_backend(key_ref.backend())
.map_err(|error| FactoryError::Keystore(error.to_string()))?;
let keystore_algorithm = to_keystore_algorithm(algorithm)?;
let backend = key_ref.backend().to_string();
let label = key_ref.label().to_owned();
let selector = KeySelector::new(label.clone(), Some(keystore_algorithm))
.map_err(|error| FactoryError::Keystore(error.to_string()))?;
let opener = store
.opener()
.ok_or_else(|| FactoryError::Keystore(format!("backend {backend} cannot open keys")))?;
let signer = opener.open(&selector).map_err(|error| match error {
mkit_keystore::Error::KeyNotFound(_) => FactoryError::MissingKeystoreKey {
algorithm,
backend,
reason: error.to_string(),
},
other => FactoryError::Keystore(other.to_string()),
})?;
Ok(Box::new(KeystoreAttestSigner { algorithm, signer }))
}
fn configured_key_ref(cfg: &Config, algorithm: Algorithm) -> &str {
match algorithm {
Algorithm::Ed25519 => cfg.key.ed25519_ref_or_fallback(),
Algorithm::Secp256k1 => cfg.key.secp256k1_ref_or_fallback(),
Algorithm::P256 => cfg.key.p256_ref_or_fallback(),
#[cfg(feature = "bls-threshold")]
Algorithm::Bls12381Threshold => "",
}
}
#[allow(clippy::unnecessary_wraps)]
fn to_keystore_algorithm(algorithm: Algorithm) -> Result<mkit_keystore::Algorithm, FactoryError> {
match algorithm {
Algorithm::Ed25519 => Ok(mkit_keystore::Algorithm::Ed25519),
Algorithm::Secp256k1 => Ok(mkit_keystore::Algorithm::Secp256k1),
Algorithm::P256 => Ok(mkit_keystore::Algorithm::P256),
#[cfg(feature = "bls-threshold")]
Algorithm::Bls12381Threshold => Err(FactoryError::UnknownAlgorithm(
"bls12381-thr keystore backend (issue #160) is not yet wired into the factory"
.to_owned(),
)),
}
}
struct KeystoreAttestSigner {
algorithm: Algorithm,
signer: Box<dyn mkit_keystore::KeySigner>,
}
impl std::fmt::Debug for KeystoreAttestSigner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("KeystoreAttestSigner")
.field("algorithm", &self.algorithm)
.field("signer", &"<keystore>")
.finish()
}
}
impl Signer for KeystoreAttestSigner {
fn algorithm(&self) -> Algorithm {
self.algorithm
}
fn keyid(&self) -> Result<String, mkit_attest::Error> {
self.signer
.keyid()
.map(mkit_keystore::KeyId::into_string)
.map_err(|error| mkit_attest::Error::ExternalSignerBadResponse(error.to_string()))
}
fn sign(&mut self, pae: &[u8]) -> Result<Vec<u8>, mkit_attest::Error> {
self.signer
.sign(pae)
.map_err(|error| mkit_attest::Error::ExternalSignerBadResponse(error.to_string()))
}
}
fn build_repo_key_signer(
layout: &RepoLayout,
algorithm: Algorithm,
cfg: &Config,
) -> Result<Box<dyn Signer>, FactoryError> {
match algorithm {
Algorithm::Ed25519 => {
let rel = cfg.signing_key.as_str();
let path = crate::config::resolve_key_path(layout, rel).map_err(|e| {
FactoryError::InvalidKeyFile {
path: rel.to_owned(),
reason: e.to_string(),
}
})?;
if !path.exists() {
return Err(FactoryError::MissingKeyFile {
algorithm,
path: path.display().to_string(),
});
}
let kp =
mkit_core::sign::load_key(&path).map_err(|e| FactoryError::InvalidKeyFile {
path: path.display().to_string(),
reason: e.to_string(),
})?;
Ok(Box::new(mkit_attest::RepoKeySigner::new(kp)))
}
Algorithm::Secp256k1 => {
let rel = cfg.attest.secp256k1_key_path_or_default();
let secret = load_raw_secret(layout, rel, algorithm)?;
let signer = mkit_attest::signer_k256::Secp256k1Signer::from_seed_zeroizing(&secret)
.map_err(|e| FactoryError::Signer(e.to_string()))?;
Ok(Box::new(signer))
}
Algorithm::P256 => {
let rel = cfg.attest.p256_key_path_or_default();
let secret = load_raw_secret(layout, rel, algorithm)?;
let signer = mkit_attest::signer_p256::P256Signer::from_seed_zeroizing(&secret)
.map_err(|e| FactoryError::Signer(e.to_string()))?;
Ok(Box::new(signer))
}
#[cfg(feature = "bls-threshold")]
Algorithm::Bls12381Threshold => Err(FactoryError::UnknownAlgorithm(
"bls12381-thr repo-key signer (issue #160) awaits the release-party CLI".to_owned(),
)),
}
}
fn build_external_signer(
algorithm: Algorithm,
config: &crate::config::AttestConfig,
) -> Result<Box<dyn Signer>, FactoryError> {
if config.external_signer_path.is_empty() {
return Err(FactoryError::ExternalSignerPath(
"empty — set `attest.external_signer_path` in user-scoped \
config ($XDG_CONFIG_HOME/mkit/config). Per-repo .mkit/config \
cannot set this key (security)."
.into(),
));
}
let mut ext = ExternalSigner::with_algorithm(&config.external_signer_path, algorithm)
.map_err(|e| FactoryError::ExternalSignerPath(e.to_string()))?
.with_args(config.external_signer_args.clone());
if let Some(secs) = config.external_signer_timeout_secs {
ext = ext.with_timeout(std::time::Duration::from_secs(secs));
}
Ok(Box::new(ext))
}
fn load_raw_secret(
layout: &RepoLayout,
rel_path: &str,
algorithm: Algorithm,
) -> Result<Zeroizing<[u8; 32]>, FactoryError> {
let path = crate::config::resolve_key_path(layout, rel_path).map_err(|e| {
FactoryError::InvalidKeyFile {
path: rel_path.to_owned(),
reason: e.to_string(),
}
})?;
if !path.exists() {
return Err(FactoryError::MissingKeyFile {
algorithm,
path: path.display().to_string(),
});
}
mkit_core::sign::load_raw_32(&path).map_err(|e| FactoryError::InvalidKeyFile {
path: rel_path.to_owned(),
reason: e.to_string(),
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::path::Path;
#[cfg(unix)]
fn set_secure_mode(path: &Path, mode: u32) {
use std::os::unix::fs::PermissionsExt;
let mut perm = fs::metadata(path).unwrap().permissions();
perm.set_mode(mode);
fs::set_permissions(path, perm).unwrap();
}
#[cfg(not(unix))]
fn set_secure_mode(_path: &Path, _mode: u32) {}
fn write_ed25519_key(path: &Path, bytes: &[u8; 32]) {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).unwrap();
set_secure_mode(parent, 0o700);
}
fs::write(path, bytes).unwrap();
set_secure_mode(path, 0o600);
}
#[test]
fn parse_algorithm_round_trip() {
assert_eq!(parse_algorithm("ed25519").unwrap(), Algorithm::Ed25519);
assert_eq!(parse_algorithm("secp256k1").unwrap(), Algorithm::Secp256k1);
assert_eq!(parse_algorithm("p256").unwrap(), Algorithm::P256);
}
#[test]
fn parse_algorithm_rejects_unknown() {
match parse_algorithm("rsa") {
Err(FactoryError::UnknownAlgorithm(s)) => assert_eq!(s, "rsa"),
Err(other) => panic!("unexpected error: {other}"),
Ok(_) => panic!("unexpected success"),
}
}
#[test]
fn unknown_signer_kind_errors() {
let td = tempfile::tempdir().unwrap();
let cfg = Config::with_defaults();
match build_signer(
&RepoLayout::single(td.path()),
Algorithm::Ed25519,
"sigstore",
&cfg,
) {
Err(FactoryError::UnknownSignerKind(s)) => assert_eq!(s, "sigstore"),
Err(other) => panic!("unexpected error: {other}"),
Ok(_) => panic!("unexpected success"),
}
}
#[test]
fn repo_key_ed25519_missing_key_errors_with_keygen_hint() {
let td = tempfile::tempdir().unwrap();
let cfg = Config::with_defaults();
match build_signer(
&RepoLayout::single(td.path()),
Algorithm::Ed25519,
"repo-key",
&cfg,
) {
Err(FactoryError::MissingKeyFile { algorithm, path }) => {
assert_eq!(algorithm, Algorithm::Ed25519);
assert!(path.contains("default.key"), "{path}");
}
Err(other) => panic!("unexpected error: {other}"),
Ok(_) => panic!("unexpected success"),
}
assert!(
!td.path().join(".mkit/keys/default.key").exists(),
"factory must not silently create the key file"
);
}
#[test]
fn repo_key_ed25519_loads_existing_key() {
let td = tempfile::tempdir().unwrap();
let key_path = td.path().join(".mkit/keys/default.key");
write_ed25519_key(&key_path, &[0xCDu8; 32]);
let cfg = Config::with_defaults();
let signer = build_signer(
&RepoLayout::single(td.path()),
Algorithm::Ed25519,
"repo-key",
&cfg,
)
.expect("ed25519 repo-key should load existing key");
assert_eq!(signer.algorithm(), Algorithm::Ed25519);
}
#[test]
fn repo_key_ed25519_honours_signing_key_config() {
let td = tempfile::tempdir().unwrap();
let key_path = td.path().join(".mkit/keys/custom-global.key");
write_ed25519_key(&key_path, &[0xEFu8; 32]);
let mut cfg = Config::with_defaults();
cfg.signing_key = ".mkit/keys/custom-global.key".into();
let signer = build_signer(
&RepoLayout::single(td.path()),
Algorithm::Ed25519,
"repo-key",
&cfg,
)
.expect("custom signing_key path should load");
assert_eq!(signer.algorithm(), Algorithm::Ed25519);
}
#[test]
fn repo_key_secp256k1_missing_key_errors_with_keygen_hint() {
let td = tempfile::tempdir().unwrap();
let cfg = Config::with_defaults();
match build_signer(
&RepoLayout::single(td.path()),
Algorithm::Secp256k1,
"repo-key",
&cfg,
) {
Err(FactoryError::MissingKeyFile { algorithm, path }) => {
assert_eq!(algorithm, Algorithm::Secp256k1);
assert!(path.contains("secp256k1"));
}
Err(other) => panic!("unexpected error: {other}"),
Ok(_) => panic!("unexpected success"),
}
}
#[test]
fn repo_key_p256_loads_existing_raw_secret() {
let td = tempfile::tempdir().unwrap();
fs::create_dir_all(td.path().join(".mkit/keys")).unwrap();
let mut secret = [0u8; 32];
secret[31] = 3;
fs::write(td.path().join(".mkit/keys/p256.key"), secret).unwrap();
set_secure_mode(&td.path().join(".mkit/keys/p256.key"), 0o600);
set_secure_mode(&td.path().join(".mkit/keys"), 0o700);
let cfg = Config::with_defaults();
let signer = build_signer(
&RepoLayout::single(td.path()),
Algorithm::P256,
"repo-key",
&cfg,
)
.expect("p256 repo-key should load raw secret");
assert_eq!(signer.algorithm(), Algorithm::P256);
}
#[test]
fn repo_key_wrong_length_key_errors() {
let td = tempfile::tempdir().unwrap();
fs::create_dir_all(td.path().join(".mkit/keys")).unwrap();
fs::write(td.path().join(".mkit/keys/secp256k1.key"), b"short").unwrap();
set_secure_mode(&td.path().join(".mkit/keys/secp256k1.key"), 0o600);
set_secure_mode(&td.path().join(".mkit/keys"), 0o700);
let cfg = Config::with_defaults();
match build_signer(
&RepoLayout::single(td.path()),
Algorithm::Secp256k1,
"repo-key",
&cfg,
) {
Err(FactoryError::InvalidKeyFile { reason, .. }) => {
assert!(reason.contains("32 bytes"), "{reason}");
}
Err(other) => panic!("unexpected error: {other}"),
Ok(_) => panic!("unexpected success"),
}
}
#[test]
fn external_signer_requires_path() {
let td = tempfile::tempdir().unwrap();
let cfg = Config::with_defaults();
match build_signer(
&RepoLayout::single(td.path()),
Algorithm::Ed25519,
"external",
&cfg,
) {
Err(FactoryError::ExternalSignerPath(_)) => {}
Err(other) => panic!("unexpected error: {other}"),
Ok(_) => panic!("unexpected success"),
}
}
}