autoschematic-core 0.13.0

Core shared functionality for Autoschematic: workflow engine, state management, and Git integrations
Documentation
use anyhow::{Result, anyhow};
use base64::Engine;
use base64::prelude::BASE64_STANDARD;
use elliptic_curve::SecretKey;
use k256::Secp256k1;
use k256::ecdsa::{Signature, SigningKey};
use k256::pkcs8::DecodePrivateKey;
use k256::pkcs8::der::pem;
use rand_core::OsRng;
use signature::SignerMut;
use std::fs;
use std::path::PathBuf;

use crate::keystore::KeyStore;

#[derive(Debug)]
pub struct OndiskKeyStore {
    key_dir: PathBuf,
}

// TODO could make this generic over curve types?
/// This KeyStore implementation uses on-disk elliptic curve keypairs stored in the .pem format.
/// It is currently hardcoded to Secp256k1.
impl OndiskKeyStore {
    fn key_path(&self, id: &str) -> PathBuf {
        self.key_dir.join(id)
    }
}

impl KeyStore for OndiskKeyStore {
    fn new(path: &str) -> Result<Self> {
        let key_dir = PathBuf::from(path);
        if !key_dir.exists() {
            return Err(anyhow!("OndiskKeystore failed to init: No key store found at {}", path));
        }
        let keystore = OndiskKeyStore { key_dir };

        if let Ok(keys) = keystore.list()
            && keys.is_empty()
        {
            keystore.create_keypair("main")?;
        }

        Ok(keystore)
    }

    fn list(&self) -> Result<Vec<String>> {
        let mut key_ids: Vec<String> = Vec::new();
        for entry in fs::read_dir(&self.key_dir)? {
            let entry = entry?;
            if entry.file_type()?.is_file()
                && let Some(file_name) = entry.file_name().to_str()
            {
                let Ok(pem) = fs::read_to_string(entry.path()) else {
                    tracing::error!("Couldn't read key at {}", file_name);
                    continue;
                };
                let Ok(_) = SecretKey::<Secp256k1>::from_sec1_pem(&pem) else {
                    tracing::error!("Couldn't parse key at {}", file_name);
                    continue;
                };
                key_ids.push(file_name.to_string());
            }
        }
        Ok(key_ids)
    }

    fn sign(&self, id: &str, payload: &str) -> Result<String> {
        let key_path = self.key_path(id);
        let private_key_bytes = fs::read(key_path)?;
        let mut signing_key = SigningKey::from_pkcs8_der(&private_key_bytes)?;
        let signature: Signature = signing_key.sign(payload.as_bytes());
        Ok(hex::encode(signature.to_bytes()))
    }

    fn get_public_key(&self, id: &str) -> Result<String> {
        let key_path = self.key_path(id);
        let pem = fs::read_to_string(key_path)?;
        let secret_key = SecretKey::<Secp256k1>::from_sec1_pem(&pem)?;
        let pub_bytes = secret_key.public_key().to_sec1_bytes();
        Ok(BASE64_STANDARD.encode(pub_bytes))
    }

    fn get_private_key(&self, id: &str) -> Result<String> {
        let key_path = self.key_path(id);
        let pem = fs::read_to_string(key_path)?;
        let secret_key = SecretKey::<Secp256k1>::from_sec1_pem(&pem)?;
        let pub_bytes = secret_key.to_bytes();
        Ok(BASE64_STANDARD.encode(pub_bytes))
    }

    fn create_keypair(&self, id: &str) -> Result<()> {
        // let mut rng = OsRng;
        let secret = SecretKey::<Secp256k1>::random(&mut OsRng);
        // let secret = EphemeralSecret::random(&mut OsRng);
        let pem = secret.to_sec1_pem(pem::LineEnding::LF)?;
        let out_path = self.key_dir.join(format!("{id}.pem"));
        fs::write(out_path, pem)?;

        Ok(())
    }

    fn delete_keypair(&self, id: &str) -> Result<()> {
        let out_path = self.key_dir.join(format!("{id}.pem"));
        if out_path.is_file() {
            fs::remove_file(out_path)?;
        }
        Ok(())
    }
}