update job to include signatures
This commit is contained in:
96
src/job.rs
96
src/job.rs
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user