use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use thiserror::Error;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct VerificationResult {
pub success: bool,
pub message: String,
pub schema_hash: Option<String>,
pub public_key_url: Option<String>,
pub signature: Option<SignatureInfo>,
pub metadata: Option<HashMap<String, serde_json::Value>>,
pub timestamp: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SignatureInfo {
pub algorithm: String,
pub signature: String,
pub key_fingerprint: Option<String>,
pub valid: bool,
}
#[derive(Error, Debug, Clone)]
pub enum SchemaPinError {
#[error("CLI execution failed: {reason}")]
ExecutionFailed { reason: String },
#[error("Binary not found at path: {path}")]
BinaryNotFound { path: String },
#[error("Invalid arguments: {args:?}")]
InvalidArguments { args: Vec<String> },
#[error("JSON parsing failed: {reason}")]
JsonParsingFailed { reason: String },
#[error("Verification failed: {reason}")]
VerificationFailed { reason: String },
#[error("Timeout occurred after {seconds} seconds")]
Timeout { seconds: u64 },
#[error("IO error: {reason}")]
IoError { reason: String },
#[error("Schema file not found: {path}")]
SchemaFileNotFound { path: String },
#[error("Public key URL invalid: {url}")]
InvalidPublicKeyUrl { url: String },
#[error("Signing failed: {reason}")]
SigningFailed { reason: String },
#[error("Private key file not found: {path}")]
PrivateKeyNotFound { path: String },
#[error("Invalid private key format: {reason}")]
InvalidPrivateKey { reason: String },
}
#[derive(Debug, Clone)]
pub struct SchemaPinConfig {
pub binary_path: String,
pub timeout_seconds: u64,
pub capture_stderr: bool,
pub environment: HashMap<String, String>,
}
impl Default for SchemaPinConfig {
fn default() -> Self {
Self {
binary_path: "/home/jascha/Documents/repos/SchemaPin/go/bin/schemapin-cli".to_string(),
timeout_seconds: 30,
capture_stderr: true,
environment: HashMap::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct VerifyArgs {
pub schema_path: String,
pub public_key_url: String,
pub additional_args: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct SignArgs {
pub schema_path: String,
pub private_key_path: String,
pub output_path: Option<String>,
pub additional_args: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SigningResult {
pub success: bool,
pub message: String,
pub schema_hash: Option<String>,
pub signed_schema_path: Option<String>,
pub signature: Option<SignatureInfo>,
pub metadata: Option<HashMap<String, serde_json::Value>>,
pub timestamp: Option<String>,
}
impl VerifyArgs {
pub fn new(schema_path: String, public_key_url: String) -> Self {
Self {
schema_path,
public_key_url,
additional_args: Vec::new(),
}
}
pub fn with_arg(mut self, arg: String) -> Self {
self.additional_args.push(arg);
self
}
pub fn to_args(&self) -> Vec<String> {
let mut args = vec![
"verify".to_string(),
"--schema".to_string(),
self.schema_path.clone(),
"--public-key-url".to_string(),
self.public_key_url.clone(),
];
args.extend(self.additional_args.clone());
args
}
}
impl SignArgs {
pub fn new(schema_path: String, private_key_path: String) -> Self {
Self {
schema_path,
private_key_path,
output_path: None,
additional_args: Vec::new(),
}
}
pub fn with_output_path(mut self, output_path: String) -> Self {
self.output_path = Some(output_path);
self
}
pub fn with_arg(mut self, arg: String) -> Self {
self.additional_args.push(arg);
self
}
pub fn to_args(&self) -> Vec<String> {
let mut args = vec![
"sign".to_string(),
"--schema".to_string(),
self.schema_path.clone(),
"--private-key".to_string(),
self.private_key_path.clone(),
];
if let Some(ref output_path) = self.output_path {
args.push("--output".to_string());
args.push(output_path.clone());
}
args.extend(self.additional_args.clone());
args
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PinnedKey {
pub identifier: String,
pub public_key: String,
pub algorithm: String,
pub fingerprint: String,
pub pinned_at: String,
pub metadata: Option<HashMap<String, serde_json::Value>>,
}
impl PinnedKey {
pub fn new(
identifier: String,
public_key: String,
algorithm: String,
fingerprint: String,
) -> Self {
Self {
identifier,
public_key,
algorithm,
fingerprint,
pinned_at: chrono::Utc::now().to_rfc3339(),
metadata: None,
}
}
pub fn with_metadata(
identifier: String,
public_key: String,
algorithm: String,
fingerprint: String,
metadata: HashMap<String, serde_json::Value>,
) -> Self {
Self {
identifier,
public_key,
algorithm,
fingerprint,
pinned_at: chrono::Utc::now().to_rfc3339(),
metadata: Some(metadata),
}
}
}
#[derive(Debug, Clone)]
pub struct KeyStoreConfig {
pub store_path: PathBuf,
pub create_if_missing: bool,
pub file_permissions: Option<u32>,
}
impl Default for KeyStoreConfig {
fn default() -> Self {
let mut store_path = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
store_path.push(".symbiont");
store_path.push("schemapin_keys.json");
Self {
store_path,
create_if_missing: true,
file_permissions: Some(0o600), }
}
}
#[derive(Error, Debug, Clone)]
pub enum KeyStoreError {
#[error("Key store file not found: {path}")]
StoreFileNotFound { path: String },
#[error("Failed to read key store: {reason}")]
ReadFailed { reason: String },
#[error("Failed to write key store: {reason}")]
WriteFailed { reason: String },
#[error("Key not found for identifier: {identifier}")]
KeyNotFound { identifier: String },
#[error("Key already pinned for identifier: {identifier}")]
KeyAlreadyPinned { identifier: String },
#[error("Key mismatch for identifier: {identifier}")]
KeyMismatch { identifier: String },
#[error("Invalid key format: {reason}")]
InvalidKeyFormat { reason: String },
#[error("Serialization failed: {reason}")]
SerializationFailed { reason: String },
#[error("IO error: {reason}")]
IoError { reason: String },
#[error("Permission denied: {reason}")]
PermissionDenied { reason: String },
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_verify_args_creation() {
let args = VerifyArgs::new(
"/path/to/schema.json".to_string(),
"https://example.com/pubkey".to_string(),
);
assert_eq!(args.schema_path, "/path/to/schema.json");
assert_eq!(args.public_key_url, "https://example.com/pubkey");
assert!(args.additional_args.is_empty());
}
#[test]
fn test_verify_args_with_additional() {
let args = VerifyArgs::new(
"/path/to/schema.json".to_string(),
"https://example.com/pubkey".to_string(),
)
.with_arg("--verbose".to_string())
.with_arg("--format=json".to_string());
assert_eq!(args.additional_args.len(), 2);
assert_eq!(args.additional_args[0], "--verbose");
assert_eq!(args.additional_args[1], "--format=json");
}
#[test]
fn test_verify_args_to_args() {
let args = VerifyArgs::new(
"/path/to/schema.json".to_string(),
"https://example.com/pubkey".to_string(),
)
.with_arg("--verbose".to_string());
let cmd_args = args.to_args();
assert_eq!(
cmd_args,
vec![
"verify",
"--schema",
"/path/to/schema.json",
"--public-key-url",
"https://example.com/pubkey",
"--verbose"
]
);
}
#[test]
fn test_default_config() {
let config = SchemaPinConfig::default();
assert_eq!(
config.binary_path,
"/home/jascha/Documents/repos/SchemaPin/go/bin/schemapin-cli"
);
assert_eq!(config.timeout_seconds, 30);
assert!(config.capture_stderr);
assert!(config.environment.is_empty());
}
#[test]
fn test_sign_args_creation() {
let args = SignArgs::new(
"/path/to/schema.json".to_string(),
"/path/to/private.key".to_string(),
);
assert_eq!(args.schema_path, "/path/to/schema.json");
assert_eq!(args.private_key_path, "/path/to/private.key");
assert!(args.output_path.is_none());
assert!(args.additional_args.is_empty());
}
#[test]
fn test_sign_args_with_output() {
let args = SignArgs::new(
"/path/to/schema.json".to_string(),
"/path/to/private.key".to_string(),
)
.with_output_path("/path/to/signed_schema.json".to_string())
.with_arg("--verbose".to_string());
assert_eq!(
args.output_path,
Some("/path/to/signed_schema.json".to_string())
);
assert_eq!(args.additional_args.len(), 1);
assert_eq!(args.additional_args[0], "--verbose");
}
#[test]
fn test_sign_args_to_args() {
let args = SignArgs::new(
"/path/to/schema.json".to_string(),
"/path/to/private.key".to_string(),
)
.with_output_path("/path/to/signed.json".to_string())
.with_arg("--format=json".to_string());
let cmd_args = args.to_args();
assert_eq!(
cmd_args,
vec![
"sign",
"--schema",
"/path/to/schema.json",
"--private-key",
"/path/to/private.key",
"--output",
"/path/to/signed.json",
"--format=json"
]
);
}
}