From beada81abf5bc4ab9a1e98372be8a26c32a7d9df Mon Sep 17 00:00:00 2001 From: bordumb Date: Sat, 4 Apr 2026 00:30:27 -0700 Subject: [PATCH 1/2] feat: add signed commit_sha to artifact attestations with CLI flags, DRY canonical_data() method, and Node/Python SDK support --- .../src/commands/artifact/batch_sign.rs | 2 + crates/auths-cli/src/commands/artifact/mod.rs | 58 +++++++++ .../auths-cli/src/commands/artifact/sign.rs | 2 + .../auths-cli/src/commands/artifact/verify.rs | 60 +++++++++ crates/auths-cli/src/commands/git_helpers.rs | 49 ++++++++ crates/auths-cli/src/commands/mod.rs | 1 + crates/auths-cli/src/commands/org.rs | 2 + crates/auths-cli/src/commands/sign.rs | 2 + crates/auths-cli/src/commands/sign_commit.rs | 16 +-- .../auths-cli/src/commands/verify_commit.rs | 12 +- crates/auths-cli/tests/cases/verify.rs | 27 +--- crates/auths-id/src/agent_identity.rs | 1 + crates/auths-id/src/attestation/core.rs | 27 +--- crates/auths-id/src/attestation/create.rs | 93 ++++++-------- crates/auths-id/src/attestation/verify.rs | 26 +--- crates/auths-id/tests/cases/lifecycle.rs | 1 + .../auths-sdk/src/domains/device/service.rs | 2 + .../auths-sdk/src/domains/identity/service.rs | 1 + crates/auths-sdk/src/domains/org/service.rs | 1 + .../auths-sdk/src/domains/signing/service.rs | 45 ++++++- crates/auths-sdk/src/pairing/mod.rs | 1 + crates/auths-sdk/src/signing.rs | 2 +- .../src/workflows/ci/batch_attest.rs | 3 + .../src/workflows/ci/machine_identity.rs | 22 +--- crates/auths-sdk/src/workflows/org.rs | 1 + crates/auths-sdk/tests/cases/artifact.rs | 8 +- crates/auths-verifier/src/core.rs | 37 ++++++ crates/auths-verifier/src/verify.rs | 115 +----------------- .../tests/cases/expiration_skew.rs | 21 +--- .../tests/cases/revocation_adversarial.rs | 21 +--- .../tests/cases/serialization_pinning.rs | 42 +------ docs/sdk/rust/signing-and-verification.md | 1 + packages/auths-node/index.d.ts | 11 +- packages/auths-node/lib/artifacts.ts | 6 + packages/auths-node/lib/native.ts | 6 +- packages/auths-node/src/artifact.rs | 9 +- packages/auths-node/src/org.rs | 1 + packages/auths-python/python/auths/_client.py | 8 +- packages/auths-python/src/artifact_sign.rs | 17 ++- packages/auths-python/src/org.rs | 1 + 40 files changed, 383 insertions(+), 378 deletions(-) create mode 100644 crates/auths-cli/src/commands/git_helpers.rs diff --git a/crates/auths-cli/src/commands/artifact/batch_sign.rs b/crates/auths-cli/src/commands/artifact/batch_sign.rs index 69258d67..e4b99fea 100644 --- a/crates/auths-cli/src/commands/artifact/batch_sign.rs +++ b/crates/auths-cli/src/commands/artifact/batch_sign.rs @@ -38,6 +38,7 @@ pub fn handle_batch_sign( attestation_dir: Option, expires_in: Option, note: Option, + commit_sha: Option, repo_opt: Option, passphrase_provider: Arc, env_config: &EnvironmentConfig, @@ -67,6 +68,7 @@ pub fn handle_batch_sign( identity_key: key.map(|s| s.to_string()), expires_in, note, + commit_sha, }; let result = batch_sign_artifacts(config, &ctx) diff --git a/crates/auths-cli/src/commands/artifact/mod.rs b/crates/auths-cli/src/commands/artifact/mod.rs index b2fcae22..1631662c 100644 --- a/crates/auths-cli/src/commands/artifact/mod.rs +++ b/crates/auths-cli/src/commands/artifact/mod.rs @@ -12,6 +12,7 @@ use std::sync::Arc; use anyhow::{Result, bail}; use auths_core::config::EnvironmentConfig; use auths_core::signing::PassphraseProvider; +use auths_sdk::signing::validate_commit_sha; #[derive(Args, Debug, Clone)] #[command( @@ -73,6 +74,14 @@ pub enum ArtifactSubcommand { /// Optional note to embed in the attestation. #[arg(long)] note: Option, + + /// Git commit SHA to embed in the attestation (auto-detected from HEAD if omitted). + #[arg(long, conflicts_with = "no_commit")] + commit: Option, + + /// Do not embed any commit SHA in the attestation. + #[arg(long, conflicts_with = "commit")] + no_commit: bool, }, /// Sign and publish an artifact attestation to a registry. @@ -110,6 +119,14 @@ pub enum ArtifactSubcommand { /// Optional note to embed in the attestation. #[arg(long)] note: Option, + + /// Git commit SHA to embed in the attestation (auto-detected from HEAD if omitted). + #[arg(long, conflicts_with = "no_commit")] + commit: Option, + + /// Do not embed any commit SHA in the attestation. + #[arg(long, conflicts_with = "commit")] + no_commit: bool, }, /// Sign multiple artifacts matching a glob pattern. @@ -140,6 +157,14 @@ pub enum ArtifactSubcommand { /// Optional note to embed in each attestation. #[arg(long)] note: Option, + + /// Git commit SHA to embed in the attestation (auto-detected from HEAD if omitted). + #[arg(long, conflicts_with = "no_commit")] + commit: Option, + + /// Do not embed any commit SHA in the attestation. + #[arg(long, conflicts_with = "commit")] + no_commit: bool, }, /// Verify an artifact's signature against an Auths identity. @@ -167,9 +192,28 @@ pub enum ArtifactSubcommand { /// Witness quorum threshold (default: 1). #[arg(long, default_value = "1")] witness_threshold: usize, + + /// Also verify the source commit's signing attestation. + #[arg(long)] + verify_commit: bool, }, } +/// Resolve the commit SHA from CLI flags. +fn resolve_commit_sha_from_flags( + commit: Option, + no_commit: bool, +) -> Result> { + if no_commit { + return Ok(None); + } + if let Some(sha) = commit { + let validated = validate_commit_sha(&sha).map_err(|e| anyhow::anyhow!("{}", e))?; + return Ok(Some(validated)); + } + Ok(crate::commands::git_helpers::resolve_head_silent()) +} + /// Handle the `artifact` command dispatch. pub fn handle_artifact( cmd: ArtifactCommand, @@ -185,7 +229,10 @@ pub fn handle_artifact( device_key, expires_in, note, + commit, + no_commit, } => { + let commit_sha = resolve_commit_sha_from_flags(commit, no_commit)?; let resolved_alias = match device_key { Some(alias) => alias, None => crate::commands::key_detect::auto_detect_device_key( @@ -200,6 +247,7 @@ pub fn handle_artifact( &resolved_alias, expires_in, note, + commit_sha, repo_opt, passphrase_provider, env_config, @@ -214,7 +262,10 @@ pub fn handle_artifact( device_key, expires_in, note, + commit, + no_commit, } => { + let commit_sha = resolve_commit_sha_from_flags(commit, no_commit)?; let sig_path = match (signature, file.as_ref()) { (Some(sig), _) => sig, (None, Some(artifact)) => { @@ -236,6 +287,7 @@ pub fn handle_artifact( &resolved_alias, expires_in, note, + commit_sha, repo_opt.clone(), passphrase_provider, env_config, @@ -256,7 +308,10 @@ pub fn handle_artifact( attestation_dir, expires_in, note, + commit, + no_commit, } => { + let commit_sha = resolve_commit_sha_from_flags(commit, no_commit)?; let resolved_alias = match device_key { Some(alias) => alias, None => crate::commands::key_detect::auto_detect_device_key( @@ -271,6 +326,7 @@ pub fn handle_artifact( attestation_dir, expires_in, note, + commit_sha, repo_opt, passphrase_provider, env_config, @@ -283,6 +339,7 @@ pub fn handle_artifact( witness_receipts, witness_keys, witness_threshold, + verify_commit, } => { let rt = tokio::runtime::Runtime::new()?; rt.block_on(verify::handle_verify( @@ -292,6 +349,7 @@ pub fn handle_artifact( witness_receipts, &witness_keys, witness_threshold, + verify_commit, )) } } diff --git a/crates/auths-cli/src/commands/artifact/sign.rs b/crates/auths-cli/src/commands/artifact/sign.rs index e8d3492d..7cc631c9 100644 --- a/crates/auths-cli/src/commands/artifact/sign.rs +++ b/crates/auths-cli/src/commands/artifact/sign.rs @@ -21,6 +21,7 @@ pub fn handle_sign( device_key: &str, expires_in: Option, note: Option, + commit_sha: Option, repo_opt: Option, passphrase_provider: Arc, env_config: &EnvironmentConfig, @@ -35,6 +36,7 @@ pub fn handle_sign( device_key: SigningKeyMaterial::Alias(KeyAlias::new_unchecked(device_key)), expires_in, note, + commit_sha, }; let result = sign_artifact(params, &ctx) diff --git a/crates/auths-cli/src/commands/artifact/verify.rs b/crates/auths-cli/src/commands/artifact/verify.rs index 29e568ad..24d2d1cd 100644 --- a/crates/auths-cli/src/commands/artifact/verify.rs +++ b/crates/auths-cli/src/commands/artifact/verify.rs @@ -37,6 +37,10 @@ struct VerifyArtifactResult { #[serde(skip_serializing_if = "Option::is_none")] issuer: Option, #[serde(skip_serializing_if = "Option::is_none")] + commit_sha: Option, + #[serde(skip_serializing_if = "Option::is_none")] + commit_verified: Option, + #[serde(skip_serializing_if = "Option::is_none")] error: Option, } @@ -50,6 +54,7 @@ pub async fn handle_verify( witness_receipts: Option, witness_keys: &[String], witness_threshold: usize, + verify_commit: bool, ) -> Result<()> { let file_str = file.to_string_lossy().to_string(); @@ -139,6 +144,8 @@ pub async fn handle_verify( capability_valid: None, witness_quorum: None, issuer: Some(attestation.issuer.to_string()), + commit_sha: attestation.commit_sha.clone(), + commit_verified: None, error: Some(format!( "Digest mismatch: file={}, attestation={}", file_digest.hex, artifact_meta.digest.hex @@ -201,6 +208,55 @@ pub async fn handle_verify( valid = false; } + // 8b. Display commit linkage info (always, when present) + let commit_sha_val = attestation.commit_sha.clone(); + if let Some(ref sha) = commit_sha_val + && !is_json_mode() + { + eprintln!(" Commit: {}", sha); + } + + // 8c. Optional commit attestation verification + let commit_verified = if verify_commit { + match &commit_sha_val { + None => { + if !is_json_mode() { + eprintln!( + "warning: artifact attestation has no commit_sha field; \ + re-sign with: auths artifact sign --commit " + ); + } + None + } + Some(sha) => { + // Look up commit attestation via git ref + let commit_ref = format!("refs/auths/commits/{}", sha); + let lookup = std::process::Command::new("git") + .args(["show", &format!("{}:attestation.json", commit_ref)]) + .output(); + match lookup { + Ok(output) if output.status.success() => { + if !is_json_mode() { + eprintln!(" Commit {}: signing attestation found", &sha[..12]); + } + Some(true) + } + _ => { + if !is_json_mode() { + eprintln!( + "warning: no signing attestation found for commit {}", + &sha[..std::cmp::min(sha.len(), 12)] + ); + } + Some(false) + } + } + } + } + } else { + None + }; + let exit_code = if valid { 0 } else { 1 }; output_result( @@ -214,6 +270,8 @@ pub async fn handle_verify( capability_valid, witness_quorum, issuer: Some(identity_did.to_string()), + commit_sha: commit_sha_val, + commit_verified, error: None, }, ) @@ -313,6 +371,8 @@ fn output_error(file: &str, exit_code: i32, message: &str) -> Result<()> { capability_valid: None, witness_quorum: None, issuer: None, + commit_sha: None, + commit_verified: None, error: Some(message.to_string()), }; println!("{}", serde_json::to_string(&result)?); diff --git a/crates/auths-cli/src/commands/git_helpers.rs b/crates/auths-cli/src/commands/git_helpers.rs new file mode 100644 index 00000000..f9421aee --- /dev/null +++ b/crates/auths-cli/src/commands/git_helpers.rs @@ -0,0 +1,49 @@ +use anyhow::{Context, Result, anyhow}; +use std::process::Command; + +/// Resolve a git ref to a full commit SHA. +/// +/// Args: +/// * `commit_ref`: A git ref (branch name, tag, SHA, HEAD, etc.). +/// +/// Usage: +/// ```ignore +/// let sha = resolve_commit_sha("HEAD")?; +/// let sha = resolve_commit_sha("v1.0.0")?; +/// ``` +pub fn resolve_commit_sha(commit_ref: &str) -> Result { + let output = Command::new("git") + .args(["rev-parse", commit_ref]) + .output() + .context("Failed to resolve commit reference")?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(anyhow!( + "Invalid commit reference '{}': {}", + commit_ref, + stderr.trim() + )); + } + + Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) +} + +/// Silently attempt to resolve HEAD to a full commit SHA. +/// +/// Returns `None` if not in a git repo, no commits exist, or git is unavailable. +/// +/// Usage: +/// ```ignore +/// let sha = resolve_head_silent(); // Some("abc123...") or None +/// ``` +pub fn resolve_head_silent() -> Option { + Command::new("git") + .args(["rev-parse", "--verify", "--quiet", "HEAD"]) + .output() + .ok() + .filter(|o| o.status.success()) + .and_then(|o| String::from_utf8(o.stdout).ok()) + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) +} diff --git a/crates/auths-cli/src/commands/mod.rs b/crates/auths-cli/src/commands/mod.rs index 12b60a04..ca777878 100644 --- a/crates/auths-cli/src/commands/mod.rs +++ b/crates/auths-cli/src/commands/mod.rs @@ -17,6 +17,7 @@ pub mod doctor; pub mod emergency; pub mod error_lookup; pub mod git; +pub mod git_helpers; pub mod id; pub mod index; pub mod init; diff --git a/crates/auths-cli/src/commands/org.rs b/crates/auths-cli/src/commands/org.rs index 7d760bf4..7b42d7d9 100644 --- a/crates/auths-cli/src/commands/org.rs +++ b/crates/auths-cli/src/commands/org.rs @@ -381,6 +381,7 @@ pub fn handle_org( admin_capabilities, Some(Role::Admin), None, // Root admin has no delegator + None, // commit_sha ) .context("Failed to create admin attestation")?; @@ -499,6 +500,7 @@ pub fn handle_org( vec![], None, None, + None, // commit_sha ) .context("Failed to create signed attestation object")?; diff --git a/crates/auths-cli/src/commands/sign.rs b/crates/auths-cli/src/commands/sign.rs index ade36945..5858bfd7 100644 --- a/crates/auths-cli/src/commands/sign.rs +++ b/crates/auths-cli/src/commands/sign.rs @@ -152,6 +152,7 @@ pub fn handle_sign_unified( Some(alias) => alias.to_string(), None => super::key_detect::auto_detect_device_key(repo_opt.as_deref(), env_config)?, }; + let commit_sha = super::git_helpers::resolve_head_silent(); handle_artifact_sign( &path, cmd.sig_output, @@ -159,6 +160,7 @@ pub fn handle_sign_unified( &device_key_alias, cmd.expires_in, cmd.note, + commit_sha, repo_opt, passphrase_provider, env_config, diff --git a/crates/auths-cli/src/commands/sign_commit.rs b/crates/auths-cli/src/commands/sign_commit.rs index ad959e3b..7c7e0c6f 100644 --- a/crates/auths-cli/src/commands/sign_commit.rs +++ b/crates/auths-cli/src/commands/sign_commit.rs @@ -86,23 +86,9 @@ fn get_commit_author(commit_sha: &str) -> Result { Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) } -/// Resolve commit reference to full SHA. -fn resolve_commit_sha(commit_ref: &str) -> Result { - let output = Command::new("git") - .args(["rev-parse", commit_ref]) - .output() - .context("Failed to resolve commit reference")?; - - if !output.status.success() { - return Err(anyhow!("Invalid commit reference: {}", commit_ref)); - } - - Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) -} - /// Handle the sign-commit command. pub fn handle_sign_commit(cmd: SignCommitCommand, ctx: &CliConfig) -> Result<()> { - let commit_sha = resolve_commit_sha(&cmd.commit)?; + let commit_sha = super::git_helpers::resolve_commit_sha(&cmd.commit)?; let commit_message = get_commit_message(&commit_sha).ok(); let author = get_commit_author(&commit_sha).ok(); diff --git a/crates/auths-cli/src/commands/verify_commit.rs b/crates/auths-cli/src/commands/verify_commit.rs index 56fa197b..d37c63f9 100644 --- a/crates/auths-cli/src/commands/verify_commit.rs +++ b/crates/auths-cli/src/commands/verify_commit.rs @@ -704,17 +704,7 @@ enum SignatureInfo { } fn resolve_commit_sha(commit_ref: &str) -> Result { - let output = Command::new("git") - .args(["rev-parse", commit_ref]) - .output() - .context("Failed to run git rev-parse")?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - return Err(anyhow!("Invalid commit reference: {}", stderr.trim())); - } - - Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) + super::git_helpers::resolve_commit_sha(commit_ref) } fn get_commit_signature(sha: &str) -> Result { diff --git a/crates/auths-cli/tests/cases/verify.rs b/crates/auths-cli/tests/cases/verify.rs index a1e7bc98..49d34b64 100644 --- a/crates/auths-cli/tests/cases/verify.rs +++ b/crates/auths-cli/tests/cases/verify.rs @@ -2,8 +2,7 @@ use assert_cmd::Command; use auths_crypto::testing::gen_keypair; use auths_verifier::AttestationBuilder; use auths_verifier::core::{ - Attestation, CanonicalAttestationData, Ed25519PublicKey, Ed25519Signature, - canonicalize_attestation_data, + Attestation, Ed25519PublicKey, Ed25519Signature, canonicalize_attestation_data, }; use auths_verifier::types::{CanonicalDid, DeviceDID}; use chrono::{Duration, Utc}; @@ -30,28 +29,8 @@ fn create_signed_attestation( .timestamp(Some(Utc::now())) .build(); - // Create canonical data for signing (includes org fields) - let data = CanonicalAttestationData { - version: att.version, - rid: &att.rid, - issuer: &att.issuer, - subject: &att.subject, - device_public_key: att.device_public_key.as_bytes(), - payload: &att.payload, - timestamp: &att.timestamp, - expires_at: &att.expires_at, - revoked_at: &att.revoked_at, - note: &att.note, - role: att.role.as_ref().map(|r| r.as_str()), - capabilities: if att.capabilities.is_empty() { - None - } else { - Some(&att.capabilities) - }, - delegated_by: att.delegated_by.as_ref(), - signer_type: att.signer_type.as_ref(), - }; - let canonical_bytes = canonicalize_attestation_data(&data).unwrap(); + // Create canonical data for signing (single source of truth via canonical_data()) + let canonical_bytes = canonicalize_attestation_data(&att.canonical_data()).unwrap(); // Sign with issuer (identity) key att.identity_signature = diff --git a/crates/auths-id/src/agent_identity.rs b/crates/auths-id/src/agent_identity.rs index 0501bc78..c37e0529 100644 --- a/crates/auths-id/src/agent_identity.rs +++ b/crates/auths-id/src/agent_identity.rs @@ -307,6 +307,7 @@ fn sign_agent_attestation( vec![], None, config.delegated_by.clone(), + None, // commit_sha )?; att.signer_type = Some(SignerType::Agent); diff --git a/crates/auths-id/src/attestation/core.rs b/crates/auths-id/src/attestation/core.rs index 176255ef..f39511f0 100644 --- a/crates/auths-id/src/attestation/core.rs +++ b/crates/auths-id/src/attestation/core.rs @@ -5,9 +5,7 @@ use auths_core::signing::{PassphraseProvider, SecureSigner}; use auths_core::storage::keychain::KeyAlias; -use auths_verifier::core::{ - Attestation, CanonicalAttestationData, Ed25519Signature, canonicalize_attestation_data, -}; +use auths_verifier::core::{Attestation, Ed25519Signature, canonicalize_attestation_data}; use auths_verifier::error::AttestationError; use chrono::{DateTime, Utc}; @@ -67,28 +65,7 @@ pub fn resign_attestation( identity_alias: Option<&KeyAlias>, device_alias: &KeyAlias, ) -> Result<(), AttestationError> { - let data_to_canonicalize = CanonicalAttestationData { - version: attestation.version, - rid: &attestation.rid, - issuer: &attestation.issuer, - subject: &attestation.subject, - device_public_key: attestation.device_public_key.as_bytes(), - payload: &attestation.payload, - timestamp: &attestation.timestamp, - expires_at: &attestation.expires_at, - revoked_at: &attestation.revoked_at, - note: &attestation.note, - role: attestation.role.as_ref().map(|r| r.as_str()), - capabilities: if attestation.capabilities.is_empty() { - None - } else { - Some(&attestation.capabilities) - }, - delegated_by: attestation.delegated_by.as_ref(), - signer_type: attestation.signer_type.as_ref(), - }; - - let message_to_sign = canonicalize_attestation_data(&data_to_canonicalize)?; + let message_to_sign = canonicalize_attestation_data(&attestation.canonical_data())?; // Sign with the identity key (if alias provided) if let Some(alias) = identity_alias { diff --git a/crates/auths-id/src/attestation/create.rs b/crates/auths-id/src/attestation/create.rs index 52ea24db..9e1d2ff3 100644 --- a/crates/auths-id/src/attestation/create.rs +++ b/crates/auths-id/src/attestation/create.rs @@ -4,7 +4,7 @@ use auths_core::signing::{PassphraseProvider, SecureSigner}; use auths_core::storage::keychain::{IdentityDID, KeyAlias}; use auths_verifier::Capability; use auths_verifier::core::{ - Attestation, CanonicalAttestationData, Ed25519PublicKey, Ed25519Signature, ResourceId, Role, + Attestation, Ed25519PublicKey, Ed25519Signature, ResourceId, Role, canonicalize_attestation_data, }; use auths_verifier::error::AttestationError; @@ -71,6 +71,7 @@ pub fn create_signed_attestation( capabilities: Vec, role: Option, delegated_by: Option, + commit_sha: Option, ) -> Result { if device_public_key.len() != ED25519_PUBLIC_KEY_LEN { return Err(AttestationError::InvalidInput(format!( @@ -90,7 +91,7 @@ pub fn create_signed_attestation( } } - // Construct the canonical data to be signed + // Build attestation with empty signatures first (ActionEnvelope pattern) #[allow(clippy::disallowed_methods)] // INVARIANT: identity_did is an IdentityDID which guarantees valid DID format let issuer_canonical = CanonicalDid::new_unchecked(identity_did.as_str()); @@ -98,33 +99,37 @@ pub fn create_signed_attestation( // INVARIANT: device_did is a validated DeviceDID from the caller let subject_canonical = CanonicalDid::new_unchecked(device_did.as_str()); let delegated_canonical = delegated_by.as_ref().map(|d| CanonicalDid::from(d.clone())); - let data_to_canonicalize = CanonicalAttestationData { + + let mut attestation = Attestation { version: ATTESTATION_VERSION, - rid, - issuer: &issuer_canonical, - subject: &subject_canonical, - device_public_key, - payload: &payload, - timestamp: &meta.timestamp, - expires_at: &meta.expires_at, - revoked_at: &None, - note: &meta.note, - // Org fields included in signed envelope - role: role.as_ref().map(|r| r.as_str()), - capabilities: if capabilities.is_empty() { - None - } else { - Some(&capabilities) - }, - delegated_by: delegated_canonical.as_ref(), + subject: subject_canonical, + issuer: issuer_canonical, + rid: ResourceId::new(rid), + payload: payload.clone(), + timestamp: meta.timestamp, + expires_at: meta.expires_at, + revoked_at: None, + note: meta.note.clone(), + device_public_key: Ed25519PublicKey::try_from_slice(device_public_key) + .map_err(|e| AttestationError::InvalidInput(e.to_string()))?, + identity_signature: Ed25519Signature::empty(), + device_signature: Ed25519Signature::empty(), + role, + capabilities, + delegated_by: delegated_canonical, signer_type: None, + environment_claim: None, + commit_sha, + commit_message: None, + author: None, + oidc_binding: None, }; - // Canonicalize the attestation data - let message_to_sign = canonicalize_attestation_data(&data_to_canonicalize)?; + // Canonicalize using single source of truth + let message_to_sign = canonicalize_attestation_data(&attestation.canonical_data())?; // Sign with the identity key (if alias provided) - let identity_signature = if let Some(alias) = identity_alias { + if let Some(alias) = identity_alias { debug!("Signing attestation with identity alias '{}'", alias); let sig = signer .sign_with_alias(alias, passphrase_provider, &message_to_sign) @@ -135,15 +140,14 @@ pub fn create_signed_attestation( )) })?; debug!("Identity signature obtained successfully"); - Ed25519Signature::try_from_slice(&sig) - .map_err(|e| AttestationError::SigningError(e.to_string()))? + attestation.identity_signature = Ed25519Signature::try_from_slice(&sig) + .map_err(|e| AttestationError::SigningError(e.to_string()))?; } else { debug!("No identity alias provided, skipping identity signature (device-only attestation)"); - Ed25519Signature::empty() - }; + } // Sign with the device key if alias provided - let device_signature = if let Some(alias) = device_alias { + if let Some(alias) = device_alias { debug!("Signing attestation with device alias '{}'", alias); let sig = signer .sign_with_alias(alias, passphrase_provider, &message_to_sign) @@ -154,38 +158,13 @@ pub fn create_signed_attestation( )) })?; debug!("Device signature obtained successfully"); - Ed25519Signature::try_from_slice(&sig) - .map_err(|e| AttestationError::SigningError(e.to_string()))? + attestation.device_signature = Ed25519Signature::try_from_slice(&sig) + .map_err(|e| AttestationError::SigningError(e.to_string()))?; } else { debug!("No device alias provided, skipping device signature"); - Ed25519Signature::empty() - }; + } - // Construct final attestation - Ok(Attestation { - version: ATTESTATION_VERSION, - subject: subject_canonical, - issuer: issuer_canonical, - rid: ResourceId::new(rid), - payload: payload.clone(), - timestamp: meta.timestamp, - expires_at: meta.expires_at, - revoked_at: None, - note: meta.note.clone(), - device_public_key: Ed25519PublicKey::try_from_slice(device_public_key) - .map_err(|e| AttestationError::InvalidInput(e.to_string()))?, - identity_signature, - device_signature, - role, - capabilities, - delegated_by: delegated_canonical, - signer_type: None, - environment_claim: None, - commit_sha: None, - commit_message: None, - author: None, - oidc_binding: None, - }) + Ok(attestation) } /// Generates the canonical byte representation specifically for revocation data. diff --git a/crates/auths-id/src/attestation/verify.rs b/crates/auths-id/src/attestation/verify.rs index 284f0303..75d71b47 100644 --- a/crates/auths-id/src/attestation/verify.rs +++ b/crates/auths-id/src/attestation/verify.rs @@ -1,5 +1,5 @@ use crate::identity::resolve::DidResolver; -use auths_verifier::core::{Attestation, CanonicalAttestationData}; +use auths_verifier::core::Attestation; use auths_verifier::error::AttestationError; use chrono::{DateTime, Duration, Utc}; use log::debug; @@ -72,28 +72,8 @@ pub fn verify_with_resolver( })?; let issuer_pk_bytes = *resolved.public_key(); - // 3. Reconstruct canonical data (MUST match create_with_signatures, includes org fields) - let data_to_canonicalize = CanonicalAttestationData { - version: att.version, - rid: &att.rid, - issuer: &att.issuer, - subject: &att.subject, - device_public_key: att.device_public_key.as_bytes(), - payload: &att.payload, - timestamp: &att.timestamp, - expires_at: &att.expires_at, - revoked_at: &att.revoked_at, - note: &att.note, - role: att.role.as_ref().map(|r| r.as_str()), - capabilities: if att.capabilities.is_empty() { - None - } else { - Some(&att.capabilities) - }, - delegated_by: att.delegated_by.as_ref(), - signer_type: att.signer_type.as_ref(), - }; - let canonical_json_string = json_canon::to_string(&data_to_canonicalize).map_err(|e| { + // 3. Reconstruct canonical data (single source of truth via canonical_data()) + let canonical_json_string = json_canon::to_string(&att.canonical_data()).map_err(|e| { AttestationError::SerializationError(format!( "Failed to create canonical JSON for verification: {}", e diff --git a/crates/auths-id/tests/cases/lifecycle.rs b/crates/auths-id/tests/cases/lifecycle.rs index 47f853df..048b2959 100644 --- a/crates/auths-id/tests/cases/lifecycle.rs +++ b/crates/auths-id/tests/cases/lifecycle.rs @@ -121,6 +121,7 @@ fn create_test_attestation( vec![], None, None, + None, // commit_sha ) .expect("Failed to create signed attestation") } diff --git a/crates/auths-sdk/src/domains/device/service.rs b/crates/auths-sdk/src/domains/device/service.rs index bc19759d..6d222f09 100644 --- a/crates/auths-sdk/src/domains/device/service.rs +++ b/crates/auths-sdk/src/domains/device/service.rs @@ -224,6 +224,7 @@ pub fn extend_device( vec![], None, None, + None, // commit_sha ) .map_err(DeviceExtensionError::AttestationFailed)?; @@ -316,6 +317,7 @@ fn sign_and_persist_attestation( params.capabilities.clone(), None, None, + None, // commit_sha ) .map_err(DeviceError::AttestationError)?; diff --git a/crates/auths-sdk/src/domains/identity/service.rs b/crates/auths-sdk/src/domains/identity/service.rs index 13ef2c41..f2f205f3 100644 --- a/crates/auths-sdk/src/domains/identity/service.rs +++ b/crates/auths-sdk/src/domains/identity/service.rs @@ -321,6 +321,7 @@ fn bind_device( vec![], None, None, + None, // commit_sha ) .map_err(|e| SetupError::StorageError(e.into()))?; diff --git a/crates/auths-sdk/src/domains/org/service.rs b/crates/auths-sdk/src/domains/org/service.rs index e1541ae4..e689fadf 100644 --- a/crates/auths-sdk/src/domains/org/service.rs +++ b/crates/auths-sdk/src/domains/org/service.rs @@ -372,6 +372,7 @@ pub fn add_organization_member( // INVARIANT: admin_att.subject is a CanonicalDid from a verified attestation loaded by find_admin() Some(IdentityDID::new_unchecked(admin_att.subject.to_string())) }, + None, // commit_sha ) .map_err(|e| OrgError::Signing(e.to_string()))?; diff --git a/crates/auths-sdk/src/domains/signing/service.rs b/crates/auths-sdk/src/domains/signing/service.rs index 475f0a91..0e13c478 100644 --- a/crates/auths-sdk/src/domains/signing/service.rs +++ b/crates/auths-sdk/src/domains/signing/service.rs @@ -202,6 +202,7 @@ pub enum SigningKeyMaterial { /// device_key: SigningKeyMaterial::Direct(my_seed), /// expires_in: Some(31_536_000), /// note: None, +/// commit_sha: None, /// }; /// ``` pub struct ArtifactSigningParams { @@ -215,6 +216,8 @@ pub struct ArtifactSigningParams { pub expires_in: Option, /// Optional human-readable annotation embedded in the attestation. pub note: Option, + /// Git commit SHA for provenance binding (included in signed canonical data). + pub commit_sha: Option, } /// Result of a successful artifact attestation signing operation. @@ -262,6 +265,10 @@ pub enum ArtifactSigningError { /// Adding the device signature to a partially-signed attestation failed. #[error("attestation re-signing failed: {0}")] ResignFailed(String), + + /// The provided commit SHA has an invalid format. + #[error("invalid commit SHA: {0} (expected 40 or 64 hex characters)")] + InvalidCommitSha(String), } impl auths_core::error::AuthsErrorInfo for ArtifactSigningError { @@ -273,6 +280,7 @@ impl auths_core::error::AuthsErrorInfo for ArtifactSigningError { Self::DigestFailed(_) => "AUTHS-E5853", Self::AttestationFailed(_) => "AUTHS-E5854", Self::ResignFailed(_) => "AUTHS-E5855", + Self::InvalidCommitSha(_) => "AUTHS-E5856", } } @@ -290,6 +298,9 @@ impl auths_core::error::AuthsErrorInfo for ArtifactSigningError { Self::ResignFailed(_) => { Some("Verify your device key is accessible with `auths status`") } + Self::InvalidCommitSha(_) => { + Some("Provide a full SHA-1 (40 hex chars) or SHA-256 (64 hex chars) commit hash") + } } } } @@ -392,6 +403,24 @@ fn resolve_required_key( })? } +/// Validate and normalize a commit SHA (40-char SHA-1 or 64-char SHA-256). +/// +/// Args: +/// * `sha`: The raw commit SHA string to validate. +/// +/// Usage: +/// ```ignore +/// let normalized = validate_commit_sha("AbCd1234...")?; +/// ``` +pub fn validate_commit_sha(sha: &str) -> Result { + let normalized = sha.to_ascii_lowercase(); + let len = normalized.len(); + if (len != 40 && len != 64) || !normalized.bytes().all(|b| b.is_ascii_hexdigit()) { + return Err(ArtifactSigningError::InvalidCommitSha(sha.to_string())); + } + Ok(normalized) +} + /// Full artifact attestation signing pipeline. /// /// Loads the identity, resolves key material (supporting both keychain aliases @@ -410,6 +439,7 @@ fn resolve_required_key( /// device_key: SigningKeyMaterial::Direct(seed), /// expires_in: Some(31_536_000), /// note: None, +/// commit_sha: None, /// }; /// let result = sign_artifact(params, &ctx)?; /// ``` @@ -474,6 +504,11 @@ pub fn sign_artifact( let payload = serde_json::to_value(&artifact_meta) .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?; + let validated_commit_sha = params + .commit_sha + .map(|sha| validate_commit_sha(&sha)) + .transpose()?; + let signer = SeedMapSigner { seeds }; // Seeds are already resolved — passphrase provider will not be called. let noop_provider = auths_core::PrefilledPassphraseProvider::new(""); @@ -493,6 +528,7 @@ pub fn sign_artifact( vec![Capability::sign_release()], None, None, + validated_commit_sha, ) .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?; @@ -528,11 +564,12 @@ pub fn sign_artifact( /// * `data` - Raw artifact bytes to sign. /// * `expires_in` - Optional TTL in seconds. /// * `note` - Optional attestation note. +/// * `commit_sha` - Optional git commit SHA for provenance binding (40 or 64 hex chars). /// /// Usage: /// ```ignore /// let did = IdentityDID::parse("did:keri:E...")?; -/// let result = sign_artifact_raw(Utc::now(), &seed, &did, b"payload", None, None)?; +/// let result = sign_artifact_raw(Utc::now(), &seed, &did, b"payload", None, None, None)?; /// ``` pub fn sign_artifact_raw( now: DateTime, @@ -541,6 +578,7 @@ pub fn sign_artifact_raw( data: &[u8], expires_in: Option, note: Option, + commit_sha: Option, ) -> Result { let pubkey = provider_bridge::ed25519_public_key_from_seed_sync(seed) .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?; @@ -584,6 +622,10 @@ pub fn sign_artifact_raw( // Seeds are already resolved — passphrase provider will not be called. let noop_provider = auths_core::PrefilledPassphraseProvider::new(""); + let validated_commit_sha = commit_sha + .map(|sha| validate_commit_sha(&sha)) + .transpose()?; + let attestation = create_signed_attestation( now, &rid, @@ -599,6 +641,7 @@ pub fn sign_artifact_raw( vec![Capability::sign_release()], None, None, + validated_commit_sha, ) .map_err(|e| ArtifactSigningError::AttestationFailed(e.to_string()))?; diff --git a/crates/auths-sdk/src/pairing/mod.rs b/crates/auths-sdk/src/pairing/mod.rs index ca57a9de..a78fd1bf 100644 --- a/crates/auths-sdk/src/pairing/mod.rs +++ b/crates/auths-sdk/src/pairing/mod.rs @@ -365,6 +365,7 @@ pub fn create_pairing_attestation( device_capabilities, None, None, + None, // commit_sha ) .map_err(|e| PairingError::AttestationFailed(e.to_string()))?; diff --git a/crates/auths-sdk/src/signing.rs b/crates/auths-sdk/src/signing.rs index 4296e4c3..102befc2 100644 --- a/crates/auths-sdk/src/signing.rs +++ b/crates/auths-sdk/src/signing.rs @@ -3,5 +3,5 @@ pub use crate::domains::signing::service::{ ArtifactSigningError, ArtifactSigningParams, ArtifactSigningResult, SigningConfig, SigningError, SigningKeyMaterial, construct_signature_payload, sign_artifact, - sign_artifact_raw, sign_with_seed, validate_freeze_state, + sign_artifact_raw, sign_with_seed, validate_commit_sha, validate_freeze_state, }; diff --git a/crates/auths-sdk/src/workflows/ci/batch_attest.rs b/crates/auths-sdk/src/workflows/ci/batch_attest.rs index e454db90..daf1ab73 100644 --- a/crates/auths-sdk/src/workflows/ci/batch_attest.rs +++ b/crates/auths-sdk/src/workflows/ci/batch_attest.rs @@ -66,6 +66,8 @@ pub struct BatchSignConfig { pub expires_in: Option, /// Optional note for all attestations. pub note: Option, + /// Git commit SHA for provenance binding (shared across all artifacts in batch). + pub commit_sha: Option, } /// Outcome for a single artifact in a batch. @@ -202,6 +204,7 @@ pub fn batch_sign_artifacts( device_key: SigningKeyMaterial::Alias(KeyAlias::new_unchecked(&config.device_key)), expires_in: config.expires_in, note: config.note.clone(), + commit_sha: config.commit_sha.clone(), }; match sign_artifact(params, ctx) { diff --git a/crates/auths-sdk/src/workflows/ci/machine_identity.rs b/crates/auths-sdk/src/workflows/ci/machine_identity.rs index a82580f7..eff48e04 100644 --- a/crates/auths-sdk/src/workflows/ci/machine_identity.rs +++ b/crates/auths-sdk/src/workflows/ci/machine_identity.rs @@ -295,25 +295,9 @@ pub fn sign_commit_with_identity( }; // Create canonical form and sign - let canonical_data = auths_verifier::core::CanonicalAttestationData { - version: attestation.version, - rid: &attestation.rid, - issuer: &attestation.issuer, - subject: &attestation.subject, - device_public_key: attestation.device_public_key.as_bytes(), - payload: &attestation.payload, - timestamp: &attestation.timestamp, - expires_at: &attestation.expires_at, - revoked_at: &attestation.revoked_at, - note: &attestation.note, - role: None, - capabilities: None, - delegated_by: None, - signer_type: None, - }; - - let canonical_bytes = auths_verifier::core::canonicalize_attestation_data(&canonical_data) - .map_err(|e| format!("Canonicalization failed: {}", e))?; + let canonical_bytes = + auths_verifier::core::canonicalize_attestation_data(&attestation.canonical_data()) + .map_err(|e| format!("Canonicalization failed: {}", e))?; let signature = issuer_keypair.sign(&canonical_bytes); attestation.identity_signature = Ed25519Signature::try_from_slice(signature.as_ref()) diff --git a/crates/auths-sdk/src/workflows/org.rs b/crates/auths-sdk/src/workflows/org.rs index ca5a505a..f5ef3e9a 100644 --- a/crates/auths-sdk/src/workflows/org.rs +++ b/crates/auths-sdk/src/workflows/org.rs @@ -372,6 +372,7 @@ pub fn add_organization_member( // INVARIANT: admin_att.subject is a CanonicalDid from a verified attestation loaded by find_admin() Some(IdentityDID::new_unchecked(admin_att.subject.to_string())) }, + None, // commit_sha ) .map_err(|e| OrgError::Signing(e.to_string()))?; diff --git a/crates/auths-sdk/tests/cases/artifact.rs b/crates/auths-sdk/tests/cases/artifact.rs index 02cba402..0c5eaa70 100644 --- a/crates/auths-sdk/tests/cases/artifact.rs +++ b/crates/auths-sdk/tests/cases/artifact.rs @@ -131,6 +131,7 @@ fn sign_artifact_with_alias_keys_produces_valid_json() { device_key: SigningKeyMaterial::Alias(key_alias), expires_in: Some(31_536_000), note: Some("integration test".into()), + commit_sha: None, }; let result = sign_artifact(params, &ctx).unwrap(); @@ -161,6 +162,7 @@ fn sign_artifact_with_direct_device_key_produces_valid_json() { device_key: SigningKeyMaterial::Direct(device_seed), expires_in: None, note: None, + commit_sha: None, }; let result = sign_artifact(params, &ctx).unwrap(); @@ -183,6 +185,7 @@ fn sign_artifact_identity_not_found_returns_error() { device_key: SigningKeyMaterial::Direct(device_seed), expires_in: None, note: None, + commit_sha: None, }; let result = sign_artifact(params, &empty_ctx); @@ -211,6 +214,7 @@ fn sign_artifact_raw_produces_valid_attestation_json() { data, Some(86400), Some("test note".into()), + None, ) .unwrap(); @@ -238,7 +242,7 @@ fn sign_artifact_raw_without_optional_fields() { let identity_did = IdentityDID::new_unchecked("did:keri:Eminimal"); let now = Utc::now(); - let result = sign_artifact_raw(now, &seed, &identity_did, b"data", None, None).unwrap(); + let result = sign_artifact_raw(now, &seed, &identity_did, b"data", None, None, None).unwrap(); let parsed: serde_json::Value = serde_json::from_str(&result.attestation_json).unwrap(); assert!(parsed.get("expires_at").is_none() || parsed["expires_at"].is_null()); @@ -252,7 +256,7 @@ fn sign_artifact_raw_digest_matches_sha256_of_data() { let data = b"hello world"; let now = Utc::now(); - let result = sign_artifact_raw(now, &seed, &identity_did, data, None, None).unwrap(); + let result = sign_artifact_raw(now, &seed, &identity_did, data, None, None, None).unwrap(); let expected_digest = "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"; assert_eq!(result.digest, expected_digest); diff --git a/crates/auths-verifier/src/core.rs b/crates/auths-verifier/src/core.rs index ea27f778..816e9d46 100644 --- a/crates/auths-verifier/src/core.rs +++ b/crates/auths-verifier/src/core.rs @@ -894,6 +894,9 @@ pub struct CanonicalAttestationData<'a> { /// Type of signer (included in signed envelope). #[serde(skip_serializing_if = "Option::is_none")] pub signer_type: Option<&'a SignerType>, + /// Git commit SHA for provenance binding (included in signed envelope). + #[serde(skip_serializing_if = "Option::is_none")] + pub commit_sha: Option<&'a str>, } /// Produce the canonical JSON bytes over which signatures are computed. @@ -934,6 +937,40 @@ impl Attestation { .map_err(|e| AttestationError::SerializationError(e.to_string())) } + /// Returns the canonical subset of fields that signatures are computed over. + /// + /// Args: + /// * `&self`: The attestation to extract canonical data from. + /// + /// Usage: + /// ```ignore + /// let canonical = attestation.canonical_data(); + /// let bytes = canonicalize_attestation_data(&canonical)?; + /// ``` + pub fn canonical_data(&self) -> CanonicalAttestationData<'_> { + CanonicalAttestationData { + version: self.version, + rid: &self.rid, + issuer: &self.issuer, + subject: &self.subject, + device_public_key: self.device_public_key.as_bytes(), + payload: &self.payload, + timestamp: &self.timestamp, + expires_at: &self.expires_at, + revoked_at: &self.revoked_at, + note: &self.note, + role: self.role.as_ref().map(|r| r.as_str()), + capabilities: if self.capabilities.is_empty() { + None + } else { + Some(&self.capabilities) + }, + delegated_by: self.delegated_by.as_ref(), + signer_type: self.signer_type.as_ref(), + commit_sha: self.commit_sha.as_deref(), + } + } + /// Formats the attestation contents for debug or inspection purposes. pub fn to_debug_string(&self) -> String { format!( diff --git a/crates/auths-verifier/src/verify.rs b/crates/auths-verifier/src/verify.rs index 14068526..65854481 100644 --- a/crates/auths-verifier/src/verify.rs +++ b/crates/auths-verifier/src/verify.rs @@ -2,9 +2,7 @@ #[cfg(feature = "native")] use crate::core::Capability; -use crate::core::{ - Attestation, CanonicalAttestationData, VerifiedAttestation, canonicalize_attestation_data, -}; +use crate::core::{Attestation, VerifiedAttestation, canonicalize_attestation_data}; use crate::error::AttestationError; use crate::types::{ChainLink, VerificationReport, VerificationStatus}; #[cfg(feature = "native")] @@ -292,27 +290,7 @@ pub async fn verify_device_link( pub fn compute_attestation_seal_digest( attestation: &Attestation, ) -> Result { - let data = CanonicalAttestationData { - version: attestation.version, - rid: &attestation.rid, - issuer: &attestation.issuer, - subject: &attestation.subject, - device_public_key: attestation.device_public_key.as_bytes(), - payload: &attestation.payload, - timestamp: &attestation.timestamp, - expires_at: &attestation.expires_at, - revoked_at: &attestation.revoked_at, - note: &attestation.note, - role: attestation.role.as_ref().map(|r| r.as_str()), - capabilities: if attestation.capabilities.is_empty() { - None - } else { - Some(&attestation.capabilities) - }, - delegated_by: attestation.delegated_by.as_ref(), - signer_type: attestation.signer_type.as_ref(), - }; - let canonical = canonicalize_attestation_data(&data)?; + let canonical = canonicalize_attestation_data(&attestation.canonical_data())?; Ok(crate::keri::compute_said(&canonical).to_string()) } @@ -364,27 +342,7 @@ pub(crate) async fn verify_with_keys_at( } // --- 5. Reconstruct and canonicalize data --- - let data_to_canonicalize = CanonicalAttestationData { - version: att.version, - rid: &att.rid, - issuer: &att.issuer, - subject: &att.subject, - device_public_key: att.device_public_key.as_bytes(), - payload: &att.payload, - timestamp: &att.timestamp, - expires_at: &att.expires_at, - revoked_at: &att.revoked_at, - note: &att.note, - role: att.role.as_ref().map(|r| r.as_str()), - capabilities: if att.capabilities.is_empty() { - None - } else { - Some(&att.capabilities) - }, - delegated_by: att.delegated_by.as_ref(), - signer_type: att.signer_type.as_ref(), - }; - let canonical_json_bytes = canonicalize_attestation_data(&data_to_canonicalize)?; + let canonical_json_bytes = canonicalize_attestation_data(&att.canonical_data())?; let data_to_verify = canonical_json_bytes.as_slice(); debug!( "(Verify) Canonical data: {}", @@ -626,27 +584,7 @@ mod tests { oidc_binding: None, }; - let data = CanonicalAttestationData { - version: att.version, - rid: &att.rid, - issuer: &att.issuer, - subject: &att.subject, - device_public_key: att.device_public_key.as_bytes(), - payload: &att.payload, - timestamp: &att.timestamp, - expires_at: &att.expires_at, - revoked_at: &att.revoked_at, - note: &att.note, - role: att.role.as_ref().map(|r| r.as_str()), - capabilities: if att.capabilities.is_empty() { - None - } else { - Some(&att.capabilities) - }, - delegated_by: att.delegated_by.as_ref(), - signer_type: att.signer_type.as_ref(), - }; - let canonical_bytes = canonicalize_attestation_data(&data).unwrap(); + let canonical_bytes = canonicalize_attestation_data(&att.canonical_data()).unwrap(); att.identity_signature = Ed25519Signature::try_from_slice(issuer_kp.sign(&canonical_bytes).as_ref()).unwrap(); @@ -1238,28 +1176,7 @@ mod tests { oidc_binding: None, }; - let caps_ref = if att.capabilities.is_empty() { - None - } else { - Some(&att.capabilities) - }; - let data = CanonicalAttestationData { - version: att.version, - rid: &att.rid, - issuer: &att.issuer, - subject: &att.subject, - device_public_key: att.device_public_key.as_bytes(), - payload: &att.payload, - timestamp: &att.timestamp, - expires_at: &att.expires_at, - revoked_at: &att.revoked_at, - note: &att.note, - role: att.role.as_ref().map(|r| r.as_str()), - capabilities: caps_ref, - delegated_by: att.delegated_by.as_ref(), - signer_type: att.signer_type.as_ref(), - }; - let canonical_bytes = canonicalize_attestation_data(&data).unwrap(); + let canonical_bytes = canonicalize_attestation_data(&att.canonical_data()).unwrap(); att.identity_signature = Ed25519Signature::try_from_slice(issuer_kp.sign(&canonical_bytes).as_ref()).unwrap(); @@ -1592,27 +1509,7 @@ mod tests { oidc_binding: None, }; - let data = CanonicalAttestationData { - version: att.version, - rid: &att.rid, - issuer: &att.issuer, - subject: &att.subject, - device_public_key: att.device_public_key.as_bytes(), - payload: &att.payload, - timestamp: &att.timestamp, - expires_at: &att.expires_at, - revoked_at: &att.revoked_at, - note: &att.note, - role: att.role.as_ref().map(|r| r.as_str()), - capabilities: if att.capabilities.is_empty() { - None - } else { - Some(&att.capabilities) - }, - delegated_by: att.delegated_by.as_ref(), - signer_type: att.signer_type.as_ref(), - }; - let canonical_bytes = canonicalize_attestation_data(&data).unwrap(); + let canonical_bytes = canonicalize_attestation_data(&att.canonical_data()).unwrap(); att.identity_signature = Ed25519Signature::try_from_slice(issuer_kp.sign(&canonical_bytes).as_ref()).unwrap(); diff --git a/crates/auths-verifier/tests/cases/expiration_skew.rs b/crates/auths-verifier/tests/cases/expiration_skew.rs index 6e822f4c..44e3684a 100644 --- a/crates/auths-verifier/tests/cases/expiration_skew.rs +++ b/crates/auths-verifier/tests/cases/expiration_skew.rs @@ -1,8 +1,7 @@ use auths_crypto::testing::create_test_keypair; use auths_verifier::AttestationBuilder; use auths_verifier::core::{ - Attestation, CanonicalAttestationData, Ed25519PublicKey, Ed25519Signature, - canonicalize_attestation_data, + Attestation, Ed25519PublicKey, Ed25519Signature, canonicalize_attestation_data, }; use auths_verifier::verifier::Verifier; use chrono::{DateTime, Duration, Utc}; @@ -27,23 +26,7 @@ fn create_signed_attestation( .timestamp(timestamp) .build(); - let data = CanonicalAttestationData { - version: att.version, - rid: &att.rid, - issuer: &att.issuer, - subject: &att.subject, - device_public_key: att.device_public_key.as_bytes(), - payload: &att.payload, - timestamp: &att.timestamp, - expires_at: &att.expires_at, - revoked_at: &att.revoked_at, - note: &att.note, - role: None, - capabilities: None, - delegated_by: None, - signer_type: None, - }; - let canonical_bytes = canonicalize_attestation_data(&data).unwrap(); + let canonical_bytes = canonicalize_attestation_data(&att.canonical_data()).unwrap(); att.identity_signature = Ed25519Signature::try_from_slice(issuer_kp.sign(&canonical_bytes).as_ref()).unwrap(); diff --git a/crates/auths-verifier/tests/cases/revocation_adversarial.rs b/crates/auths-verifier/tests/cases/revocation_adversarial.rs index ea2af7cb..ed6164d5 100644 --- a/crates/auths-verifier/tests/cases/revocation_adversarial.rs +++ b/crates/auths-verifier/tests/cases/revocation_adversarial.rs @@ -1,8 +1,7 @@ use auths_crypto::testing::create_test_keypair; use auths_verifier::AttestationBuilder; use auths_verifier::core::{ - Attestation, CanonicalAttestationData, Ed25519PublicKey, Ed25519Signature, - canonicalize_attestation_data, + Attestation, Ed25519PublicKey, Ed25519Signature, canonicalize_attestation_data, }; use auths_verifier::verify::verify_with_keys; use chrono::{DateTime, Duration, Utc}; @@ -31,23 +30,7 @@ fn create_signed_attestation( .timestamp(Some(timestamp)) .build(); - let data = CanonicalAttestationData { - version: att.version, - rid: &att.rid, - issuer: &att.issuer, - subject: &att.subject, - device_public_key: att.device_public_key.as_bytes(), - payload: &att.payload, - timestamp: &att.timestamp, - expires_at: &att.expires_at, - revoked_at: &att.revoked_at, - note: &att.note, - role: None, - capabilities: None, - delegated_by: None, - signer_type: None, - }; - let canonical_bytes = canonicalize_attestation_data(&data).unwrap(); + let canonical_bytes = canonicalize_attestation_data(&att.canonical_data()).unwrap(); att.identity_signature = Ed25519Signature::try_from_slice(issuer_kp.sign(&canonical_bytes).as_ref()).unwrap(); diff --git a/crates/auths-verifier/tests/cases/serialization_pinning.rs b/crates/auths-verifier/tests/cases/serialization_pinning.rs index 7cb94251..cbdbc102 100644 --- a/crates/auths-verifier/tests/cases/serialization_pinning.rs +++ b/crates/auths-verifier/tests/cases/serialization_pinning.rs @@ -180,8 +180,7 @@ fn json_canon_golden_output() { #[test] fn environment_claim_excluded_from_canonical_form() { use auths_verifier::core::{ - Attestation, CanonicalAttestationData, Ed25519PublicKey, Ed25519Signature, ResourceId, - canonicalize_attestation_data, + Attestation, Ed25519PublicKey, Ed25519Signature, ResourceId, canonicalize_attestation_data, }; use auths_verifier::types::CanonicalDid; @@ -209,48 +208,15 @@ fn environment_claim_excluded_from_canonical_form() { oidc_binding: None, }; - let data = CanonicalAttestationData { - version: att.version, - rid: &att.rid, - issuer: &att.issuer, - subject: &att.subject, - device_public_key: att.device_public_key.as_bytes(), - payload: &att.payload, - timestamp: &att.timestamp, - expires_at: &att.expires_at, - revoked_at: &att.revoked_at, - note: &att.note, - role: None, - capabilities: None, - delegated_by: None, - signer_type: None, - }; - - let canonical_with_env = canonicalize_attestation_data(&data).unwrap(); + let canonical_with_env = canonicalize_attestation_data(&att.canonical_data()).unwrap(); let att_without = Attestation { environment_claim: None, ..att.clone() }; - let data_without = CanonicalAttestationData { - version: att_without.version, - rid: &att_without.rid, - issuer: &att_without.issuer, - subject: &att_without.subject, - device_public_key: att_without.device_public_key.as_bytes(), - payload: &att_without.payload, - timestamp: &att_without.timestamp, - expires_at: &att_without.expires_at, - revoked_at: &att_without.revoked_at, - note: &att_without.note, - role: None, - capabilities: None, - delegated_by: None, - signer_type: None, - }; - - let canonical_without_env = canonicalize_attestation_data(&data_without).unwrap(); + let canonical_without_env = + canonicalize_attestation_data(&att_without.canonical_data()).unwrap(); assert_eq!( canonical_with_env, canonical_without_env, diff --git a/docs/sdk/rust/signing-and-verification.md b/docs/sdk/rust/signing-and-verification.md index 109aa2fb..fd5aecd1 100644 --- a/docs/sdk/rust/signing-and-verification.md +++ b/docs/sdk/rust/signing-and-verification.md @@ -118,6 +118,7 @@ let params = ArtifactSigningParams { device_key: SigningKeyMaterial::Direct(my_seed), expires_in: Some(31_536_000), note: Some("v1.0.0 release".into()), + commit_sha: None, }; let result = sign_artifact_attestation(params, &ctx)?; diff --git a/packages/auths-node/index.d.ts b/packages/auths-node/index.d.ts index 5f869e76..4ee1a7b4 100644 --- a/packages/auths-node/index.d.ts +++ b/packages/auths-node/index.d.ts @@ -246,9 +246,9 @@ export declare function signActionAsIdentity(actionType: string, payloadJson: st */ export declare function signActionRaw(privateKeyHex: string, actionType: string, payloadJson: string, identityDid: string): string -export declare function signArtifact(filePath: string, identityKeyAlias: string, repoPath: string, passphrase?: string | undefined | null, expiresIn?: number | undefined | null, note?: string | undefined | null): NapiArtifactResult +export declare function signArtifact(filePath: string, identityKeyAlias: string, repoPath: string, passphrase?: string | undefined | null, expiresIn?: number | undefined | null, note?: string | undefined | null, commitSha?: string | undefined | null): NapiArtifactResult -export declare function signArtifactBytes(data: Buffer, identityKeyAlias: string, repoPath: string, passphrase?: string | undefined | null, expiresIn?: number | undefined | null, note?: string | undefined | null): NapiArtifactResult +export declare function signArtifactBytes(data: Buffer, identityKeyAlias: string, repoPath: string, passphrase?: string | undefined | null, expiresIn?: number | undefined | null, note?: string | undefined | null, commitSha?: string | undefined | null): NapiArtifactResult /** * Sign raw bytes with a raw Ed25519 private key, producing a dual-signed attestation. @@ -261,8 +261,13 @@ export declare function signArtifactBytes(data: Buffer, identityKeyAlias: string * * `identity_did`: Identity DID string (must be `did:keri:` format). * * `expires_in`: Optional duration in seconds until expiration. * * `note`: Optional human-readable note. + * + * Usage: + * ```ignore + * let result = sign_artifact_bytes_raw(buffer, "abcd...".into(), "did:keri:E...".into(), None, None)?; + * ``` */ -export declare function signArtifactBytesRaw(data: Buffer, privateKeyHex: string, identityDid: string, expiresIn?: number | undefined | null, note?: string | undefined | null): NapiArtifactResult +export declare function signArtifactBytesRaw(data: Buffer, privateKeyHex: string, identityDid: string, expiresIn?: number | undefined | null, note?: string | undefined | null, commitSha?: string | undefined | null): NapiArtifactResult export declare function signAsAgent(message: Buffer, keyAlias: string, repoPath: string, passphrase?: string | undefined | null): NapiCommitSignResult diff --git a/packages/auths-node/lib/artifacts.ts b/packages/auths-node/lib/artifacts.ts index 2d680a08..9ef88f53 100644 --- a/packages/auths-node/lib/artifacts.ts +++ b/packages/auths-node/lib/artifacts.ts @@ -26,6 +26,8 @@ export interface SignArtifactOptions { note?: string /** Override the client's passphrase. */ passphrase?: string + /** Optional commit SHA to bind the attestation to. */ + commitSha?: string } /** Options for {@link ArtifactService.signBytes}. */ @@ -40,6 +42,8 @@ export interface SignArtifactBytesOptions { note?: string /** Override the client's passphrase. */ passphrase?: string + /** Optional commit SHA to bind the attestation to. */ + commitSha?: string } /** @@ -85,6 +89,7 @@ export class ArtifactService { pp, opts.expiresInDays ?? null, opts.note ?? null, + opts.commitSha ?? null, ) } catch (err) { throw mapNativeError(err, CryptoError) @@ -116,6 +121,7 @@ export class ArtifactService { pp, opts.expiresInDays ?? null, opts.note ?? null, + opts.commitSha ?? null, ) } catch (err) { throw mapNativeError(err, CryptoError) diff --git a/packages/auths-node/lib/native.ts b/packages/auths-node/lib/native.ts index 2cad9990..37205fb9 100644 --- a/packages/auths-node/lib/native.ts +++ b/packages/auths-node/lib/native.ts @@ -221,9 +221,9 @@ export interface NativeBindings { listWitnesses(repoPath: string): string // Artifact - signArtifact(filePath: string, identityKeyAlias: string, repoPath: string, passphrase?: string | null, expiresInDays?: number | null, note?: string | null): NapiArtifactResult - signArtifactBytes(data: Buffer, identityKeyAlias: string, repoPath: string, passphrase?: string | null, expiresInDays?: number | null, note?: string | null): NapiArtifactResult - signArtifactBytesRaw(data: Buffer, privateKeyHex: string, identityDid: string, expiresIn?: number | null, note?: string | null): NapiArtifactResult + signArtifact(filePath: string, identityKeyAlias: string, repoPath: string, passphrase?: string | null, expiresInDays?: number | null, note?: string | null, commitSha?: string | null): NapiArtifactResult + signArtifactBytes(data: Buffer, identityKeyAlias: string, repoPath: string, passphrase?: string | null, expiresInDays?: number | null, note?: string | null, commitSha?: string | null): NapiArtifactResult + signArtifactBytesRaw(data: Buffer, privateKeyHex: string, identityDid: string, expiresIn?: number | null, note?: string | null, commitSha?: string | null): NapiArtifactResult // Audit generateAuditReport(targetRepoPath: string, authsRepoPath: string, since?: string | null, until?: string | null, author?: string | null, limit?: number | null): string diff --git a/packages/auths-node/src/artifact.rs b/packages/auths-node/src/artifact.rs index a86e266e..a0b22add 100644 --- a/packages/auths-node/src/artifact.rs +++ b/packages/auths-node/src/artifact.rs @@ -104,6 +104,7 @@ fn build_context_and_sign( passphrase: Option, expires_in: Option, note: Option, + commit_sha: Option, ) -> napi::Result { let passphrase_str = resolve_passphrase(passphrase); let env_config = make_env_config(&passphrase_str, repo_path); @@ -150,6 +151,7 @@ fn build_context_and_sign( device_key: SigningKeyMaterial::Alias(alias), expires_in: expires_in.map(|s| s as u64), note, + commit_sha, }; let result = sdk_sign_artifact(params, &ctx).map_err(|e| { @@ -175,6 +177,7 @@ pub fn sign_artifact( passphrase: Option, expires_in: Option, note: Option, + commit_sha: Option, ) -> napi::Result { let path = PathBuf::from(shellexpand::tilde(&file_path).as_ref()); if !path.exists() { @@ -194,6 +197,7 @@ pub fn sign_artifact( passphrase, expires_in, note, + commit_sha, ) } @@ -205,6 +209,7 @@ pub fn sign_artifact_bytes( passphrase: Option, expires_in: Option, note: Option, + commit_sha: Option, ) -> napi::Result { let artifact = Arc::new(BytesArtifact { data: data.to_vec(), @@ -216,6 +221,7 @@ pub fn sign_artifact_bytes( passphrase, expires_in, note, + commit_sha, ) } @@ -241,6 +247,7 @@ pub fn sign_artifact_bytes_raw( identity_did: String, expires_in: Option, note: Option, + commit_sha: Option, ) -> napi::Result { let seed = decode_seed_hex(&private_key_hex).map_err(|e| { format_error("AUTHS_INVALID_INPUT", format!("Invalid private key: {e}")) @@ -266,7 +273,7 @@ pub fn sign_artifact_bytes_raw( let now = Utc::now(); let data_len = data.len(); - let result = sign_artifact_raw(now, &seed, &did, data.as_ref(), expires_in_u64, note) + let result = sign_artifact_raw(now, &seed, &did, data.as_ref(), expires_in_u64, note, commit_sha) .map_err(|e| { format_error( "AUTHS_SIGNING_FAILED", diff --git a/packages/auths-node/src/org.rs b/packages/auths-node/src/org.rs index a654e28f..6d9ac612 100644 --- a/packages/auths-node/src/org.rs +++ b/packages/auths-node/src/org.rs @@ -165,6 +165,7 @@ pub fn create_org( admin_capabilities, Some(Role::Admin), None, + None, // commit_sha ) .map_err(|e| format_error("AUTHS_ORG_ERROR", e))?; diff --git a/packages/auths-python/python/auths/_client.py b/packages/auths-python/python/auths/_client.py index 3d35b37a..a0e50092 100644 --- a/packages/auths-python/python/auths/_client.py +++ b/packages/auths-python/python/auths/_client.py @@ -543,6 +543,7 @@ def sign_artifact( identity_did: str, expires_in: int | None = None, note: str | None = None, + commit_sha: str | None = None, ) -> ArtifactSigningResult: """Sign a file artifact, producing a dual-signed attestation. @@ -554,6 +555,7 @@ def sign_artifact( identity_did: The identity DID to sign with (used as key alias). expires_in: Duration in seconds until expiration (per RFC 6749). note: Optional human-readable note. + commit_sha: Optional commit SHA to bind the attestation to. Returns: ArtifactSigningResult with the attestation JSON, RID, digest, and file size. @@ -573,7 +575,7 @@ def sign_artifact( pp = self._passphrase try: raw = _sign_artifact( - path, identity_did, self.repo_path, pp, expires_in, note, + path, identity_did, self.repo_path, pp, expires_in, note, commit_sha, ) return ArtifactSigningResult( attestation_json=raw.attestation_json, @@ -593,6 +595,7 @@ def sign_artifact_bytes( identity_did: str, expires_in: int | None = None, note: str | None = None, + commit_sha: str | None = None, ) -> ArtifactSigningResult: """Sign raw bytes, producing a dual-signed attestation. @@ -604,6 +607,7 @@ def sign_artifact_bytes( identity_did: The identity DID to sign with (used as key alias). expires_in: Duration in seconds until expiration (per RFC 6749). note: Optional human-readable note. + commit_sha: Optional commit SHA to bind the attestation to. Returns: ArtifactSigningResult with the attestation JSON, RID, digest, and size. @@ -622,7 +626,7 @@ def sign_artifact_bytes( pp = self._passphrase try: raw = _sign_artifact_bytes( - data, identity_did, self.repo_path, pp, expires_in, note, + data, identity_did, self.repo_path, pp, expires_in, note, commit_sha, ) return ArtifactSigningResult( attestation_json=raw.attestation_json, diff --git a/packages/auths-python/src/artifact_sign.rs b/packages/auths-python/src/artifact_sign.rs index 2e1eabe6..f46b8009 100644 --- a/packages/auths-python/src/artifact_sign.rs +++ b/packages/auths-python/src/artifact_sign.rs @@ -133,6 +133,7 @@ fn build_context_and_sign( passphrase: Option, expires_in: Option, note: Option, + commit_sha: Option, ) -> PyResult { let passphrase_str = resolve_passphrase(passphrase); let env_config = make_keychain_config(&passphrase_str, repo_path); @@ -180,6 +181,7 @@ fn build_context_and_sign( device_key: SigningKeyMaterial::Alias(alias), expires_in, note, + commit_sha, }; let result = sdk_sign_artifact(params, &ctx).map_err(|e| { @@ -211,7 +213,7 @@ fn build_context_and_sign( /// let result = sign_artifact(py, "release.tar.gz", "main", "~/.auths", None, None, None)?; /// ``` #[pyfunction] -#[pyo3(signature = (file_path, identity_key_alias, repo_path, passphrase=None, expires_in=None, note=None))] +#[pyo3(signature = (file_path, identity_key_alias, repo_path, passphrase=None, expires_in=None, note=None, commit_sha=None))] pub fn sign_artifact( _py: Python<'_>, file_path: &str, @@ -220,6 +222,7 @@ pub fn sign_artifact( passphrase: Option, expires_in: Option, note: Option, + commit_sha: Option, ) -> PyResult { let path = PathBuf::from(shellexpand::tilde(file_path).as_ref()); if !path.exists() { @@ -233,7 +236,7 @@ pub fn sign_artifact( let rp = repo_path.to_string(); { - build_context_and_sign(artifact, &alias, &rp, passphrase, expires_in, note) + build_context_and_sign(artifact, &alias, &rp, passphrase, expires_in, note, commit_sha) } } @@ -252,7 +255,7 @@ pub fn sign_artifact( /// let result = sign_artifact_bytes(py, b"manifest data", "main", "~/.auths", None, None, None)?; /// ``` #[pyfunction] -#[pyo3(signature = (data, identity_key_alias, repo_path, passphrase=None, expires_in=None, note=None))] +#[pyo3(signature = (data, identity_key_alias, repo_path, passphrase=None, expires_in=None, note=None, commit_sha=None))] pub fn sign_artifact_bytes( _py: Python<'_>, data: &[u8], @@ -261,6 +264,7 @@ pub fn sign_artifact_bytes( passphrase: Option, expires_in: Option, note: Option, + commit_sha: Option, ) -> PyResult { let artifact = Arc::new(BytesArtifact { data: data.to_vec(), @@ -269,7 +273,7 @@ pub fn sign_artifact_bytes( let rp = repo_path.to_string(); { - build_context_and_sign(artifact, &alias, &rp, passphrase, expires_in, note) + build_context_and_sign(artifact, &alias, &rp, passphrase, expires_in, note, commit_sha) } } @@ -289,7 +293,7 @@ pub fn sign_artifact_bytes( /// let result = sign_artifact_bytes_raw(py, b"payload", "abcd...", "did:keri:E...", None, None)?; /// ``` #[pyfunction] -#[pyo3(signature = (data, private_key_hex, identity_did, expires_in=None, note=None))] +#[pyo3(signature = (data, private_key_hex, identity_did, expires_in=None, note=None, commit_sha=None))] pub fn sign_artifact_bytes_raw( _py: Python<'_>, data: &[u8], @@ -297,6 +301,7 @@ pub fn sign_artifact_bytes_raw( identity_did: &str, expires_in: Option, note: Option, + commit_sha: Option, ) -> PyResult { let seed = decode_seed_hex(private_key_hex) .map_err(|e| PyValueError::new_err(format!("[AUTHS_INVALID_INPUT] {e}")))?; @@ -306,7 +311,7 @@ pub fn sign_artifact_bytes_raw( let now = Utc::now(); - let result = sign_artifact_raw(now, &seed, &did, data, expires_in, note).map_err(|e| { + let result = sign_artifact_raw(now, &seed, &did, data, expires_in, note, commit_sha).map_err(|e| { PyRuntimeError::new_err(format!("[AUTHS_SIGNING_FAILED] Artifact signing failed: {e}")) })?; diff --git a/packages/auths-python/src/org.rs b/packages/auths-python/src/org.rs index 7d48881d..9e4da170 100644 --- a/packages/auths-python/src/org.rs +++ b/packages/auths-python/src/org.rs @@ -149,6 +149,7 @@ pub fn create_org( admin_capabilities, Some(Role::Admin), None, + None, // commit_sha ) .map_err(|e| PyRuntimeError::new_err(format!("[AUTHS_ORG_ERROR] {e}")))?; From 83cbec4397e789e4f04718dcb1327a4af9609f06 Mon Sep 17 00:00:00 2001 From: bordumb Date: Sat, 4 Apr 2026 00:47:51 -0700 Subject: [PATCH 2/2] fix: fmt fixes --- packages/auths-node/src/artifact.rs | 32 ++++---- packages/auths-node/src/sign.rs | 14 ++-- packages/auths-node/src/verify.rs | 6 +- packages/auths-python/src/artifact_sign.rs | 17 +++-- packages/auths-python/src/lib.rs | 5 +- packages/auths-python/src/verify.rs | 86 +++++++++++----------- 6 files changed, 87 insertions(+), 73 deletions(-) diff --git a/packages/auths-node/src/artifact.rs b/packages/auths-node/src/artifact.rs index a0b22add..3e758470 100644 --- a/packages/auths-node/src/artifact.rs +++ b/packages/auths-node/src/artifact.rs @@ -249,13 +249,11 @@ pub fn sign_artifact_bytes_raw( note: Option, commit_sha: Option, ) -> napi::Result { - let seed = decode_seed_hex(&private_key_hex).map_err(|e| { - format_error("AUTHS_INVALID_INPUT", format!("Invalid private key: {e}")) - })?; + let seed = decode_seed_hex(&private_key_hex) + .map_err(|e| format_error("AUTHS_INVALID_INPUT", format!("Invalid private key: {e}")))?; - let did = IdentityDID::parse(&identity_did).map_err(|e| { - format_error("AUTHS_INVALID_INPUT", format!("Invalid identity DID: {e}")) - })?; + let did = IdentityDID::parse(&identity_did) + .map_err(|e| format_error("AUTHS_INVALID_INPUT", format!("Invalid identity DID: {e}")))?; let expires_in_u64 = expires_in .map(|v| { @@ -273,13 +271,21 @@ pub fn sign_artifact_bytes_raw( let now = Utc::now(); let data_len = data.len(); - let result = sign_artifact_raw(now, &seed, &did, data.as_ref(), expires_in_u64, note, commit_sha) - .map_err(|e| { - format_error( - "AUTHS_SIGNING_FAILED", - format!("Artifact signing failed: {e}"), - ) - })?; + let result = sign_artifact_raw( + now, + &seed, + &did, + data.as_ref(), + expires_in_u64, + note, + commit_sha, + ) + .map_err(|e| { + format_error( + "AUTHS_SIGNING_FAILED", + format!("Artifact signing failed: {e}"), + ) + })?; Ok(NapiArtifactResult { attestation_json: result.attestation_json, diff --git a/packages/auths-node/src/sign.rs b/packages/auths-node/src/sign.rs index c096b0b2..e7bab8ca 100644 --- a/packages/auths-node/src/sign.rs +++ b/packages/auths-node/src/sign.rs @@ -218,8 +218,12 @@ pub fn sign_action_as_agent( /// Decode a hex-encoded Ed25519 seed and validate its length. fn decode_seed_hex(private_key_hex: &str) -> napi::Result> { - let seed = hex::decode(private_key_hex) - .map_err(|e| format_error("AUTHS_INVALID_INPUT", format!("Invalid private key hex: {e}")))?; + let seed = hex::decode(private_key_hex).map_err(|e| { + format_error( + "AUTHS_INVALID_INPUT", + format!("Invalid private key hex: {e}"), + ) + })?; if seed.len() != 32 { return Err(format_error( "AUTHS_INVALID_INPUT", @@ -306,9 +310,9 @@ pub fn sign_action_raw( environment: None, }; - let canonical = envelope.canonical_bytes().map_err(|e| { - format_error("AUTHS_SERIALIZATION_ERROR", e) - })?; + let canonical = envelope + .canonical_bytes() + .map_err(|e| format_error("AUTHS_SERIALIZATION_ERROR", e))?; let keypair = Ed25519KeyPair::from_seed_unchecked(&seed).map_err(|e| { format_error( diff --git a/packages/auths-node/src/verify.rs b/packages/auths-node/src/verify.rs index fc90c574..eb32e5f0 100644 --- a/packages/auths-node/src/verify.rs +++ b/packages/auths-node/src/verify.rs @@ -488,9 +488,9 @@ pub fn verify_action_envelope( let sig_bytes = hex::decode(&envelope.signature) .map_err(|e| format_error("AUTHS_INVALID_INPUT", format!("Invalid signature hex: {e}")))?; - let canonical = envelope.canonical_bytes().map_err(|e| { - format_error("AUTHS_SERIALIZATION_ERROR", e) - })?; + let canonical = envelope + .canonical_bytes() + .map_err(|e| format_error("AUTHS_SERIALIZATION_ERROR", e))?; let key = ring::signature::UnparsedPublicKey::new(&ring::signature::ED25519, &pk_bytes); match key.verify(&canonical, &sig_bytes) { diff --git a/packages/auths-python/src/artifact_sign.rs b/packages/auths-python/src/artifact_sign.rs index f46b8009..244f6669 100644 --- a/packages/auths-python/src/artifact_sign.rs +++ b/packages/auths-python/src/artifact_sign.rs @@ -236,7 +236,9 @@ pub fn sign_artifact( let rp = repo_path.to_string(); { - build_context_and_sign(artifact, &alias, &rp, passphrase, expires_in, note, commit_sha) + build_context_and_sign( + artifact, &alias, &rp, passphrase, expires_in, note, commit_sha, + ) } } @@ -273,7 +275,9 @@ pub fn sign_artifact_bytes( let rp = repo_path.to_string(); { - build_context_and_sign(artifact, &alias, &rp, passphrase, expires_in, note, commit_sha) + build_context_and_sign( + artifact, &alias, &rp, passphrase, expires_in, note, commit_sha, + ) } } @@ -311,9 +315,12 @@ pub fn sign_artifact_bytes_raw( let now = Utc::now(); - let result = sign_artifact_raw(now, &seed, &did, data, expires_in, note, commit_sha).map_err(|e| { - PyRuntimeError::new_err(format!("[AUTHS_SIGNING_FAILED] Artifact signing failed: {e}")) - })?; + let result = + sign_artifact_raw(now, &seed, &did, data, expires_in, note, commit_sha).map_err(|e| { + PyRuntimeError::new_err(format!( + "[AUTHS_SIGNING_FAILED] Artifact signing failed: {e}" + )) + })?; Ok(PyArtifactResult { attestation_json: result.attestation_json, diff --git a/packages/auths-python/src/lib.rs b/packages/auths-python/src/lib.rs index 562462e5..0ccd7c30 100644 --- a/packages/auths-python/src/lib.rs +++ b/packages/auths-python/src/lib.rs @@ -88,10 +88,7 @@ fn _native(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_function(wrap_pyfunction!(artifact_sign::sign_artifact, m)?)?; m.add_function(wrap_pyfunction!(artifact_sign::sign_artifact_bytes, m)?)?; - m.add_function(wrap_pyfunction!( - artifact_sign::sign_artifact_bytes_raw, - m - )?)?; + m.add_function(wrap_pyfunction!(artifact_sign::sign_artifact_bytes_raw, m)?)?; m.add_class::()?; m.add_function(wrap_pyfunction!(commit_sign::sign_commit, m)?)?; diff --git a/packages/auths-python/src/verify.rs b/packages/auths-python/src/verify.rs index 37c3b2fa..cbf6fca5 100644 --- a/packages/auths-python/src/verify.rs +++ b/packages/auths-python/src/verify.rs @@ -64,17 +64,17 @@ pub fn verify_attestation( }; match runtime().block_on(verify_with_keys(&att, &issuer_pk_bytes)) { - Ok(_) => Ok(VerificationResult { - valid: true, - error: None, - error_code: None, - }), - Err(e) => Ok(VerificationResult { - valid: false, - error_code: Some(e.error_code().to_string()), - error: Some(e.to_string()), - }), - } + Ok(_) => Ok(VerificationResult { + valid: true, + error: None, + error_code: None, + }), + Err(e) => Ok(VerificationResult { + valid: false, + error_code: Some(e.error_code().to_string()), + error: Some(e.to_string()), + }), + } } /// Verify a chain of attestations from a root identity to a leaf device. @@ -408,17 +408,17 @@ pub fn verify_at_time( }; match runtime().block_on(rust_verify_at_time(&att, &issuer_pk_bytes, at)) { - Ok(_) => Ok(VerificationResult { - valid: true, - error: None, - error_code: None, - }), - Err(e) => Ok(VerificationResult { - valid: false, - error_code: Some(e.error_code().to_string()), - error: Some(e.to_string()), - }), - } + Ok(_) => Ok(VerificationResult { + valid: true, + error: None, + error_code: None, + }), + Err(e) => Ok(VerificationResult { + valid: false, + error_code: Some(e.error_code().to_string()), + error: Some(e.to_string()), + }), + } } /// Verify an attestation at a specific historical timestamp with capability check. @@ -460,29 +460,29 @@ pub fn verify_at_time_with_capability( })?; match runtime().block_on(rust_verify_at_time(&att, &issuer_pk_bytes, at)) { - Ok(_) => { - if att.capabilities.contains(&cap) { - Ok(VerificationResult { - valid: true, - error: None, - error_code: None, - }) - } else { - Ok(VerificationResult { - valid: false, - error: Some(format!( - "Attestation does not grant required capability '{required_capability}'" - )), - error_code: Some("AUTHS_MISSING_CAPABILITY".to_string()), - }) - } + Ok(_) => { + if att.capabilities.contains(&cap) { + Ok(VerificationResult { + valid: true, + error: None, + error_code: None, + }) + } else { + Ok(VerificationResult { + valid: false, + error: Some(format!( + "Attestation does not grant required capability '{required_capability}'" + )), + error_code: Some("AUTHS_MISSING_CAPABILITY".to_string()), + }) } - Err(e) => Ok(VerificationResult { - valid: false, - error_code: Some(e.error_code().to_string()), - error: Some(e.to_string()), - }), } + Err(e) => Ok(VerificationResult { + valid: false, + error_code: Some(e.error_code().to_string()), + error: Some(e.to_string()), + }), + } } /// Verify a chain of attestations with witness receipt quorum enforcement.