use std::fmt;
use std::fs;
use std::io;
use std::path::Path;
use a7n_platform::fs::write_user_only;
use a7n_proto::{MachineId, Pin, ProtoError};
use rcgen::{
CertificateParams, DistinguishedName, DnType, KeyPair, PublicKeyData, SerialNumber, SigningKey,
date_time_ymd,
};
use sha2::{Digest, Sha256};
use x509_parser::prelude::{FromDer, SubjectPublicKeyInfo, X509Certificate};
const ED25519_OID: &str = "1.3.101.112";
const KEY_FILE: &str = "identity.key";
const CERT_FILE: &str = "identity.crt";
const SUBJECT_COMMON_NAME: &str = "apeirogon";
const CERT_SERIAL: u64 = 1;
const NOT_BEFORE: (i32, u8, u8) = (2026, 1, 1);
const NOT_AFTER: (i32, u8, u8) = (2126, 1, 1);
#[derive(Debug, thiserror::Error)]
pub(crate) enum IdentityError {
#[error("identity file error: {0}")]
Io(#[from] io::Error),
#[error("key or certificate generation failed: {0}")]
Rcgen(#[from] rcgen::Error),
#[error("certificate is not valid X.509: {0}")]
X509(String),
#[error("stored certificate does not belong to the stored key")]
CertKeyMismatch,
#[error("desk signing requires an Ed25519 identity")]
UnsupportedSigningKey,
#[error(transparent)]
Proto(#[from] ProtoError),
}
pub(crate) struct Identity {
pub(crate) key: KeyPair,
pub(crate) cert_der: Vec<u8>,
pub(crate) machine_id: MachineId,
pub(crate) pin: Pin,
}
impl fmt::Debug for Identity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Identity")
.field("machine_id", &self.machine_id)
.field("pin", &self.pin)
.finish_non_exhaustive()
}
}
fn machine_id_of_spki(spki_der: &[u8]) -> Result<MachineId, IdentityError> {
let digest = Sha256::digest(spki_der);
Ok(MachineId::try_from(&digest[..MachineId::LEN])?)
}
pub(crate) fn machine_id_of(cert_der: &[u8]) -> Result<MachineId, IdentityError> {
let (_, cert) = X509Certificate::from_der(cert_der)
.map_err(|error| IdentityError::X509(error.to_string()))?;
machine_id_of_spki(cert.tbs_certificate.subject_pki.raw)
}
pub(crate) fn pin_of(cert_der: &[u8]) -> Result<Pin, IdentityError> {
Ok(Pin::try_from(&Sha256::digest(cert_der)[..])?)
}
pub(crate) fn spki_of(cert_der: &[u8]) -> Result<Vec<u8>, IdentityError> {
let (_, cert) = X509Certificate::from_der(cert_der)
.map_err(|error| IdentityError::X509(error.to_string()))?;
Ok(cert.tbs_certificate.subject_pki.raw.to_vec())
}
pub(crate) fn sign(identity: &Identity, message: &[u8]) -> Result<Vec<u8>, IdentityError> {
if identity.key.algorithm() != &rcgen::PKCS_ED25519 {
return Err(IdentityError::UnsupportedSigningKey);
}
Ok(identity.key.sign(message)?)
}
pub(crate) fn verify(spki_der: &[u8], message: &[u8], signature: &[u8]) -> bool {
let Ok((remaining, spki)) = SubjectPublicKeyInfo::from_der(spki_der) else {
return false;
};
if !remaining.is_empty()
|| spki.algorithm.algorithm.to_id_string() != ED25519_OID
|| spki.algorithm.parameters.is_some()
|| spki.subject_public_key.unused_bits != 0
{
return false;
}
ring::signature::UnparsedPublicKey::new(
&ring::signature::ED25519,
&spki.subject_public_key.data,
)
.verify(message, signature)
.is_ok()
}
pub(crate) fn issue_certificate(key: &KeyPair) -> Result<Vec<u8>, IdentityError> {
let mut params = CertificateParams::new(Vec::new())?;
let mut name = DistinguishedName::new();
name.push(DnType::CommonName, SUBJECT_COMMON_NAME);
params.distinguished_name = name;
params.serial_number = Some(SerialNumber::from(CERT_SERIAL));
params.not_before = date_time_ymd(NOT_BEFORE.0, NOT_BEFORE.1, NOT_BEFORE.2);
params.not_after = date_time_ymd(NOT_AFTER.0, NOT_AFTER.1, NOT_AFTER.2);
Ok(params.self_signed(key)?.der().to_vec())
}
pub(crate) fn load_or_create(dir: &Path) -> Result<Identity, IdentityError> {
let key = load_or_create_key(&dir.join(KEY_FILE))?;
let cert_der = load_or_issue_certificate(&dir.join(CERT_FILE), &key)?;
let machine_id = machine_id_of(&cert_der)?;
if machine_id != machine_id_of_spki(&key.subject_public_key_info())? {
return Err(IdentityError::CertKeyMismatch);
}
let pin = pin_of(&cert_der)?;
Ok(Identity {
key,
cert_der,
machine_id,
pin,
})
}
fn load_or_create_key(path: &Path) -> Result<KeyPair, IdentityError> {
match fs::read_to_string(path) {
Ok(pem) => Ok(KeyPair::from_pem(&pem)?),
Err(error) if error.kind() == io::ErrorKind::NotFound => {
let key = KeyPair::generate_for(&rcgen::PKCS_ED25519)?;
write_user_only(path, key.serialize_pem().as_bytes())?;
Ok(key)
}
Err(error) => Err(error.into()),
}
}
fn load_or_issue_certificate(path: &Path, key: &KeyPair) -> Result<Vec<u8>, IdentityError> {
match fs::read(path) {
Ok(der) => Ok(der),
Err(error) if error.kind() == io::ErrorKind::NotFound => {
let der = issue_certificate(key)?;
write_user_only(path, &der)?;
Ok(der)
}
Err(error) => Err(error.into()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sign_device_key_verifies_only_correct_key_and_message() {
let dir = tempfile::tempdir().unwrap();
let identity = load_or_create(dir.path()).unwrap();
let other = KeyPair::generate_for(&rcgen::PKCS_ED25519).unwrap();
let p256 = KeyPair::generate().unwrap();
let message = b"desk record";
let signature = sign(&identity, message).unwrap();
assert!(verify(
&identity.key.subject_public_key_info(),
message,
&signature
));
assert!(!verify(
&other.subject_public_key_info(),
message,
&signature
));
assert!(!verify(
&identity.key.subject_public_key_info(),
b"changed",
&signature
));
assert!(!verify(
&p256.subject_public_key_info(),
message,
&signature
));
assert!(!verify(b"bad DER", message, &signature));
}
}