Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions packages/auths-node/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<NapiVerificationResult>

export declare function verifyAttestationWithCapability(attestationJson: string, issuerPkHex: string, requiredCapability: string): Promise<NapiVerificationResult>
Expand Down
3 changes: 3 additions & 0 deletions packages/auths-node/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions packages/auths-node/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 3 additions & 0 deletions packages/auths-node/lib/native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -239,6 +241,7 @@ export interface NativeBindings {
joinPairingSession(shortCode: string, endpoint: string, token: string, repoPath: string, deviceName?: string | null, passphrase?: string | null): Promise<NapiPairingResponse>

// Verification
verifyActionEnvelope(envelopeJson: string, publicKeyHex: string): NapiVerificationResult
verifyAttestation(attestationJson: string, issuerPkHex: string): Promise<NapiVerificationResult>
verifyChain(attestationsJson: string[], rootPkHex: string): Promise<NapiVerificationReport>
verifyDeviceAuthorization(identityDid: string, deviceDid: string, attestationsJson: string[], identityPkHex: string): Promise<NapiVerificationReport>
Expand Down
114 changes: 114 additions & 0 deletions packages/auths-node/src/sign.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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<Vec<u8>> {
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<String> {
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<String> {
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}"),
)
})
}
61 changes: 61 additions & 0 deletions packages/auths-node/src/verify.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use auths_verifier::action::ActionEnvelope;
use auths_verifier::core::{
Attestation, Capability, MAX_ATTESTATION_JSON_SIZE, MAX_JSON_BATCH_SIZE,
};
Expand Down Expand Up @@ -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<NapiVerificationResult> {
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()),
}),
}
}
Loading
Loading