#![allow(clippy::disallowed_methods, clippy::disallowed_types)]
use std::path::Path;
use auths_crypto::{CurveType, TypedSeed, TypedSignerKey};
use serde::{Deserialize, Serialize};
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum WitnessIdentityError {
#[error("witness identity keystore not found at {path}; pass --generate to create one")]
NotFound {
path: String,
},
#[error("witness identity keystore at {path} is corrupt: {reason}")]
Corrupt {
path: String,
reason: String,
},
#[error("witness identity keystore already exists at {path}; refusing to overwrite")]
AlreadyExists {
path: String,
},
#[error("witness identity keystore I/O at {path}: {source}")]
Io {
path: String,
#[source]
source: std::io::Error,
},
#[error("invalid witness signing key: {0}")]
InvalidKey(String),
#[error("unknown curve tag {0:?}; expected \"ed25519\" or \"p256\"")]
UnknownCurve(String),
}
#[derive(Serialize, Deserialize)]
struct WitnessIdentityFile {
curve: String,
seed: String,
}
fn curve_tag(curve: CurveType) -> &'static str {
match curve {
CurveType::Ed25519 => "ed25519",
CurveType::P256 => "p256",
}
}
fn parse_curve(tag: &str) -> Result<CurveType, WitnessIdentityError> {
match tag {
"ed25519" => Ok(CurveType::Ed25519),
"p256" => Ok(CurveType::P256),
other => Err(WitnessIdentityError::UnknownCurve(other.to_string())),
}
}
fn signer_from_seed(
curve: CurveType,
seed_bytes: [u8; 32],
) -> Result<TypedSignerKey, WitnessIdentityError> {
let seed = match curve {
CurveType::Ed25519 => TypedSeed::Ed25519(seed_bytes),
CurveType::P256 => TypedSeed::P256(seed_bytes),
};
TypedSignerKey::from_seed(seed).map_err(|e| WitnessIdentityError::InvalidKey(e.to_string()))
}
fn decode_seed(
seed_hex: &str,
on_bad: impl Fn(String) -> WitnessIdentityError,
) -> Result<[u8; 32], WitnessIdentityError> {
let bytes = hex::decode(seed_hex.trim()).map_err(|e| on_bad(format!("seed not hex: {e}")))?;
bytes
.as_slice()
.try_into()
.map_err(|_| on_bad(format!("seed must be 32 bytes, got {}", bytes.len())))
}
pub fn load_witness_signer(path: &Path) -> Result<TypedSignerKey, WitnessIdentityError> {
let p = path.display().to_string();
if !path.exists() {
return Err(WitnessIdentityError::NotFound { path: p });
}
let bytes = std::fs::read(path).map_err(|e| WitnessIdentityError::Io {
path: p.clone(),
source: e,
})?;
let file: WitnessIdentityFile =
serde_json::from_slice(&bytes).map_err(|e| WitnessIdentityError::Corrupt {
path: p.clone(),
reason: e.to_string(),
})?;
let curve = parse_curve(&file.curve)?;
let seed_bytes = decode_seed(&file.seed, |reason| WitnessIdentityError::Corrupt {
path: p.clone(),
reason,
})?;
signer_from_seed(curve, seed_bytes)
}
pub fn generate_and_persist_witness_signer(
path: &Path,
curve: CurveType,
) -> Result<TypedSignerKey, WitnessIdentityError> {
let p = path.display().to_string();
if path.exists() {
return Err(WitnessIdentityError::AlreadyExists { path: p });
}
let (seed_bytes, _pubkey) = super::server::generate_keypair_for_curve(curve)
.map_err(|e| WitnessIdentityError::InvalidKey(e.to_string()))?;
let signer = signer_from_seed(curve, seed_bytes)?;
let file = WitnessIdentityFile {
curve: curve_tag(curve).to_string(),
seed: hex::encode(seed_bytes),
};
let json = serde_json::to_vec_pretty(&file).map_err(|e| WitnessIdentityError::Corrupt {
path: p.clone(),
reason: e.to_string(),
})?;
if let Some(parent) = path.parent()
&& !parent.as_os_str().is_empty()
{
std::fs::create_dir_all(parent).map_err(|e| WitnessIdentityError::Io {
path: p.clone(),
source: e,
})?;
}
write_secret_file(path, &json).map_err(|e| WitnessIdentityError::Io { path: p, source: e })?;
Ok(signer)
}
pub fn witness_signer_from_seed_hex(
curve: CurveType,
seed_hex: &str,
) -> Result<TypedSignerKey, WitnessIdentityError> {
let seed_bytes = decode_seed(seed_hex, WitnessIdentityError::InvalidKey)?;
signer_from_seed(curve, seed_bytes)
}
fn write_secret_file(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
use std::io::Write;
let mut opts = std::fs::OpenOptions::new();
opts.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
opts.mode(0o600);
}
let mut f = opts.open(path)?;
f.write_all(bytes)
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
fn run_roundtrip(curve: CurveType) {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("witness-identity.json");
let generated = generate_and_persist_witness_signer(&path, curve).unwrap();
let loaded = load_witness_signer(&path).unwrap();
assert_eq!(generated.curve(), loaded.curve());
assert_eq!(generated.public_key(), loaded.public_key());
}
#[test]
fn roundtrip_ed25519_is_stable() {
run_roundtrip(CurveType::Ed25519);
}
#[test]
fn roundtrip_p256_is_stable() {
run_roundtrip(CurveType::P256);
}
#[test]
fn missing_keystore_fails_closed() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("absent.json");
assert!(matches!(
load_witness_signer(&path),
Err(WitnessIdentityError::NotFound { .. })
));
}
#[test]
fn corrupt_keystore_is_hard_error() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("corrupt.json");
std::fs::write(&path, b"{ not valid json").unwrap();
assert!(matches!(
load_witness_signer(&path),
Err(WitnessIdentityError::Corrupt { .. })
));
}
#[test]
fn generate_refuses_to_overwrite() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("witness-identity.json");
generate_and_persist_witness_signer(&path, CurveType::P256).unwrap();
assert!(matches!(
generate_and_persist_witness_signer(&path, CurveType::P256),
Err(WitnessIdentityError::AlreadyExists { .. })
));
}
#[cfg(unix)]
#[test]
fn persisted_key_is_0600() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("witness-identity.json");
generate_and_persist_witness_signer(&path, CurveType::P256).unwrap();
let mode = std::fs::metadata(&path).unwrap().permissions().mode();
assert_eq!(mode & 0o777, 0o600, "keystore must be owner-only");
}
#[test]
fn env_seed_matches_file_seed() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("witness-identity.json");
let from_file = generate_and_persist_witness_signer(&path, CurveType::Ed25519).unwrap();
let bytes = std::fs::read(&path).unwrap();
let file: WitnessIdentityFile = serde_json::from_slice(&bytes).unwrap();
let from_env = witness_signer_from_seed_hex(CurveType::Ed25519, &file.seed).unwrap();
assert_eq!(from_file.public_key(), from_env.public_key());
}
}