//! Create a PKCS#10 certificate signing request. use serde_json::json; use sycamore::prelude::*; use wasm_bindgen_futures::spawn_local; use crate::api; use crate::components::crypto::{ add_key_parameters, apply_signature_hash, key_parameter_input, key_type_select, signature_hash_input, }; use crate::components::nav::{app_nav, whoami}; use crate::components::ui::{page_header, toast_error}; use crate::model::{KeySummary, Me, OptionsPage}; pub fn RequestNewPage(vault_id: String) -> View { let id: &'static str = Box::leak(vault_id.clone().into_boxed_str()); let me = create_signal(None::); let options = create_signal(None::); let keys = create_signal(Vec::::new()); let error = create_signal(None::); let saving = create_signal(false); let name = create_signal(String::new()); let comment = create_signal(String::new()); let key_id = create_signal(String::new()); let new_key = create_signal(false); let key_type = create_signal("rsa".to_string()); let key_size = create_signal("2048".to_string()); let curve = create_signal("secp256r1".to_string()); let signature_hash = create_signal("sha256".to_string()); let cn = create_signal(String::new()); let country = create_signal(String::new()); let state = create_signal(String::new()); let locality = create_signal(String::new()); let organization = create_signal(String::new()); let org_unit = create_signal(String::new()); let email = create_signal(String::new()); let san_text = create_signal(String::new()); let challenge = create_signal(String::new()); let unstructured = create_signal(String::new()); let ku_digital = create_signal(true); let ku_encipher = create_signal(false); let ku_cert_sign = create_signal(false); let ku_crl_sign = create_signal(false); let eku_server = create_signal(false); let eku_client = create_signal(false); let eku_code = create_signal(false); let eku_email = create_signal(false); spawn_local(async move { if let Ok(user) = api::me().await { if me.is_alive() { me.set(Some(user)); } } if let Ok(page) = api::keys(&id).await { if keys.is_alive() { keys.set(page.results.into_iter().filter(|key| key.has_private).collect()); } } if let Ok(page) = api::options(id).await { if options.is_alive() { options.set(Some(page)); } } }); let signing_key_type = move || { if new_key.get() { return Some(key_type.get_clone()); } let selected = key_id.get_clone(); keys.get_clone() .into_iter() .find(|key| key.id == selected) .map(|key| key.key_type) }; let submit = move |_| { let request_name = name.get_clone(); if request_name.trim().is_empty() { error.set(Some("request name is required".to_string())); return; } let mut subject = Vec::new(); for (key, value) in [ ("C", country.get_clone()), ("ST", state.get_clone()), ("L", locality.get_clone()), ("O", organization.get_clone()), ("OU", org_unit.get_clone()), ("CN", cn.get_clone()), ("emailAddress", email.get_clone()), ] { if !value.trim().is_empty() { subject.push(json!({"name": key, "value": value.trim()})); } } if subject.is_empty() { error.set(Some("the subject must not be empty".to_string())); return; } let usages = [ (ku_digital.get(), "digital_signature"), (ku_encipher.get(), "key_encipherment"), (ku_cert_sign.get(), "key_cert_sign"), (ku_crl_sign.get(), "crl_sign"), ] .iter() .filter(|(checked, _)| *checked) .map(|(_, name)| json!(name)) .collect::>(); let eku = [ (eku_server.get(), "serverAuth"), (eku_client.get(), "clientAuth"), (eku_code.get(), "codeSigning"), (eku_email.get(), "emailProtection"), ] .iter() .filter(|(checked, _)| *checked) .map(|(_, name)| json!(name)) .collect::>(); let mut extensions = json!({ "key_usage": {"critical": true, "usages": usages}, "extended_key_usage": {"critical": false, "usages": eku}, }); let san = parse_san(&san_text.get_clone()); if !san.is_empty() { extensions["subject_alt_name"] = json!({"critical": false, "entries": san}); } let mut payload = json!({ "name": request_name, "comment": comment.get_clone(), "subject": subject, "extensions": extensions, }); apply_signature_hash( &mut payload, signing_key_type().as_deref(), &signature_hash.get_clone(), ); if new_key.get() { let mut generated_key = json!({ "key_type": key_type.get_clone(), "name": format!("{} key", name.get_clone()), }); add_key_parameters( &mut generated_key, &key_type.get_clone(), &key_size.get_clone(), &curve.get_clone(), ); payload["new_key"] = generated_key; } else if !key_id.get_clone().is_empty() { payload["key_id"] = json!(key_id.get_clone()); } else { error.set(Some("select a key or generate a new one".to_string())); return; } let mut attributes = serde_json::Map::new(); if !challenge.get_clone().is_empty() { attributes.insert("1.2.840.113549.1.9.7".to_string(), json!(challenge.get_clone())); } if !unstructured.get_clone().is_empty() { attributes.insert("1.2.840.113549.1.9.2".to_string(), json!(unstructured.get_clone())); } payload["attributes"] = serde_json::Value::Object(attributes); saving.set(true); let id = id.clone(); spawn_local(async move { match api::create_request(&id, &payload).await { Ok(request) => { saving.set(false); sycamore_router::navigate(&format!("/vaults/{}/requests/{}", id, request.summary.id)); return; } Err(message) if message == api::LOGIN_REQUIRED => {} Err(message) => error.set(Some(message)), } if saving.is_alive() { saving.set(false); } }); }; let key_options = move || { keys.get_clone() .into_iter() .map(|key| view! { option(value=key.id.clone()) { (key.name.clone()) " (" (key.key_type.clone()) ")" } }) .collect::>() }; let nav = move || match me.get_clone() { Some(user) => whoami(&user), None => view! {}, }; view! { (app_nav("/", Some(id), nav)) main(class="container page-shell py-4") { (toast_error(error)) (page_header("New certificate signing request", "The CSR contains the public key and requested extensions; it is signed by the private key.", view! { a(class="btn btn-outline-light", href=format!("/vaults/{id}/requests")) { "Cancel" } })) div(class="card") { div(class="card-body") { div(class="form-section") { div(class="form-section-title") { "General" } div(class="row g-3") { div(class="col-md-6") { label(class="form-label") { "Internal name" } input(class="form-control", bind:value=name) } div(class="col-md-6") { (signature_hash_input( "Signature hash", options, signing_key_type, signature_hash, )) } div(class="col-12") { label(class="form-label") { "Comment" } textarea(class="form-control", rows="2", bind:value=comment) {} } } } div(class="form-section") { div(class="form-section-title") { "Subject" } div(class="row g-3") { (subject_input("Common name (CN)", cn)) (subject_input("Organization (O)", organization)) (subject_input("Organizational unit (OU)", org_unit)) (subject_input("Country (C)", country)) (subject_input("State / province (ST)", state)) (subject_input("Locality (L)", locality)) (subject_input("E-mail address", email)) } } div(class="form-section") { div(class="form-section-title") { "Private key" } div(class="form-check mb-2") { input(class="form-check-input", r#type="checkbox", id="new-key", bind:checked=new_key) label(class="form-check-label", r#for="new-key") { "Generate a new key" } } (if new_key.get() { view! { div(class="row g-3") { div(class="col-md-4") { label(class="form-label") { "Key type" } (key_type_select(options, key_type)) } div(class="col-md-4") { (key_parameter_input(options, key_type, key_size, curve)) } } } } else { view! { label(class="form-label") { "Existing key" } select(class="form-select", bind:value=key_id) { option(value="") { "— select a key —" } (key_options()) } } }) } div(class="form-section") { div(class="form-section-title") { "Requested extensions" } div(class="row g-3") { div(class="col-md-6") { label(class="form-label") { "Subject alternative names" } input(class="form-control", placeholder="DNS:example.com, IP:192.0.2.1", bind:value=san_text) } div(class="col-md-6") { label(class="form-label") { "Challenge password" } input(class="form-control", bind:value=challenge) } div(class="col-md-6") { label(class="form-label") { "Unstructured name" } input(class="form-control", bind:value=unstructured) } } div(class="row g-3 mt-1") { div(class="col-md-6") { div(class="form-section-title") { "Key usage" } (check("Digital signature", ku_digital)) (check("Key encipherment", ku_encipher)) (check("Certificate sign", ku_cert_sign)) (check("CRL sign", ku_crl_sign)) } div(class="col-md-6") { div(class="form-section-title") { "Extended key usage" } (check("TLS server", eku_server)) (check("TLS client", eku_client)) (check("Code signing", eku_code)) (check("E-mail protection", eku_email)) } } } button(class="btn btn-primary", disabled=saving.get(), on:click=submit) { (if saving.get() { "Creating…" } else { "Create request" }) } } } } } } fn subject_input(label: &'static str, value: Signal) -> View { view! { div(class="col-md-6") { label(class="form-label") { (label) } input(class="form-control", bind:value=value) } } } fn check(label: &'static str, value: Signal) -> View { view! { div(class="form-check") { input(class="form-check-input", r#type="checkbox", bind:checked=value) label(class="form-check-label") { (label) } } } } fn parse_san(text: &str) -> Vec { text.split(',') .map(|chunk| chunk.trim()) .filter(|chunk| !chunk.is_empty()) .map(|chunk| { if let Some((prefix, value)) = chunk.split_once(':') { let kind = match prefix.trim().to_lowercase().as_str() { "ip" => "ip", "uri" => "uri", "email" => "email", "rid" => "rid", "dns" => "dns", _ => "dns", }; json!({"type": kind, "value": value.trim()}) } else { json!({"type": "dns", "value": chunk}) } }) .collect() }