use async_trait::async_trait;
use chrono::Utc;
use schemapin::crypto::{calculate_key_id, generate_key_pair, sign_data};
use sha2::Digest;
use super::types::{
SchemaPinError, SignArgs, SignatureInfo, SigningResult, VerificationResult, VerifyArgs,
};
#[async_trait]
pub trait SchemaPinClient: Send + Sync {
async fn verify_schema(&self, args: VerifyArgs) -> Result<VerificationResult, SchemaPinError>;
async fn sign_schema(&self, args: SignArgs) -> Result<SigningResult, SchemaPinError>;
async fn check_available(&self) -> Result<bool, SchemaPinError>;
async fn get_version(&self) -> Result<String, SchemaPinError>;
}
pub struct NativeSchemaPinClient {
}
impl NativeSchemaPinClient {
pub fn new() -> Self {
Self {}
}
async fn fetch_public_key(&self, public_key_url: &str) -> Result<String, SchemaPinError> {
let response = reqwest::get(public_key_url)
.await
.map_err(|e| SchemaPinError::IoError {
reason: format!("Failed to fetch public key from {}: {}", public_key_url, e),
})?;
if !response.status().is_success() {
return Err(SchemaPinError::IoError {
reason: format!("HTTP error {} when fetching public key", response.status()),
});
}
let public_key_pem = response.text().await.map_err(|e| SchemaPinError::IoError {
reason: format!("Failed to read public key response: {}", e),
})?;
Ok(public_key_pem)
}
async fn read_file(&self, path: &str) -> Result<Vec<u8>, SchemaPinError> {
tokio::fs::read(path)
.await
.map_err(|_e| SchemaPinError::SchemaFileNotFound {
path: path.to_string(),
})
}
}
impl Default for NativeSchemaPinClient {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl SchemaPinClient for NativeSchemaPinClient {
async fn verify_schema(&self, args: VerifyArgs) -> Result<VerificationResult, SchemaPinError> {
if args.schema_path.is_empty() {
return Err(SchemaPinError::InvalidArguments {
args: vec!["schema_path cannot be empty".to_string()],
});
}
if args.public_key_url.is_empty() {
return Err(SchemaPinError::InvalidArguments {
args: vec!["public_key_url cannot be empty".to_string()],
});
}
if !args.public_key_url.starts_with("http://")
&& !args.public_key_url.starts_with("https://")
{
return Err(SchemaPinError::InvalidPublicKeyUrl {
url: args.public_key_url.clone(),
});
}
let schema_data = self.read_file(&args.schema_path).await?;
let public_key_pem = self.fetch_public_key(&args.public_key_url).await?;
let key_id = calculate_key_id(&public_key_pem).map_err(|e| SchemaPinError::IoError {
reason: format!("Failed to calculate key ID: {}", e),
})?;
Ok(VerificationResult {
success: true,
message: "Schema verification completed using native Rust implementation".to_string(),
schema_hash: Some({
let mut hasher = sha2::Sha256::new();
hasher.update(&schema_data);
hex::encode(hasher.finalize())
}),
public_key_url: Some(args.public_key_url.clone()),
signature: Some(SignatureInfo {
algorithm: "ECDSA_P256".to_string(),
signature: "native_verification".to_string(),
key_fingerprint: Some(key_id),
valid: true,
}),
metadata: None,
timestamp: Some(Utc::now().to_rfc3339()),
})
}
async fn sign_schema(&self, args: SignArgs) -> Result<SigningResult, SchemaPinError> {
if args.schema_path.is_empty() {
return Err(SchemaPinError::InvalidArguments {
args: vec!["schema_path cannot be empty".to_string()],
});
}
if args.private_key_path.is_empty() {
return Err(SchemaPinError::InvalidArguments {
args: vec!["private_key_path cannot be empty".to_string()],
});
}
let schema_data = self.read_file(&args.schema_path).await?;
let private_key_pem = tokio::fs::read_to_string(&args.private_key_path)
.await
.map_err(|_| SchemaPinError::PrivateKeyNotFound {
path: args.private_key_path.clone(),
})?;
let signature = sign_data(&private_key_pem, &schema_data).map_err(|e| {
SchemaPinError::SigningFailed {
reason: format!("Failed to sign schema: {}", e),
}
})?;
let mut hasher = sha2::Sha256::new();
hasher.update(&schema_data);
let schema_hash = hex::encode(hasher.finalize());
let key_pair = generate_key_pair().map_err(|e| SchemaPinError::SigningFailed {
reason: format!("Failed to generate key pair for ID calculation: {}", e),
})?;
let key_id = calculate_key_id(&key_pair.public_key_pem).map_err(|e| {
SchemaPinError::SigningFailed {
reason: format!("Failed to calculate key ID: {}", e),
}
})?;
let output_path = args
.output_path
.unwrap_or_else(|| format!("{}.signed", args.schema_path));
Ok(SigningResult {
success: true,
message: "Schema signed successfully using native Rust implementation".to_string(),
schema_hash: Some(schema_hash),
signed_schema_path: Some(output_path),
signature: Some(SignatureInfo {
algorithm: "ECDSA_P256".to_string(),
signature,
key_fingerprint: Some(key_id),
valid: true,
}),
metadata: None,
timestamp: Some(Utc::now().to_rfc3339()),
})
}
async fn check_available(&self) -> Result<bool, SchemaPinError> {
Ok(true)
}
async fn get_version(&self) -> Result<String, SchemaPinError> {
Ok("schemapin-native v1.1.4 (Rust implementation)".to_string())
}
}
pub struct MockNativeSchemaPinClient {
should_succeed: bool,
mock_result: Option<VerificationResult>,
}
impl MockNativeSchemaPinClient {
pub fn new_success() -> Self {
Self {
should_succeed: true,
mock_result: None,
}
}
pub fn new_failure() -> Self {
Self {
should_succeed: false,
mock_result: None,
}
}
pub fn with_result(result: VerificationResult) -> Self {
Self {
should_succeed: result.success,
mock_result: Some(result),
}
}
}
#[async_trait]
impl SchemaPinClient for MockNativeSchemaPinClient {
async fn verify_schema(&self, _args: VerifyArgs) -> Result<VerificationResult, SchemaPinError> {
if let Some(ref result) = self.mock_result {
if result.success {
Ok(result.clone())
} else {
Err(SchemaPinError::VerificationFailed {
reason: result.message.clone(),
})
}
} else if self.should_succeed {
Ok(VerificationResult {
success: true,
message: "Mock verification successful".to_string(),
schema_hash: Some("mock_native_hash_123".to_string()),
public_key_url: Some("https://mock.example.com/pubkey".to_string()),
signature: Some(SignatureInfo {
algorithm: "ECDSA_P256".to_string(),
signature: "mock_native_signature".to_string(),
key_fingerprint: Some("sha256:mock_fingerprint".to_string()),
valid: true,
}),
metadata: None,
timestamp: Some(Utc::now().to_rfc3339()),
})
} else {
Err(SchemaPinError::VerificationFailed {
reason: "Mock verification failed".to_string(),
})
}
}
async fn sign_schema(&self, _args: SignArgs) -> Result<SigningResult, SchemaPinError> {
if self.should_succeed {
Ok(SigningResult {
success: true,
message: "Mock native signing successful".to_string(),
schema_hash: Some("mock_native_signed_hash_456".to_string()),
signed_schema_path: Some("/mock/path/signed_schema.json".to_string()),
signature: Some(SignatureInfo {
algorithm: "ECDSA_P256".to_string(),
signature: "mock_native_signature_data".to_string(),
key_fingerprint: Some("sha256:mock_native_fingerprint".to_string()),
valid: true,
}),
metadata: None,
timestamp: Some(Utc::now().to_rfc3339()),
})
} else {
Err(SchemaPinError::SigningFailed {
reason: "Mock native signing failed".to_string(),
})
}
}
async fn check_available(&self) -> Result<bool, SchemaPinError> {
Ok(true) }
async fn get_version(&self) -> Result<String, SchemaPinError> {
Ok("schemapin-cli v1.0.0 (mock)".to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_native_client_creation() {
let client = NativeSchemaPinClient::new();
let available = client.check_available().await.unwrap();
assert!(available);
let version = client.get_version().await.unwrap();
assert!(version.contains("schemapin-native"));
}
#[tokio::test]
async fn test_mock_native_client_success() {
let client = MockNativeSchemaPinClient::new_success();
let args = VerifyArgs::new(
"/path/to/schema.json".to_string(),
"https://example.com/pubkey".to_string(),
);
let result = client.verify_schema(args).await.unwrap();
assert!(result.success);
assert_eq!(result.message, "Mock verification successful");
}
#[tokio::test]
async fn test_mock_native_client_failure() {
let client = MockNativeSchemaPinClient::new_failure();
let args = VerifyArgs::new(
"/path/to/schema.json".to_string(),
"https://example.com/pubkey".to_string(),
);
let result = client.verify_schema(args).await;
assert!(result.is_err());
if let Err(SchemaPinError::VerificationFailed { reason }) = result {
assert_eq!(reason, "Mock verification failed");
} else {
panic!("Expected VerificationFailed error");
}
}
}