diff --git a/packages/auths-node/index.d.ts b/packages/auths-node/index.d.ts index 889285dc..44dbf586 100644 --- a/packages/auths-node/index.d.ts +++ b/packages/auths-node/index.d.ts @@ -230,6 +230,22 @@ export declare function signActionAsAgent(actionType: string, payloadJson: strin export declare function signActionAsIdentity(actionType: string, payloadJson: string, identityDid: string, repoPath: string, passphrase?: string | undefined | null): NapiActionEnvelope +/** + * Sign an action envelope with a hex-encoded Ed25519 private key. + * + * Args: + * * `private_key_hex`: Ed25519 seed as hex string (64 chars = 32 bytes). + * * `action_type`: Application-defined action type (e.g. "tool_call"). + * * `payload_json`: JSON string for the payload field. + * * `identity_did`: Signer's identity DID (e.g. "did:keri:E..."). + * + * Usage: + * ```ignore + * let envelope = sign_action_raw("deadbeef...".into(), "tool_call".into(), "{}".into(), "did:keri:E...".into())?; + * ``` + */ +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 signArtifactBytes(data: Buffer, identityKeyAlias: string, repoPath: string, passphrase?: string | undefined | null, expiresIn?: number | undefined | null, note?: string | undefined | null): NapiArtifactResult @@ -238,8 +254,36 @@ export declare function signAsAgent(message: Buffer, keyAlias: string, repoPath: export declare function signAsIdentity(message: Buffer, identityDid: string, repoPath: string, passphrase?: string | undefined | null): NapiCommitSignResult +/** + * Sign raw bytes with a hex-encoded Ed25519 private key. + * + * Args: + * * `private_key_hex`: Ed25519 seed as hex string (64 chars = 32 bytes). + * * `message`: The bytes to sign. + * + * Usage: + * ```ignore + * let sig = sign_bytes_raw("deadbeef...".into(), buffer)?; + * ``` + */ +export declare function signBytesRaw(privateKeyHex: string, message: Buffer): string + export declare function signCommit(data: Buffer, identityKeyAlias: string, repoPath: string, passphrase?: string | undefined | null): NapiCommitSignPemResult +/** + * Verify an action envelope's Ed25519 signature with a raw public key. + * + * Args: + * * `envelope_json`: The complete action envelope as a JSON string. + * * `public_key_hex`: The signer's Ed25519 public key in hex format (64 chars). + * + * Usage: + * ```ignore + * let result = verify_action_envelope("{...}".into(), "abcd1234...".into())?; + * ``` + */ +export declare function verifyActionEnvelope(envelopeJson: string, publicKeyHex: string): NapiVerificationResult + export declare function verifyAttestation(attestationJson: string, issuerPkHex: string): Promise export declare function verifyAttestationWithCapability(attestationJson: string, issuerPkHex: string, requiredCapability: string): Promise diff --git a/packages/auths-node/index.js b/packages/auths-node/index.js index f9a987aa..9a6055f7 100644 --- a/packages/auths-node/index.js +++ b/packages/auths-node/index.js @@ -606,11 +606,14 @@ module.exports.rotateIdentityKeys = nativeBinding.rotateIdentityKeys module.exports.runDiagnostics = nativeBinding.runDiagnostics module.exports.signActionAsAgent = nativeBinding.signActionAsAgent module.exports.signActionAsIdentity = nativeBinding.signActionAsIdentity +module.exports.signActionRaw = nativeBinding.signActionRaw module.exports.signArtifact = nativeBinding.signArtifact module.exports.signArtifactBytes = nativeBinding.signArtifactBytes module.exports.signAsAgent = nativeBinding.signAsAgent module.exports.signAsIdentity = nativeBinding.signAsIdentity +module.exports.signBytesRaw = nativeBinding.signBytesRaw module.exports.signCommit = nativeBinding.signCommit +module.exports.verifyActionEnvelope = nativeBinding.verifyActionEnvelope module.exports.verifyAttestation = nativeBinding.verifyAttestation module.exports.verifyAttestationWithCapability = nativeBinding.verifyAttestationWithCapability module.exports.verifyAtTime = nativeBinding.verifyAtTime diff --git a/packages/auths-node/lib/index.ts b/packages/auths-node/lib/index.ts index b6703983..34518d17 100644 --- a/packages/auths-node/lib/index.ts +++ b/packages/auths-node/lib/index.ts @@ -123,3 +123,6 @@ export { import native from './native' export const version: () => string = native.version +export const signBytesRaw: (privateKeyHex: string, message: Buffer) => string = native.signBytesRaw +export const signActionRaw: (privateKeyHex: string, actionType: string, payloadJson: string, identityDid: string) => string = native.signActionRaw +export const verifyActionEnvelope: (envelopeJson: string, publicKeyHex: string) => { valid: boolean; error?: string | null; errorCode?: string | null } = native.verifyActionEnvelope diff --git a/packages/auths-node/lib/native.ts b/packages/auths-node/lib/native.ts index eb02dcd5..e6913b23 100644 --- a/packages/auths-node/lib/native.ts +++ b/packages/auths-node/lib/native.ts @@ -192,6 +192,8 @@ export interface NativeBindings { signActionAsIdentity(actionType: string, payloadJson: string, identityDid: string, repoPath: string, passphrase?: string | null): NapiActionEnvelope signAsAgent(message: Buffer, keyAlias: string, repoPath: string, passphrase?: string | null): NapiCommitSignResult signActionAsAgent(actionType: string, payloadJson: string, keyAlias: string, agentDid: string, repoPath: string, passphrase?: string | null): NapiActionEnvelope + signBytesRaw(privateKeyHex: string, message: Buffer): string + signActionRaw(privateKeyHex: string, actionType: string, payloadJson: string, identityDid: string): string // Commit signing signCommit(data: Buffer, identityKeyAlias: string, repoPath: string, passphrase?: string | null): NapiCommitSignPemResult @@ -239,6 +241,7 @@ export interface NativeBindings { joinPairingSession(shortCode: string, endpoint: string, token: string, repoPath: string, deviceName?: string | null, passphrase?: string | null): Promise // Verification + verifyActionEnvelope(envelopeJson: string, publicKeyHex: string): NapiVerificationResult verifyAttestation(attestationJson: string, issuerPkHex: string): Promise verifyChain(attestationsJson: string[], rootPkHex: string): Promise verifyDeviceAuthorization(identityDid: string, deviceDid: string, attestationsJson: string[], identityPkHex: string): Promise diff --git a/packages/auths-node/src/sign.rs b/packages/auths-node/src/sign.rs index 492d0a93..c096b0b2 100644 --- a/packages/auths-node/src/sign.rs +++ b/packages/auths-node/src/sign.rs @@ -1,8 +1,10 @@ use auths_core::signing::{PrefilledPassphraseProvider, SecureSigner, StorageSigner}; use auths_core::storage::keychain::{KeyAlias, get_platform_keychain_with_config}; +use auths_verifier::action::ActionEnvelope; use auths_verifier::core::MAX_ATTESTATION_JSON_SIZE; use auths_verifier::types::IdentityDID; use napi_derive::napi; +use ring::signature::Ed25519KeyPair; use crate::error::format_error; use crate::helpers::{make_env_config, resolve_passphrase}; @@ -213,3 +215,115 @@ pub fn sign_action_as_agent( signer_did: agent_did, }) } + +/// 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}")))?; + if seed.len() != 32 { + return Err(format_error( + "AUTHS_INVALID_INPUT", + format!( + "Invalid private key length: expected 32 bytes (64 hex chars), got {}", + seed.len() + ), + )); + } + Ok(seed) +} + +/// Sign raw bytes with a hex-encoded Ed25519 private key. +/// +/// Args: +/// * `private_key_hex`: Ed25519 seed as hex string (64 chars = 32 bytes). +/// * `message`: The bytes to sign. +/// +/// Usage: +/// ```ignore +/// let sig = sign_bytes_raw("deadbeef...".into(), buffer)?; +/// ``` +#[napi] +pub fn sign_bytes_raw( + private_key_hex: String, + message: napi::bindgen_prelude::Buffer, +) -> napi::Result { + let seed = decode_seed_hex(&private_key_hex)?; + let keypair = Ed25519KeyPair::from_seed_unchecked(&seed).map_err(|e| { + format_error( + "AUTHS_CRYPTO_ERROR", + format!("Failed to create keypair: {e}"), + ) + })?; + let sig = keypair.sign(message.as_ref()); + Ok(hex::encode(sig.as_ref())) +} + +/// Sign an action envelope with a hex-encoded Ed25519 private key. +/// +/// Args: +/// * `private_key_hex`: Ed25519 seed as hex string (64 chars = 32 bytes). +/// * `action_type`: Application-defined action type (e.g. "tool_call"). +/// * `payload_json`: JSON string for the payload field. +/// * `identity_did`: Signer's identity DID (e.g. "did:keri:E..."). +/// +/// Usage: +/// ```ignore +/// let envelope = sign_action_raw("deadbeef...".into(), "tool_call".into(), "{}".into(), "did:keri:E...".into())?; +/// ``` +#[napi] +pub fn sign_action_raw( + private_key_hex: String, + action_type: String, + payload_json: String, + identity_did: String, +) -> napi::Result { + if payload_json.len() > MAX_ATTESTATION_JSON_SIZE { + return Err(format_error( + "AUTHS_INVALID_INPUT", + format!( + "Payload JSON too large: {} bytes, max {MAX_ATTESTATION_JSON_SIZE}", + payload_json.len() + ), + )); + } + + let payload: serde_json::Value = serde_json::from_str(&payload_json) + .map_err(|e| format_error("AUTHS_INVALID_INPUT", format!("Invalid payload JSON: {e}")))?; + + let seed = decode_seed_hex(&private_key_hex)?; + + #[allow(clippy::disallowed_methods)] // Presentation boundary + let timestamp = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true); + + let mut envelope = ActionEnvelope { + version: "1.0".into(), + action_type, + identity: identity_did, + payload, + timestamp, + signature: String::new(), + attestation_chain: None, + environment: None, + }; + + 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( + "AUTHS_CRYPTO_ERROR", + format!("Failed to create keypair: {e}"), + ) + })?; + + let sig = keypair.sign(&canonical); + envelope.signature = hex::encode(sig.as_ref()); + + serde_json::to_string(&envelope).map_err(|e| { + format_error( + "AUTHS_SERIALIZATION_ERROR", + format!("Failed to serialize envelope: {e}"), + ) + }) +} diff --git a/packages/auths-node/src/verify.rs b/packages/auths-node/src/verify.rs index 555e444c..fc90c574 100644 --- a/packages/auths-node/src/verify.rs +++ b/packages/auths-node/src/verify.rs @@ -1,3 +1,4 @@ +use auths_verifier::action::ActionEnvelope; use auths_verifier::core::{ Attestation, Capability, MAX_ATTESTATION_JSON_SIZE, MAX_JSON_BATCH_SIZE, }; @@ -445,3 +446,63 @@ pub async fn verify_chain_with_witnesses( )), } } + +/// Verify an action envelope's Ed25519 signature with a raw public key. +/// +/// Args: +/// * `envelope_json`: The complete action envelope as a JSON string. +/// * `public_key_hex`: The signer's Ed25519 public key in hex format (64 chars). +/// +/// Usage: +/// ```ignore +/// let result = verify_action_envelope("{...}".into(), "abcd1234...".into())?; +/// ``` +#[napi] +pub fn verify_action_envelope( + envelope_json: String, + public_key_hex: String, +) -> napi::Result { + if envelope_json.len() > MAX_ATTESTATION_JSON_SIZE { + return Err(format_error( + "AUTHS_INVALID_INPUT", + format!( + "Envelope JSON too large: {} bytes, max {MAX_ATTESTATION_JSON_SIZE}", + envelope_json.len() + ), + )); + } + + let pk_bytes = decode_pk_hex(&public_key_hex, "public key")?; + + let envelope: ActionEnvelope = serde_json::from_str(&envelope_json) + .map_err(|e| format_error("AUTHS_INVALID_INPUT", format!("Invalid envelope JSON: {e}")))?; + + if envelope.version != "1.0" { + return Ok(NapiVerificationResult { + valid: false, + error: Some(format!("Unsupported version: {}", envelope.version)), + error_code: Some("AUTHS_INVALID_INPUT".to_string()), + }); + } + + 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 key = ring::signature::UnparsedPublicKey::new(&ring::signature::ED25519, &pk_bytes); + match key.verify(&canonical, &sig_bytes) { + Ok(()) => Ok(NapiVerificationResult { + valid: true, + error: None, + error_code: None, + }), + Err(_) => Ok(NapiVerificationResult { + valid: false, + error: Some("Ed25519 signature verification failed".to_string()), + error_code: Some("AUTHS_ISSUER_SIG_FAILED".to_string()), + }), + } +} diff --git a/packages/auths-python/Cargo.lock b/packages/auths-python/Cargo.lock index 6271dbf5..ebf1c9fa 100644 --- a/packages/auths-python/Cargo.lock +++ b/packages/auths-python/Cargo.lock @@ -124,7 +124,7 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "auths-core" -version = "0.0.1-rc.10" +version = "0.1.0" dependencies = [ "aes-gcm", "argon2", @@ -169,7 +169,7 @@ dependencies = [ [[package]] name = "auths-crypto" -version = "0.0.1-rc.10" +version = "0.1.0" dependencies = [ "async-trait", "base64", @@ -183,7 +183,7 @@ dependencies = [ [[package]] name = "auths-id" -version = "0.0.1-rc.10" +version = "0.1.0" dependencies = [ "async-trait", "auths-core", @@ -219,7 +219,7 @@ dependencies = [ [[package]] name = "auths-infra-git" -version = "0.0.1-rc.10" +version = "0.1.0" dependencies = [ "auths-core", "auths-sdk", @@ -232,7 +232,7 @@ dependencies = [ [[package]] name = "auths-infra-http" -version = "0.0.1-rc.10" +version = "0.1.0" dependencies = [ "async-trait", "auths-core", @@ -258,7 +258,7 @@ dependencies = [ [[package]] name = "auths-oidc-port" -version = "0.0.1-rc.10" +version = "0.1.0" dependencies = [ "async-trait", "auths-crypto", @@ -271,7 +271,7 @@ dependencies = [ [[package]] name = "auths-pairing-daemon" -version = "0.0.1-rc.10" +version = "0.1.0" dependencies = [ "auths-core", "axum", @@ -289,7 +289,7 @@ dependencies = [ [[package]] name = "auths-pairing-protocol" -version = "0.0.1-rc.10" +version = "0.1.0" dependencies = [ "auths-crypto", "base64", @@ -308,7 +308,7 @@ dependencies = [ [[package]] name = "auths-policy" -version = "0.0.1-rc.10" +version = "0.1.0" dependencies = [ "auths-verifier", "blake3", @@ -348,7 +348,7 @@ dependencies = [ [[package]] name = "auths-sdk" -version = "0.0.1-rc.10" +version = "0.1.0" dependencies = [ "async-trait", "auths-core", @@ -381,7 +381,7 @@ dependencies = [ [[package]] name = "auths-storage" -version = "0.0.1-rc.10" +version = "0.1.0" dependencies = [ "async-trait", "auths-core", @@ -404,7 +404,7 @@ dependencies = [ [[package]] name = "auths-telemetry" -version = "0.0.1-rc.10" +version = "0.1.0" dependencies = [ "chrono", "metrics", @@ -419,7 +419,7 @@ dependencies = [ [[package]] name = "auths-transparency" -version = "0.0.1-rc.10" +version = "0.1.0" dependencies = [ "async-trait", "auths-crypto", @@ -439,7 +439,7 @@ dependencies = [ [[package]] name = "auths-utils" -version = "0.0.1-rc.10" +version = "0.1.0" dependencies = [ "dirs", "thiserror 2.0.18", @@ -447,7 +447,7 @@ dependencies = [ [[package]] name = "auths-verifier" -version = "0.0.1-rc.10" +version = "0.1.0" dependencies = [ "async-trait", "auths-crypto",