update job to include signatures

This commit is contained in:
Timur Gordon
2025-10-24 10:44:36 +02:00
parent 2e19ed09df
commit 90754cc4ac
5 changed files with 526 additions and 1 deletions

View File

@@ -7,6 +7,15 @@ use log::{error};
pub use crate::client::Client;
/// Signature for a job - contains the signatory's public key and their signature
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JobSignature {
/// Public key of the signatory (hex-encoded secp256k1 public key)
pub public_key: String,
/// Signature (hex-encoded secp256k1 signature)
pub signature: String,
}
/// Job status enumeration
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum JobStatus {
@@ -59,6 +68,9 @@ pub struct Job {
pub env_vars: HashMap<String, String>, // environment variables for script execution
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
/// Signatures from authorized signatories (public keys are included in each signature)
pub signatures: Vec<JobSignature>,
}
/// Error types for job operations
@@ -76,6 +88,10 @@ pub enum JobError {
Timeout(String),
#[error("Invalid job data: {0}")]
InvalidData(String),
#[error("Signature verification failed: {0}")]
SignatureVerificationFailed(String),
#[error("Unauthorized: {0}")]
Unauthorized(String),
}
impl Job {
@@ -99,8 +115,88 @@ impl Job {
env_vars: HashMap::new(),
created_at: now,
updated_at: now,
signatures: Vec::new(),
}
}
/// Get the canonical representation of the job for signing
/// This creates a deterministic string representation that can be hashed and signed
/// Note: Signatures are excluded from the canonical representation
pub fn canonical_representation(&self) -> String {
// Create a deterministic representation excluding signatures
// Sort env_vars keys for deterministic ordering
let mut env_vars_sorted: Vec<_> = self.env_vars.iter().collect();
env_vars_sorted.sort_by_key(|&(k, _)| k);
format!(
"{}:{}:{}:{}:{}:{}:{}:{:?}",
self.id,
self.caller_id,
self.context_id,
self.payload,
self.runner,
self.executor,
self.timeout,
env_vars_sorted
)
}
/// Get list of signatory public keys from signatures
pub fn signatories(&self) -> Vec<String> {
self.signatures.iter()
.map(|sig| sig.public_key.clone())
.collect()
}
/// Verify that all signatures are valid
/// Returns Ok(()) if verification passes, Err otherwise
#[cfg(feature = "crypto")]
pub fn verify_signatures(&self) -> Result<(), JobError> {
use secp256k1::{Message, PublicKey, Secp256k1, ecdsa::Signature};
use sha2::{Sha256, Digest};
if self.signatures.is_empty() {
return Err(JobError::Unauthorized("No signatures provided".to_string()));
}
// Get the canonical representation and hash it
let canonical = self.canonical_representation();
let mut hasher = Sha256::new();
hasher.update(canonical.as_bytes());
let hash = hasher.finalize();
let secp = Secp256k1::verification_only();
let message = Message::from_digest_slice(&hash)
.map_err(|e| JobError::SignatureVerificationFailed(format!("Invalid message: {}", e)))?;
// Verify each signature
for sig_data in &self.signatures {
// Decode public key
let pubkey_bytes = hex::decode(&sig_data.public_key)
.map_err(|e| JobError::SignatureVerificationFailed(format!("Invalid public key hex: {}", e)))?;
let pubkey = PublicKey::from_slice(&pubkey_bytes)
.map_err(|e| JobError::SignatureVerificationFailed(format!("Invalid public key: {}", e)))?;
// Decode signature
let sig_bytes = hex::decode(&sig_data.signature)
.map_err(|e| JobError::SignatureVerificationFailed(format!("Invalid signature hex: {}", e)))?;
let signature = Signature::from_compact(&sig_bytes)
.map_err(|e| JobError::SignatureVerificationFailed(format!("Invalid signature: {}", e)))?;
// Verify signature
secp.verify_ecdsa(&message, &signature, &pubkey)
.map_err(|e| JobError::SignatureVerificationFailed(format!("Signature verification failed: {}", e)))?;
}
Ok(())
}
/// Verify signatures (no-op when crypto feature is disabled)
#[cfg(not(feature = "crypto"))]
pub fn verify_signatures(&self) -> Result<(), JobError> {
log::warn!("Signature verification disabled - crypto feature not enabled");
Ok(())
}
}
/// Builder for constructing job execution requests.