use wasm_bindgen::prelude::*;
use crate::checkpoint::Checkpoint;
use crate::commitment::{Commitment, Opening};
use crate::coniks::{AbsenceProof, LookupProof, Namespace};
use crate::leaf::key_history_v1::Entry;
use crate::note::{SignedNote, VerifierKey};
use crate::policy::{CommitmentHash, NamespacePolicy, SignedPolicy};
use crate::proof::{verify_consistency, verify_inclusion};
use crate::vrf::{Ecvrf, VrfPublicKey};
use metamorphic_crypto::b64;
#[wasm_bindgen(js_name = "verifyInclusion")]
pub fn verify_inclusion_wasm(
index: u64,
size: u64,
leaf_hash_b64: &str,
proof_b64: Vec<String>,
root_b64: &str,
) -> Result<bool, JsValue> {
let leaf = decode(leaf_hash_b64)?;
let proof = decode_proof(&proof_b64)?;
let root = decode(root_b64)?;
verify_inclusion(index, size, &leaf, &proof, &root).map_err(to_js)?;
Ok(true)
}
#[wasm_bindgen(js_name = "verifyConsistency")]
pub fn verify_consistency_wasm(
size1: u64,
size2: u64,
proof_b64: Vec<String>,
root1_b64: &str,
root2_b64: &str,
) -> Result<bool, JsValue> {
let proof = decode_proof(&proof_b64)?;
let root1 = decode(root1_b64)?;
let root2 = decode(root2_b64)?;
verify_consistency(size1, size2, &proof, &root1, &root2).map_err(to_js)?;
Ok(true)
}
#[wasm_bindgen(js_name = "keyHistoryV1CanonicalBytes")]
pub fn key_history_v1_canonical_bytes(
seq: u64,
ts_ms: u64,
enc_x25519_b64: &str,
enc_pq_b64: &str,
signing_pub_b64: &str,
prev_entry_hash_b64: Option<String>,
) -> Result<String, JsValue> {
let entry = build_entry(
seq,
ts_ms,
enc_x25519_b64,
enc_pq_b64,
signing_pub_b64,
prev_entry_hash_b64,
)?;
Ok(b64::encode(&entry.canonical_bytes().map_err(to_js)?))
}
#[wasm_bindgen(js_name = "keyHistoryV1EntryHash")]
pub fn key_history_v1_entry_hash(
seq: u64,
ts_ms: u64,
enc_x25519_b64: &str,
enc_pq_b64: &str,
signing_pub_b64: &str,
prev_entry_hash_b64: Option<String>,
) -> Result<String, JsValue> {
let entry = build_entry(
seq,
ts_ms,
enc_x25519_b64,
enc_pq_b64,
signing_pub_b64,
prev_entry_hash_b64,
)?;
Ok(b64::encode(&entry.entry_hash().map_err(to_js)?))
}
#[wasm_bindgen(js_name = "keyHistoryV1Rfc6962LeafHash")]
pub fn key_history_v1_rfc6962_leaf_hash(
seq: u64,
ts_ms: u64,
enc_x25519_b64: &str,
enc_pq_b64: &str,
signing_pub_b64: &str,
prev_entry_hash_b64: Option<String>,
) -> Result<String, JsValue> {
let entry = build_entry(
seq,
ts_ms,
enc_x25519_b64,
enc_pq_b64,
signing_pub_b64,
prev_entry_hash_b64,
)?;
Ok(b64::encode(&entry.rfc6962_leaf_hash().map_err(to_js)?))
}
#[wasm_bindgen(js_name = "verifySignedNote")]
pub fn verify_signed_note(note_text: &str, vkeys: Vec<String>) -> Result<u32, JsValue> {
let trusted = parse_vkeys(&vkeys)?;
let note = SignedNote::parse(note_text).map_err(to_js)?;
let verified = note.verify(&trusted).map_err(to_js)?;
Ok(verified.len() as u32)
}
#[wasm_bindgen(js_name = "checkpointVerify")]
pub fn checkpoint_verify(note_text: &str, vkeys: Vec<String>) -> Result<JsValue, JsValue> {
let trusted = parse_vkeys(&vkeys)?;
let cp = Checkpoint::from_signed_note(note_text, &trusted).map_err(to_js)?;
Ok(checkpoint_to_js(&cp))
}
#[wasm_bindgen(js_name = "checkpointParse")]
pub fn checkpoint_parse(body_text: &str) -> Result<JsValue, JsValue> {
let cp = Checkpoint::parse(body_text).map_err(to_js)?;
Ok(checkpoint_to_js(&cp))
}
#[wasm_bindgen(js_name = "checkpointVerifyInclusion")]
pub fn checkpoint_verify_inclusion(
note_text: &str,
vkeys: Vec<String>,
leaf_index: u64,
leaf_hash_b64: &str,
proof_b64: Vec<String>,
) -> Result<bool, JsValue> {
let trusted = parse_vkeys(&vkeys)?;
let cp = Checkpoint::from_signed_note(note_text, &trusted).map_err(to_js)?;
let leaf = decode(leaf_hash_b64)?;
let proof = decode_proof(&proof_b64)?;
cp.verify_inclusion(leaf_index, &leaf, &proof)
.map_err(to_js)?;
Ok(true)
}
#[wasm_bindgen(js_name = "checkpointVerifyConsistency")]
pub fn checkpoint_verify_consistency(
older_note: &str,
newer_note: &str,
vkeys: Vec<String>,
proof_b64: Vec<String>,
) -> Result<bool, JsValue> {
let trusted = parse_vkeys(&vkeys)?;
let older = Checkpoint::from_signed_note(older_note, &trusted).map_err(to_js)?;
let newer = Checkpoint::from_signed_note(newer_note, &trusted).map_err(to_js)?;
let proof = decode_proof(&proof_b64)?;
older.verify_consistency(&newer, &proof).map_err(to_js)?;
Ok(true)
}
#[wasm_bindgen(js_name = "coniksVerifyLookup")]
pub fn coniks_verify_lookup(
namespace: &str,
vrf_public_b64: &str,
root_b64: &str,
identity_b64: &str,
proof_b64: &str,
) -> Result<String, JsValue> {
let ns = Namespace::parse(namespace).map_err(to_js)?;
let vrf_public = VrfPublicKey::from_bytes(decode(vrf_public_b64)?);
let root = decode_array_64(root_b64)?;
let identity = decode(identity_b64)?;
let proof = LookupProof::from_bytes(&decode(proof_b64)?).map_err(to_js)?;
let value = crate::coniks::verify_lookup(&Ecvrf, &ns, &vrf_public, &root, &identity, &proof)
.map_err(to_js)?;
Ok(b64::encode(&value))
}
#[wasm_bindgen(js_name = "coniksVerifyAbsence")]
pub fn coniks_verify_absence(
namespace: &str,
vrf_public_b64: &str,
root_b64: &str,
identity_b64: &str,
proof_b64: &str,
) -> Result<bool, JsValue> {
let ns = Namespace::parse(namespace).map_err(to_js)?;
let vrf_public = VrfPublicKey::from_bytes(decode(vrf_public_b64)?);
let root = decode_array_64(root_b64)?;
let identity = decode(identity_b64)?;
let proof = AbsenceProof::from_bytes(&decode(proof_b64)?).map_err(to_js)?;
crate::coniks::verify_absence(&Ecvrf, &ns, &vrf_public, &root, &identity, &proof)
.map_err(to_js)?;
Ok(true)
}
#[wasm_bindgen(js_name = "verifyCommitment")]
pub fn verify_commitment_wasm(
context: &str,
commitment_b64: &str,
value_b64: &str,
opening_b64: &str,
) -> Result<bool, JsValue> {
let commitment = Commitment::from_bytes(decode_array_64(commitment_b64)?);
let value = decode(value_b64)?;
let opening = Opening::from_bytes(decode_array_32(opening_b64)?);
crate::commitment::verify_commitment(context, &commitment, &value, &opening).map_err(to_js)?;
Ok(true)
}
#[wasm_bindgen(js_name = "signedPolicyVerify")]
pub fn signed_policy_verify(signed_b64: &str) -> Result<JsValue, JsValue> {
let signed = SignedPolicy::parse(&decode(signed_b64)?).map_err(to_js)?;
let policy = signed.verify().map_err(to_js)?;
policy_to_js(policy)
}
#[wasm_bindgen(js_name = "policyEnforceCheckpointSigningKey")]
pub fn policy_enforce_checkpoint_signing_key(
signed_b64: &str,
public_key_b64: &str,
) -> Result<bool, JsValue> {
let policy = verified_policy(signed_b64)?;
policy
.enforce_checkpoint_signing_key(public_key_b64)
.map_err(to_js)?;
Ok(true)
}
#[wasm_bindgen(js_name = "policyEnforceCheckpointSignature")]
pub fn policy_enforce_checkpoint_signature(
signed_b64: &str,
signature_b64: &str,
) -> Result<bool, JsValue> {
let policy = verified_policy(signed_b64)?;
policy
.enforce_checkpoint_signature(signature_b64)
.map_err(to_js)?;
Ok(true)
}
#[wasm_bindgen(js_name = "policyEnforceVrfSuiteId")]
pub fn policy_enforce_vrf_suite_id(
signed_b64: &str,
observed_suite_id: u8,
) -> Result<bool, JsValue> {
let policy = verified_policy(signed_b64)?;
policy
.enforce_vrf_suite_id(observed_suite_id)
.map_err(to_js)?;
Ok(true)
}
#[wasm_bindgen(js_name = "policyEnforceCommitmentHash")]
pub fn policy_enforce_commitment_hash(signed_b64: &str, observed: &str) -> Result<bool, JsValue> {
let policy = verified_policy(signed_b64)?;
policy
.enforce_commitment_hash(parse_commitment_hash(observed)?)
.map_err(to_js)?;
Ok(true)
}
fn decode(s: &str) -> Result<Vec<u8>, JsValue> {
b64::decode(s).map_err(|e| JsValue::from_str(&format!("base64 decode error: {e}")))
}
fn decode_proof(proof_b64: &[String]) -> Result<Vec<Vec<u8>>, JsValue> {
proof_b64.iter().map(|s| decode(s)).collect()
}
fn decode_array_64(s: &str) -> Result<[u8; 64], JsValue> {
let v = decode(s)?;
v.try_into()
.map_err(|_| JsValue::from_str("expected 64 bytes"))
}
fn decode_array_32(s: &str) -> Result<[u8; 32], JsValue> {
let v = decode(s)?;
v.try_into()
.map_err(|_| JsValue::from_str("expected 32 bytes"))
}
fn parse_vkeys(vkeys: &[String]) -> Result<Vec<VerifierKey>, JsValue> {
vkeys
.iter()
.map(|v| VerifierKey::parse(v).map_err(to_js))
.collect()
}
fn build_entry(
seq: u64,
ts_ms: u64,
enc_x25519_b64: &str,
enc_pq_b64: &str,
signing_pub_b64: &str,
prev_entry_hash_b64: Option<String>,
) -> Result<Entry, JsValue> {
let prev_entry_hash = match prev_entry_hash_b64 {
Some(ref s) if !s.is_empty() => Some(decode(s)?),
_ => None,
};
Ok(Entry {
seq,
ts_ms,
enc_x25519: decode(enc_x25519_b64)?,
enc_pq: decode(enc_pq_b64)?,
signing_pub: decode(signing_pub_b64)?,
prev_entry_hash,
})
}
fn parse_commitment_hash(s: &str) -> Result<CommitmentHash, JsValue> {
let normalized: String = s
.chars()
.filter(|c| *c != '_' && *c != '-')
.collect::<String>()
.to_ascii_lowercase();
match normalized.as_str() {
"sha3256" => Ok(CommitmentHash::Sha3_256),
"sha3512" => Ok(CommitmentHash::Sha3_512),
other => Err(JsValue::from_str(&format!(
"invalid commitment hash \"{other}\": expected \"sha3_256\" or \"sha3_512\""
))),
}
}
fn verified_policy(signed_b64: &str) -> Result<NamespacePolicy, JsValue> {
let signed = SignedPolicy::parse(&decode(signed_b64)?).map_err(to_js)?;
signed.verify().map_err(to_js)?;
Ok(signed.policy().clone())
}
fn checkpoint_to_js(cp: &Checkpoint) -> JsValue {
let obj = js_sys::Object::new();
set(&obj, "origin", &cp.origin().into());
set(&obj, "size", &(cp.size() as f64).into());
set(&obj, "rootB64", &b64::encode(cp.root_hash()).into());
let exts = js_sys::Array::new();
for ext in cp.extensions() {
exts.push(&JsValue::from_str(ext));
}
set(&obj, "extensions", &exts.into());
obj.into()
}
fn policy_to_js(policy: &NamespacePolicy) -> Result<JsValue, JsValue> {
use crate::policy::{CheckpointSuite, SecurityLevel, VrfMode};
let security_level = match policy.security_level() {
SecurityLevel::Cat3 => "cat3",
SecurityLevel::Cat5 => "cat5",
};
let checkpoint_suite = match policy.checkpoint_suite() {
CheckpointSuite::Hybrid => "hybrid",
CheckpointSuite::HybridMatched => "hybridMatched",
CheckpointSuite::PureCnsa2 => "pureCnsa2",
};
let commitment_hash = match policy.commitment_hash() {
CommitmentHash::Sha3_256 => "sha3_256",
CommitmentHash::Sha3_512 => "sha3_512",
};
let vrf_mode = match policy.vrf_mode() {
VrfMode::Classical => "classical",
VrfMode::HybridOutput => "hybridOutput",
VrfMode::PurePqExperimental => "purePqExperimental",
};
let obj = js_sys::Object::new();
set(&obj, "namespace", &policy.namespace().as_str().into());
set(
&obj,
"policySchemaVersion",
&(policy.policy_schema_version() as f64).into(),
);
set(&obj, "securityLevel", &security_level.into());
set(&obj, "checkpointSuite", &checkpoint_suite.into());
set(&obj, "commitmentHash", &commitment_hash.into());
set(&obj, "vrfMode", &vrf_mode.into());
set(
&obj,
"effectiveFrom",
&(policy.effective_from() as f64).into(),
);
set(&obj, "createdAt", &(policy.created_at() as f64).into());
set(
&obj,
"policyHashB64",
&b64::encode(&policy.policy_hash().map_err(to_js)?).into(),
);
set(
&obj,
"rfc6962LeafHashB64",
&b64::encode(&policy.rfc6962_leaf_hash()).into(),
);
Ok(obj.into())
}
fn set(obj: &js_sys::Object, key: &str, value: &JsValue) {
let _ = js_sys::Reflect::set(obj, &JsValue::from_str(key), value);
}
fn to_js(e: crate::Error) -> JsValue {
JsValue::from_str(&e.to_string())
}