use alloc::string::String;
use alloc::vec::Vec;
use chrono::{DateTime, Utc};
use keetanetwork_asn1::SubjectPublicKeyInfo;
use keetanetwork_bindings::error::CodedError;
use keetanetwork_bindings::{time as bindings_time, x509 as bindings_x509};
use keetanetwork_x509::builder::CertificateBuilder;
use keetanetwork_x509::certificates::{Certificate, CertificateBundle};
use keetanetwork_x509::error::CertificateError;
use keetanetwork_x509::{oids, utils, SerialNumber};
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::JsValue;
use crate::account::Account;
use crate::convert::{coded, coded_error, JsResult};
#[wasm_bindgen]
#[derive(Default)]
pub struct X509CertificateBuilder {
inner: CertificateBuilder,
}
#[wasm_bindgen]
impl X509CertificateBuilder {
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Self::default()
}
#[wasm_bindgen(js_name = forCa)]
pub fn for_ca() -> Self {
Self { inner: CertificateBuilder::for_ca() }
}
#[wasm_bindgen(js_name = forEndEntity)]
pub fn for_end_entity() -> Self {
Self { inner: CertificateBuilder::for_end_entity() }
}
#[wasm_bindgen(js_name = forServer)]
pub fn for_server() -> Self {
Self { inner: CertificateBuilder::for_server() }
}
#[wasm_bindgen(js_name = forClient)]
pub fn for_client() -> Self {
Self { inner: CertificateBuilder::for_client() }
}
#[wasm_bindgen(js_name = withSubjectCommonName)]
pub fn with_subject_common_name(&mut self, common_name: String) -> JsResult<()> {
let dn = utils::create_dn(&[(oids::CN, common_name.as_str())]).map_err(certificate_error)?;
self.apply(|builder| builder.with_subject_dn(dn));
Ok(())
}
#[wasm_bindgen(js_name = withIssuerCommonName)]
pub fn with_issuer_common_name(&mut self, common_name: String) -> JsResult<()> {
let dn = utils::create_dn(&[(oids::CN, common_name.as_str())]).map_err(certificate_error)?;
self.apply(|builder| builder.with_issuer_dn(dn));
Ok(())
}
#[wasm_bindgen(js_name = withSubjectPublicKeyFromAccount)]
pub fn with_subject_public_key_from_account(&mut self, account: &Account) -> JsResult<()> {
let public_key = subject_public_key(account)?;
self.apply(|builder| builder.with_subject_public_key(public_key));
Ok(())
}
#[wasm_bindgen(js_name = withSerialNumber)]
pub fn with_serial_number(&mut self, serial: u64) {
self.apply(|builder| builder.with_serial_number(SerialNumber::from(serial)));
}
#[wasm_bindgen(js_name = withValidityDays)]
pub fn with_validity_days(&mut self, days: u32) {
self.apply(|builder| builder.with_validity_days(u64::from(days)));
}
#[wasm_bindgen(js_name = withValidity)]
pub fn with_validity(&mut self, not_before_millis: f64, not_after_millis: f64) -> JsResult<()> {
let not_before = timestamp(not_before_millis, "notBefore")?;
let not_after = timestamp(not_after_millis, "notAfter")?;
self.apply(|builder| builder.with_validity(not_before, not_after));
Ok(())
}
#[wasm_bindgen(js_name = asSelfSigned)]
pub fn as_self_signed(&mut self) {
self.apply(CertificateBuilder::as_self_signed);
}
#[wasm_bindgen(js_name = withBasicConstraints)]
pub fn with_basic_constraints(&mut self, is_ca: bool, path_length: Option<u8>) {
self.apply(|builder| builder.with_basic_constraints(is_ca, path_length));
}
#[wasm_bindgen(js_name = withKeyUsage)]
pub fn with_key_usage(&mut self, bits: u16) {
self.apply(|builder| builder.with_key_usage(bits));
}
#[wasm_bindgen(js_name = withExtendedKeyUsage)]
pub fn with_extended_key_usage(&mut self, purpose_oids: Vec<String>) {
self.apply(|builder| builder.with_extended_key_usage(purpose_oids));
}
#[wasm_bindgen(js_name = withSubjectAltName)]
pub fn with_subject_alt_name(&mut self, entries: Vec<String>) {
self.apply(|builder| builder.with_subject_alt_name(entries));
}
#[wasm_bindgen(js_name = withCustomExtension)]
pub fn with_custom_extension(&mut self, oid: String, value: Vec<u8>, critical: bool) {
self.apply(|builder| builder.with_custom_extension(oid, value, critical));
}
pub fn build(&self, signer: &Account) -> JsResult<String> {
let certificate = bindings_x509::build_signed(&self.inner, &signer.inner()).map_err(coded)?;
let der = Vec::<u8>::try_from(&certificate).map_err(certificate_error)?;
Ok(hex::encode(der))
}
}
impl X509CertificateBuilder {
fn apply<F: FnOnce(CertificateBuilder) -> CertificateBuilder>(&mut self, transform: F) {
let current = core::mem::take(&mut self.inner);
self.inner = transform(current);
}
}
fn subject_public_key(account: &Account) -> JsResult<SubjectPublicKeyInfo> {
bindings_x509::subject_public_key(&account.inner()).map_err(coded)
}
fn timestamp(millis: f64, label: &str) -> JsResult<DateTime<Utc>> {
if !millis.is_finite() {
return Err(coded_error("INVALID_DATE", &alloc::format!("{label} unix milliseconds must be finite")));
}
let millis_i64 = millis as i64;
if millis_i64 as f64 != millis {
return Err(coded_error(
"INVALID_DATE",
&alloc::format!("{label} unix milliseconds must be an integer within i64 range"),
));
}
bindings_time::from_unix_millis(millis_i64, label).map_err(coded)
}
#[wasm_bindgen]
pub struct X509Certificate {
subject: String,
issuer: String,
serial_number: String,
not_before_millis: f64,
not_after_millis: f64,
chain_length: usize,
}
impl X509Certificate {
fn from_leaf(parsed: &Certificate, chain_length: usize) -> Self {
Self {
subject: parsed.to_subject(),
issuer: parsed.to_issuer(),
serial_number: hex::encode(parsed.tbs_certificate.serial_number.as_bytes()),
not_before_millis: parsed.to_not_before().timestamp_millis() as f64,
not_after_millis: parsed.to_not_after().timestamp_millis() as f64,
chain_length,
}
}
}
#[wasm_bindgen]
impl X509Certificate {
pub fn parse(certificate: String) -> JsResult<X509Certificate> {
let der =
hex::decode(&certificate).map_err(|_| coded_error("INVALID_CERTIFICATE", "certificate must be hex DER"))?;
let parsed = Certificate::try_from(der.as_slice()).map_err(certificate_error)?;
Ok(Self::from_leaf(&parsed, 1))
}
#[wasm_bindgen(js_name = parseChain)]
pub fn parse_chain(certificate: String, intermediates: Vec<String>) -> JsResult<X509Certificate> {
let leaf_der =
hex::decode(&certificate).map_err(|_| coded_error("INVALID_CERTIFICATE", "certificate must be hex DER"))?;
let mut bundle = CertificateBundle::try_from(leaf_der.as_slice()).map_err(certificate_error)?;
for intermediate in &intermediates {
let der = hex::decode(intermediate)
.map_err(|_| coded_error("INVALID_CERTIFICATE", "intermediate must be hex DER"))?;
let cert = Certificate::try_from(der.as_slice()).map_err(certificate_error)?;
bundle.add_intermediate(cert);
}
let chain_length = bundle.to_chain_length();
Ok(Self::from_leaf(bundle.to_certificate(), chain_length))
}
#[wasm_bindgen(getter)]
pub fn subject(&self) -> String {
self.subject.clone()
}
#[wasm_bindgen(getter)]
pub fn issuer(&self) -> String {
self.issuer.clone()
}
#[wasm_bindgen(getter, js_name = serialNumber)]
pub fn serial_number(&self) -> String {
self.serial_number.clone()
}
#[wasm_bindgen(getter, js_name = notBeforeMilliseconds)]
pub fn not_before_millis(&self) -> f64 {
self.not_before_millis
}
#[wasm_bindgen(getter, js_name = notAfterMilliseconds)]
pub fn not_after_millis(&self) -> f64 {
self.not_after_millis
}
#[wasm_bindgen(getter, js_name = chainLength)]
pub fn chain_length(&self) -> usize {
self.chain_length
}
}
#[cfg(all(test, target_family = "wasm"))]
mod wasm_tests {
use wasm_bindgen_test::wasm_bindgen_test;
use super::*;
use crate::certificate::ManageCertificate;
fn signing_account() -> Account {
let seed = Account::generate_seed().expect("seed generation must succeed");
Account::from_seed(seed, 0, Some(String::from("ed25519"))).expect("seeded account must derive")
}
#[wasm_bindgen_test]
fn self_signed_server_preset_builds_to_compliant_der() {
let account = signing_account();
let mut builder = X509CertificateBuilder::for_server();
builder
.with_subject_common_name(String::from("example.com"))
.expect("subject common name must set");
builder.as_self_signed();
builder
.with_subject_public_key_from_account(&account)
.expect("subject key must derive from a signing account");
builder.with_serial_number(1);
let der_hex = builder
.build(&account)
.expect("self-signed certificate must build");
let der = hex::decode(&der_hex).expect("built certificate must be hex");
assert!(Certificate::try_from(der).is_ok());
assert!(ManageCertificate::add(der_hex).is_ok());
}
#[wasm_bindgen_test]
fn build_requires_a_subject_public_key() {
let account = signing_account();
let mut builder = X509CertificateBuilder::new();
builder
.with_subject_common_name(String::from("example.com"))
.expect("subject common name must set");
builder.as_self_signed();
builder.with_serial_number(1);
builder.with_validity_days(365);
assert!(builder.build(&account).is_err());
}
#[wasm_bindgen_test]
fn parse_recovers_the_subject_issuer_and_serial() {
let account = signing_account();
let mut builder = X509CertificateBuilder::for_client();
builder
.with_subject_common_name(String::from("leaf.example"))
.expect("subject common name must set");
builder
.with_issuer_common_name(String::from("issuer.example"))
.expect("issuer common name must set");
builder
.with_subject_public_key_from_account(&account)
.expect("subject key must derive from a signing account");
builder.with_serial_number(7);
let der_hex = builder.build(&account).expect("certificate must build");
let parsed = X509Certificate::parse(der_hex).expect("built certificate must parse");
assert!(parsed.subject().contains("leaf.example"));
assert!(parsed.issuer().contains("issuer.example"));
assert_eq!(parsed.serial_number(), "07");
assert!(parsed.not_after_millis() > parsed.not_before_millis());
}
#[wasm_bindgen_test]
fn change_hash_matches_the_der_digest() {
let account = signing_account();
let mut builder = X509CertificateBuilder::for_client();
builder
.with_subject_common_name(String::from("leaf.example"))
.expect("subject common name must set");
builder.as_self_signed();
builder
.with_subject_public_key_from_account(&account)
.expect("subject key must derive from a signing account");
builder.with_serial_number(1);
let der_hex = builder.build(&account).expect("certificate must build");
let add = ManageCertificate::add(der_hex).expect("add change must assemble");
let remove = ManageCertificate::remove(add.hash()).expect("remove change must accept the add hash");
assert_eq!(add.hash().len(), 64);
assert_eq!(add.hash(), remove.hash());
}
#[wasm_bindgen_test]
fn parse_chain_links_the_leaf_to_its_issuer() {
let ca = signing_account();
let leaf = signing_account();
let mut ca_builder = X509CertificateBuilder::for_ca();
ca_builder
.with_subject_common_name(String::from("Test CA"))
.expect("ca subject common name must set");
ca_builder.as_self_signed();
ca_builder
.with_subject_public_key_from_account(&ca)
.expect("ca subject key must derive");
ca_builder.with_serial_number(1);
let ca_der = ca_builder.build(&ca).expect("ca certificate must build");
let mut leaf_builder = X509CertificateBuilder::for_client();
leaf_builder
.with_subject_common_name(String::from("leaf.example"))
.expect("leaf subject common name must set");
leaf_builder
.with_issuer_common_name(String::from("Test CA"))
.expect("leaf issuer common name must set");
leaf_builder
.with_subject_public_key_from_account(&leaf)
.expect("leaf subject key must derive");
leaf_builder.with_serial_number(2);
let leaf_der = leaf_builder
.build(&ca)
.expect("leaf certificate must build");
let chain = X509Certificate::parse_chain(leaf_der, alloc::vec![ca_der]).expect("chain must parse");
assert_eq!(chain.chain_length(), 2);
assert!(chain.subject().contains("leaf.example"));
assert!(chain.issuer().contains("Test CA"));
}
#[wasm_bindgen_test]
fn parse_chain_of_a_lone_leaf_has_unit_length() {
let account = signing_account();
let mut builder = X509CertificateBuilder::for_client();
builder
.with_subject_common_name(String::from("leaf.example"))
.expect("subject common name must set");
builder.as_self_signed();
builder
.with_subject_public_key_from_account(&account)
.expect("subject key must derive");
builder.with_serial_number(1);
let der_hex = builder.build(&account).expect("certificate must build");
let chain = X509Certificate::parse_chain(der_hex, alloc::vec![]).expect("chain must parse");
assert_eq!(chain.chain_length(), 1);
}
}
fn certificate_error(error: CertificateError) -> JsValue {
coded(CodedError::from(error))
}