use crate::CoreError;
use crate::envelope::decrypt_private_key;
use crate::material::{AgentMaterial, UnlockSecret};
use crate::sign::{DetachedSigner, Ed25519DalekSigner, Pq2025Signer, SigningAlgorithm};
use crate::verify::{
VerificationOutcome, build_signature_content_v2, build_signature_metadata,
default_signed_fields, sha256_hex, verify_document,
};
use base64::Engine as _;
use secrecy::ExposeSecret;
use serde_json::{Value, json};
const JACS_SIGNATURE_FIELDNAME: &str = "jacsSignature";
pub struct CoreAgent {
pub(crate) signer: Option<Box<dyn DetachedSigner>>,
pub(crate) algorithm: SigningAlgorithm,
pub(crate) public_key: Vec<u8>,
pub(crate) agent_json: Value,
}
impl std::fmt::Debug for CoreAgent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CoreAgent")
.field("algorithm", &self.algorithm)
.field("public_key_len", &self.public_key.len())
.field("unlocked", &self.signer.is_some())
.finish()
}
}
impl CoreAgent {
pub fn from_encrypted_material(
material: AgentMaterial,
secret: UnlockSecret<'_>,
) -> Result<Self, CoreError> {
let signer: Box<dyn DetachedSigner> = match secret {
UnlockSecret::Password(password) => {
let decrypted = decrypt_private_key(&material.encrypted_private_key, password)?;
build_signer(material.algorithm, decrypted.as_slice())?
}
UnlockSecret::RawPrivateKey(secret_box) => {
build_signer(material.algorithm, secret_box.expose_secret())?
}
};
if signer.public_key() != material.public_key.as_slice() {
return Err(CoreError::MalformedKey(
"stored public key does not match the key derived from the unlocked private key"
.into(),
));
}
Ok(Self {
signer: Some(signer),
algorithm: material.algorithm,
public_key: material.public_key,
agent_json: material.agent,
})
}
pub fn ephemeral(algorithm: SigningAlgorithm) -> Result<Self, CoreError> {
let signer: Box<dyn DetachedSigner> = match algorithm {
SigningAlgorithm::Ed25519 => Box::new(Ed25519DalekSigner::generate()?),
SigningAlgorithm::Pq2025 => Box::new(Pq2025Signer::generate()?),
};
let public_key = signer.public_key().to_vec();
let agent_json = ephemeral_agent_json(algorithm, &public_key);
Ok(Self {
signer: Some(signer),
algorithm,
public_key,
agent_json,
})
}
pub fn algorithm(&self) -> SigningAlgorithm {
self.algorithm
}
pub fn public_key(&self) -> &[u8] {
&self.public_key
}
pub fn is_unlocked(&self) -> bool {
self.signer.is_some()
}
pub fn clear_secrets(&mut self) {
if let Some(signer) = self.signer.as_mut() {
signer.clear_secrets();
}
self.signer = None;
}
pub fn export_agent(&self) -> Value {
self.agent_json.clone()
}
pub fn export_encrypted_material(&self, password: &str) -> Result<AgentMaterial, CoreError> {
let signer = self.signer.as_ref().ok_or(CoreError::Locked)?;
let raw_private = signer.export_private_key_bytes()?;
let encrypted = crate::envelope::encrypt_private_key(&raw_private, password)?;
use zeroize::Zeroize as _;
let mut raw_private = raw_private;
raw_private.zeroize();
Ok(AgentMaterial {
config: serde_json::json!({}),
agent: self.agent_json.clone(),
public_key: self.public_key.clone(),
encrypted_private_key: encrypted,
algorithm: self.algorithm,
})
}
pub fn sign_message(&mut self, data: &Value) -> Result<Value, CoreError> {
let mut document = json!({
"jacsType": "message",
"jacsLevel": "raw",
"content": data,
});
self.sign_document_inplace(&mut document, JACS_SIGNATURE_FIELDNAME)?;
Ok(document)
}
pub fn sign_document_inplace(
&mut self,
document: &mut Value,
placement_key: &str,
) -> Result<(), CoreError> {
let signer = self.signer.as_ref().ok_or(CoreError::Locked)?;
let algorithm = self.algorithm;
let public_key_hash = sha256_hex(&self.public_key);
let agent_id = self
.agent_json
.get("jacsId")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let agent_version = self
.agent_json
.get("jacsVersion")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let date = chrono::Utc::now().to_rfc3339();
let iat = chrono::Utc::now().timestamp();
let jti = uuid::Uuid::now_v7().to_string();
let fields = default_signed_fields(document, placement_key);
let metadata = build_signature_metadata(
&agent_id,
&agent_version,
&date,
iat,
&jti,
algorithm,
&public_key_hash,
&fields,
);
let canonical = build_signature_content_v2(document, &fields, placement_key, &metadata)?;
let sig_bytes = signer.sign(canonical.as_bytes())?;
let signature_b64 = base64::engine::general_purpose::STANDARD.encode(&sig_bytes);
let mut sig_object = metadata;
sig_object["signature"] = json!(signature_b64);
document
.as_object_mut()
.ok_or_else(|| {
CoreError::MalformedDocument(
"document must be a JSON object to attach a signature".into(),
)
})?
.insert(placement_key.to_string(), sig_object);
Ok(())
}
pub fn sign_raw_bytes(&self, bytes: &[u8]) -> Result<Vec<u8>, CoreError> {
let signer = self.signer.as_ref().ok_or(CoreError::Locked)?;
signer.sign(bytes)
}
pub fn verify_raw_bytes_with_key(
public_key: &[u8],
algorithm: SigningAlgorithm,
bytes: &[u8],
signature: &[u8],
) -> Result<bool, CoreError> {
match algorithm {
SigningAlgorithm::Ed25519 => {
match Ed25519DalekSigner::verify(public_key, bytes, signature) {
Ok(()) => Ok(true),
Err(CoreError::SignatureInvalid(_)) => Ok(false),
Err(other) => Err(other),
}
}
SigningAlgorithm::Pq2025 => match Pq2025Signer::verify(public_key, bytes, signature) {
Ok(()) => Ok(true),
Err(CoreError::SignatureInvalid(_)) => Ok(false),
Err(other) => Err(other),
},
}
}
pub fn verify(&self, signed: &Value) -> Result<VerificationOutcome, CoreError> {
verify_document(
signed,
&self.public_key,
self.algorithm,
JACS_SIGNATURE_FIELDNAME,
)
}
pub fn verify_with_key(
signed: &Value,
public_key: &[u8],
algorithm: SigningAlgorithm,
) -> Result<VerificationOutcome, CoreError> {
verify_document(signed, public_key, algorithm, JACS_SIGNATURE_FIELDNAME)
}
}
fn build_signer(
algorithm: SigningAlgorithm,
private_key_bytes: &[u8],
) -> Result<Box<dyn DetachedSigner>, CoreError> {
match algorithm {
SigningAlgorithm::Ed25519 => {
if private_key_bytes.len() == 32 {
Ok(Box::new(Ed25519DalekSigner::from_private_scalar(
private_key_bytes,
)?))
} else {
Ok(Box::new(Ed25519DalekSigner::from_pkcs8(private_key_bytes)?))
}
}
SigningAlgorithm::Pq2025 => Ok(Box::new(Pq2025Signer::from_private_bytes(
private_key_bytes,
)?)),
}
}
pub fn ephemeral_agent_json(algorithm: SigningAlgorithm, public_key: &[u8]) -> Value {
json!({
"jacsId": uuid::Uuid::new_v4().to_string(),
"jacsVersion": "v1",
"name": "ephemeral",
"algorithm": algorithm.as_str(),
"publicKeyLen": public_key.len(),
})
}