Add full XCA curve and digest parity
CI / Build and push docker image (push) Successful in 37s
CI / Build and push OCSP image (push) Successful in 38s

This commit is contained in:
2026-10-26 12:06:17 +08:00
parent e83dc42d3d
commit 1a7b4a10e8
36 changed files with 4326 additions and 331 deletions
+552
View File
@@ -0,0 +1,552 @@
//! Shared cryptographic option metadata, selection rules, and form controls.
use serde_json::{json, Value};
use sycamore::prelude::*;
use crate::model::{CurveOption, HashOption, KeyTypeOption, OptionsPage};
const DEFAULT_HASH: &str = "sha256";
const DEFAULT_CURVE: &str = "secp256r1";
const DEFAULT_SIZE: &str = "2048";
pub fn is_ed25519(key_type: Option<&str>) -> bool {
key_type
.map(|kind| kind.eq_ignore_ascii_case("ed25519"))
.unwrap_or(false)
}
fn key_type_rule<'a>(options: &'a OptionsPage, key_type: &str) -> Option<&'a KeyTypeOption> {
options
.key_types
.iter()
.find(|rule| rule.key_type.eq_ignore_ascii_case(key_type))
}
/// The rich options are authoritative; the legacy arrays keep the controls
/// usable while an older vault options response is being upgraded.
pub fn available_hashes(options: &OptionsPage) -> Vec<HashOption> {
if !options.hash_options.is_empty() {
return options.hash_options.clone();
}
options
.hash_algorithms
.iter()
.map(|name| HashOption {
name: name.clone(),
label: name.clone(),
insecure: false,
})
.collect()
}
/// Return hashes accepted by the actual signing key. `None` means that no key
/// has been selected yet, so the unrestricted list is shown temporarily.
pub fn allowed_hashes(options: &OptionsPage, key_type: Option<&str>) -> Vec<HashOption> {
let available = available_hashes(options);
if is_ed25519(key_type) {
return Vec::new();
}
let Some(key_type) = key_type else {
return available;
};
let Some(rule) = key_type_rule(options, key_type) else {
return Vec::new();
};
let Some(allowed) = rule.hashes.as_ref() else {
return available;
};
available
.into_iter()
.filter(|hash| allowed.iter().any(|name| name == &hash.name))
.collect()
}
/// Preserve a valid explicit choice, prefer SHA-256 for stale choices, and
/// finally use the first algorithm accepted by the key.
pub fn valid_hash(selected: &str, allowed: &[HashOption]) -> String {
let names = allowed
.iter()
.map(|option| option.name.clone())
.collect::<Vec<_>>();
valid_choice(selected, &names, DEFAULT_HASH)
}
fn valid_choice(selected: &str, choices: &[String], preferred: &str) -> String {
if choices.iter().any(|choice| choice == selected) {
return selected.to_string();
}
if choices.iter().any(|choice| choice == preferred) {
return preferred.to_string();
}
choices.first().cloned().unwrap_or_default()
}
pub fn available_curves(options: &OptionsPage, key_type: &str) -> Vec<CurveOption> {
let mut available = if !options.curve_options.is_empty() {
options.curve_options.clone()
} else {
options
.curves
.iter()
.map(|name| CurveOption {
name: name.clone(),
label: name.clone(),
..CurveOption::default()
})
.collect::<Vec<_>>()
};
if let Some(allowed) = key_type_rule(options, key_type).and_then(|rule| rule.curves.as_ref()) {
available.retain(|curve| allowed.iter().any(|name| name == &curve.name));
}
available
}
pub fn key_sizes(options: &OptionsPage, key_type: &str) -> Vec<String> {
key_type_rule(options, key_type)
.and_then(|rule| rule.sizes.as_ref())
.cloned()
.unwrap_or_default()
.into_iter()
.map(|size| size.to_string())
.collect()
}
/// Add only parameters understood by the selected key algorithm.
pub fn add_key_parameters(payload: &mut Value, key_type: &str, size: &str, curve: &str) {
match key_type {
"ec" => {
payload["curve"] = json!(curve);
if let Some(object) = payload.as_object_mut() {
object.remove("size");
}
}
"rsa" | "dsa" => {
payload["size"] = json!(size.parse::<i64>().unwrap_or(2048));
if let Some(object) = payload.as_object_mut() {
object.remove("curve");
}
}
_ => {
if let Some(object) = payload.as_object_mut() {
object.remove("size");
object.remove("curve");
}
}
}
}
/// Ed25519 has its own signature algorithm and must not carry a digest field.
pub fn apply_signature_hash(payload: &mut Value, key_type: Option<&str>, hash: &str) {
if is_ed25519(key_type) {
if let Some(object) = payload.as_object_mut() {
object.remove("signature_hash");
}
} else {
payload["signature_hash"] = json!(hash);
}
}
fn option_label(name: &str, label: &str, insecure: bool) -> String {
let label = if label.trim().is_empty() { name } else { label };
if insecure {
format!("{label} (insecure)")
} else {
label.to_string()
}
}
fn key_type_option_views(options: &[KeyTypeOption]) -> Vec<View> {
options
.iter()
.map(|option| {
let value = option.key_type.clone();
let label = option_label(&option.key_type, &option.label, false);
view! { option(value=value) { (label) } }
})
.collect()
}
fn hash_option_views(options: &[HashOption]) -> Vec<View> {
options
.iter()
.map(|option| {
let value = option.name.clone();
let label = option_label(&option.name, &option.label, option.insecure);
view! { option(value=value) { (label) } }
})
.collect()
}
fn curve_option_views(options: &[CurveOption]) -> Vec<View> {
let mut groups: Vec<(String, Vec<CurveOption>)> = Vec::new();
for option in options.iter().cloned() {
let group = if option.group.trim().is_empty() {
"Curves".to_string()
} else {
option.group.clone()
};
if let Some((_, items)) = groups.iter_mut().find(|(name, _)| name == &group) {
items.push(option);
} else {
groups.push((group, vec![option]));
}
}
groups
.into_iter()
.map(|(group, items)| {
let items = items
.into_iter()
.map(|option| {
let value = option.name.clone();
let label = option_label(&option.name, &option.label, option.insecure);
let comment = option.comment.clone();
view! { option(value=value, title=comment) { (label) } }
})
.collect::<Vec<View>>();
view! { optgroup(label=group) { (items) } }
})
.collect()
}
fn insecure_warning(label: String, insecure: bool, comment: &str) -> View {
if !insecure {
return view! {};
}
let comment = comment.trim();
let message = if comment.is_empty() {
format!("Warning: {label} is marked as insecure; use it only for compatibility.")
} else {
format!("Warning: {label} is marked as insecure; use it only for compatibility. {comment}")
};
view! {
div(class="alert alert-warning py-2 mb-0 mt-2", role="alert") {
(message)
}
}
}
fn hash_warning(options: &[HashOption], selected: &str) -> View {
let Some(option) = options.iter().find(|option| option.name == selected) else {
return view! {};
};
insecure_warning(
option_label(&option.name, &option.label, false),
option.insecure,
"",
)
}
fn curve_warning(options: &[CurveOption], selected: &str) -> View {
let Some(option) = options.iter().find(|option| option.name == selected) else {
return view! {};
};
insecure_warning(
option_label(&option.name, &option.label, false),
option.insecure,
&option.comment,
)
}
/// Key type selector backed by the vault options instead of a local subset.
pub fn key_type_select(options: Signal<Option<OptionsPage>>, selected: Signal<String>) -> View {
create_effect(move || {
let Some(options) = options.get_clone() else {
return;
};
let names = options
.key_types
.iter()
.map(|option| option.key_type.clone())
.collect::<Vec<_>>();
if names.is_empty() {
return;
}
let current = selected.get_clone();
let next = valid_choice(&current, &names, "rsa");
if next != current {
selected.set(next);
}
});
view! {
select(class="form-select", bind:value=selected) {
(move || options
.get_clone()
.map(|options| key_type_option_views(&options.key_types))
.unwrap_or_default())
}
}
}
/// Type-aware size/curve control shared by key, CSR, certificate and settings
/// forms. Invalid persisted values are normalized as soon as options arrive.
pub fn key_parameter_input(
options: Signal<Option<OptionsPage>>,
key_type: Signal<String>,
size: Signal<String>,
curve: Signal<String>,
) -> View {
create_effect(move || {
let Some(options) = options.get_clone() else {
return;
};
let kind = key_type.get_clone();
if kind == "ec" {
let curves = available_curves(&options, &kind)
.into_iter()
.map(|option| option.name)
.collect::<Vec<_>>();
if !curves.is_empty() {
let current = curve.get_clone();
let next = valid_choice(&current, &curves, DEFAULT_CURVE);
if next != current {
curve.set(next);
}
}
} else if !is_ed25519(Some(&kind)) {
let sizes = key_sizes(&options, &kind);
if !sizes.is_empty() {
let current = size.get_clone();
let next = valid_choice(&current, &sizes, DEFAULT_SIZE);
if next != current {
size.set(next);
}
}
}
});
view! {
(move || {
let kind = key_type.get_clone();
let Some(options) = options.get_clone() else {
return view! {
label(class="form-label") { "Key parameters" }
select(class="form-select", disabled=true) {
option(value="") { "Loading options…" }
}
};
};
if kind == "ec" {
let curves = available_curves(&options, &kind);
let warning = curve_warning(&curves, &curve.get_clone());
let mut items = curve_option_views(&curves);
if items.is_empty() {
items.push(view! { option(value="") { "No curves available" } });
}
view! {
label(class="form-label") { "Curve" }
select(class="form-select", disabled=curves.is_empty(), bind:value=curve) {
(items)
}
(warning)
}
} else if is_ed25519(Some(&kind)) {
view! {
div(class="alert alert-info py-2 mb-0") {
"Ed25519 keys have a fixed 256-bit size."
}
}
} else {
let sizes = key_sizes(&options, &kind);
let no_sizes = sizes.is_empty();
let items = if no_sizes {
vec![view! { option(value="") { "No key sizes available" } }]
} else {
sizes
.into_iter()
.map(|value| {
let option_value = value.clone();
view! { option(value=option_value) { (value) } }
})
.collect()
};
view! {
label(class="form-label") { "Key size (bits)" }
select(class="form-select", disabled=no_sizes, bind:value=size) {
(items)
}
}
}
})
}
}
/// Signature hash selector filtered by the key that will actually sign.
pub fn signature_hash_input(
label: &'static str,
options: Signal<Option<OptionsPage>>,
signing_key_type: impl Fn() -> Option<String> + Copy + 'static,
selected: Signal<String>,
) -> View {
create_effect(move || {
let Some(options) = options.get_clone() else {
return;
};
let kind = signing_key_type();
let next = if is_ed25519(kind.as_deref()) {
String::new()
} else {
valid_hash(
&selected.get_clone(),
&allowed_hashes(&options, kind.as_deref()),
)
};
if next != selected.get_clone() {
selected.set(next);
}
});
view! {
(move || {
let kind = signing_key_type();
let Some(options) = options.get_clone() else {
return view! {
label(class="form-label") { (label) }
select(class="form-select", disabled=true) {
option(value="") { "Loading hash algorithms…" }
}
};
};
if is_ed25519(kind.as_deref()) {
view! {
label(class="form-label") { (label) }
select(class="form-select", disabled=true, bind:value=selected) {
option(value="") { "Not used for Ed25519" }
}
div(class="form-text") { "Ed25519 signs without a configurable hash." }
}
} else {
let hashes = allowed_hashes(&options, kind.as_deref());
let warning = hash_warning(&hashes, &selected.get_clone());
let mut items = hash_option_views(&hashes);
if items.is_empty() {
items.push(view! { option(value="") { "No compatible hashes" } });
}
view! {
label(class="form-label") { (label) }
select(class="form-select", disabled=hashes.is_empty(), bind:value=selected) {
(items)
}
(warning)
}
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn options() -> OptionsPage {
serde_json::from_value(json!({
"key_types": [
{"type": "rsa", "label": "RSA", "sizes": [1024, 2048, 4096], "hashes": ["sha256", "sha1"]},
{"type": "dsa", "label": "DSA", "sizes": [1024, 3072], "hashes": ["sha256", "sha384"]},
{"type": "ec", "label": "EC", "curves": ["secp256r1", "weakCurve"], "hashes": ["sha384", "sha512"]},
{"type": "ed25519", "label": "ED25519", "hashes": []}
],
"hash_options": [
{"name": "sha256", "label": "SHA-256", "insecure": false},
{"name": "sha384", "label": "SHA-384", "insecure": false},
{"name": "sha512", "label": "SHA-512", "insecure": false},
{"name": "sha1", "label": "SHA-1", "insecure": true}
],
"curve_options": [
{"name": "secp256r1", "label": "NIST P-256", "group": "NIST", "insecure": false},
{"name": "weakCurve", "label": "Legacy curve", "group": "Legacy", "insecure": true},
{"name": "otherCurve", "label": "Other curve", "group": "Other", "insecure": false}
]
}))
.unwrap()
}
#[test]
fn hashes_follow_signing_key_rules() {
let options = options();
let rsa = allowed_hashes(&options, Some("rsa"));
assert_eq!(
rsa.iter()
.map(|hash| hash.name.as_str())
.collect::<Vec<_>>(),
["sha256", "sha1"]
);
assert_eq!(allowed_hashes(&options, Some("RSA")), rsa);
let ec = allowed_hashes(&options, Some("ec"));
assert_eq!(
ec.iter().map(|hash| hash.name.as_str()).collect::<Vec<_>>(),
["sha384", "sha512"]
);
assert!(allowed_hashes(&options, Some("ed25519")).is_empty());
assert_eq!(allowed_hashes(&options, None).len(), 4);
}
#[test]
fn legacy_options_remain_usable_without_rich_metadata() {
let options: OptionsPage = serde_json::from_value(json!({
"key_types": [{"type": "rsa", "label": "RSA", "sizes": [2048]}],
"hash_algorithms": ["sha256", "sha512"],
"curves": ["secp256r1"]
}))
.unwrap();
assert_eq!(allowed_hashes(&options, Some("rsa")).len(), 2);
assert_eq!(available_curves(&options, "ec").len(), 1);
}
#[test]
fn stale_hash_falls_back_without_overriding_valid_choice() {
let allowed = allowed_hashes(&options(), Some("ec"));
assert_eq!(valid_hash("sha512", &allowed), "sha512");
assert_eq!(valid_hash("sha1", &allowed), "sha384");
let allowed = allowed_hashes(&options(), Some("rsa"));
assert_eq!(valid_hash("sha512", &allowed), "sha256");
assert_eq!(valid_hash("sha1", &allowed), "sha1");
assert_eq!(valid_hash("sha256", &[]), "");
}
#[test]
fn curves_and_sizes_use_key_type_metadata() {
let options = options();
let curves = available_curves(&options, "ec");
assert_eq!(curves.len(), 2);
assert_eq!(
valid_choice(
"removed",
&curves
.iter()
.map(|curve| curve.name.clone())
.collect::<Vec<_>>(),
DEFAULT_CURVE
),
"secp256r1"
);
assert_eq!(key_sizes(&options, "dsa"), ["1024", "3072"]);
assert_eq!(
valid_choice("2048", &key_sizes(&options, "dsa"), DEFAULT_SIZE),
"1024"
);
}
#[test]
fn payload_rules_cover_curves_dsa_and_ed25519() {
let mut ec = json!({"key_type": "ec", "size": 2048});
add_key_parameters(&mut ec, "ec", "2048", "secp256r1");
assert_eq!(ec["curve"], "secp256r1");
assert!(ec.get("size").is_none());
let mut dsa = json!({"key_type": "dsa"});
add_key_parameters(&mut dsa, "dsa", "3072", "secp256r1");
assert_eq!(dsa["size"], 3072);
assert!(dsa.get("curve").is_none());
let mut ed = json!({"key_type": "ed25519", "size": 2048, "signature_hash": "sha256"});
add_key_parameters(&mut ed, "ed25519", "2048", "secp256r1");
apply_signature_hash(&mut ed, Some("ed25519"), "sha256");
assert!(ed.get("size").is_none());
assert!(ed.get("signature_hash").is_none());
}
#[test]
fn insecure_labels_are_explicit() {
assert_eq!(option_label("sha1", "SHA-1", true), "SHA-1 (insecure)");
assert_eq!(option_label("sha256", "SHA-256", false), "SHA-256");
}
}
+1
View File
@@ -1,3 +1,4 @@
pub mod crypto;
pub mod extension_form;
pub mod list_editor;
pub mod nav;
+43 -1
View File
@@ -581,12 +581,16 @@ pub struct SummaryPage {
#[derive(Debug, Clone, PartialEq, Deserialize, Default)]
pub struct OptionsPage {
#[serde(default)]
pub key_types: Vec<Value>,
pub key_types: Vec<KeyTypeOption>,
#[serde(default)]
pub hash_algorithms: Vec<String>,
#[serde(default)]
pub curves: Vec<String>,
#[serde(default)]
pub curve_options: Vec<CurveOption>,
#[serde(default)]
pub hash_options: Vec<HashOption>,
#[serde(default)]
pub dn_attributes: Vec<DnAttribute>,
#[serde(default)]
pub eku: Vec<DnAttribute>,
@@ -602,6 +606,44 @@ pub struct OptionsPage {
pub templates: Vec<TemplateSummary>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Default)]
pub struct KeyTypeOption {
#[serde(rename = "type", default)]
pub key_type: String,
#[serde(default)]
pub label: String,
#[serde(default)]
pub sizes: Option<Vec<i64>>,
#[serde(default)]
pub curves: Option<Vec<String>>,
#[serde(default)]
pub hashes: Option<Vec<String>>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Default)]
pub struct CurveOption {
#[serde(default)]
pub name: String,
#[serde(default)]
pub label: String,
#[serde(default)]
pub group: String,
#[serde(default)]
pub comment: String,
#[serde(default)]
pub insecure: bool,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Default)]
pub struct HashOption {
#[serde(default)]
pub name: String,
#[serde(default)]
pub label: String,
#[serde(default)]
pub insecure: bool,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Default)]
pub struct DnAttribute {
#[serde(default)]
+21 -9
View File
@@ -4,15 +4,19 @@ use sycamore::prelude::*;
use wasm_bindgen_futures::spawn_local;
use crate::api;
use crate::components::crypto::{apply_signature_hash, signature_hash_input};
use crate::components::nav::{app_nav, spinner, whoami};
use crate::components::ui::{copy_button, empty_state, page_header, toast_error};
use crate::dom;
use crate::model::{fmt_date, CertificateDetail, Me, Revocation, TemplateSummary};
use crate::model::{
fmt_date, CertificateDetail, Me, OptionsPage, Revocation, TemplateSummary,
};
pub fn CertificateDetailPage(vault_id: String, cert_id: String) -> View {
let id: &'static str = Box::leak(vault_id.clone().into_boxed_str());
let cert_id_value: &'static str = Box::leak(cert_id.clone().into_boxed_str());
let me = create_signal(None::<Me>);
let options = create_signal(None::<OptionsPage>);
let cert = create_signal(None::<CertificateDetail>);
let error = create_signal(None::<String>);
let loading = create_signal(true);
@@ -85,6 +89,9 @@ pub fn CertificateDetailPage(vault_id: String, cert_id: String) -> View {
if let Ok(user) = api::me().await {
me.set(Some(user));
}
if let Ok(page) = api::options(id).await {
options.set(Some(page));
}
loading.set(false);
});
load();
@@ -188,13 +195,18 @@ pub fn CertificateDetailPage(vault_id: String, cert_id: String) -> View {
});
};
let ca_key_type = move || {
cert.get_clone()
.and_then(|certificate| certificate.key_type.clone())
};
let generate_crl = move |_| {
let payload = json!({
let mut payload = json!({
"days": crl_days.get_clone().parse::<i64>().unwrap_or(30),
"signature_hash": crl_hash.get_clone(),
"with_reason": true,
"include_aki": true,
});
apply_signature_hash(&mut payload, ca_key_type().as_deref(), &crl_hash.get_clone());
spawn_local(async move {
match api::generate_crl(id, cert_id_value, &payload).await {
Ok(crl) => {
@@ -518,12 +530,12 @@ pub fn CertificateDetailPage(vault_id: String, cert_id: String) -> View {
input(class="form-control", bind:value=crl_days)
}
div(class="col-md-3") {
label(class="form-label") { "Hash" }
select(class="form-select", bind:value=crl_hash) {
option(value="sha256") { "SHA-256" }
option(value="sha384") { "SHA-384" }
option(value="sha512") { "SHA-512" }
}
(signature_hash_input(
"Hash",
options,
ca_key_type,
crl_hash,
))
}
div(class="col-md-3") {
button(class="btn btn-primary", on:click=generate_crl) { "Generate CRL" }
+70 -23
View File
@@ -4,6 +4,10 @@ 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::extension_form::{
build_extra_extensions, extension_fields, load_extra_extensions, ExtensionSignals,
};
@@ -14,7 +18,7 @@ use crate::components::list_editor::{
use crate::components::nav::{app_nav, whoami};
use crate::components::ui::{page_header, toast_error};
use crate::model::{
CertificateDetail, CertificateSummary, DnAttribute, KeySummary, Me, TemplateSummary,
CertificateDetail, CertificateSummary, DnAttribute, KeySummary, Me, OptionsPage, TemplateSummary,
};
const DEFAULT_EXPLICIT_DN: [&str; 7] = ["C", "ST", "L", "O", "OU", "CN", "emailAddress"];
@@ -73,6 +77,7 @@ pub fn CertificateNewPage(vault_id: String, from_cert: Option<String>) -> View {
let reactive_root = use_global_scope();
let id: &'static str = Box::leak(vault_id.clone().into_boxed_str());
let me = create_signal(None::<Me>);
let options = create_signal(None::<OptionsPage>);
let keys = create_signal(Vec::<KeySummary>::new());
let signers = create_signal(Vec::<CertificateSummary>::new());
let templates = create_signal(Vec::<TemplateSummary>::new());
@@ -82,6 +87,7 @@ pub fn CertificateNewPage(vault_id: String, from_cert: Option<String>) -> View {
let request_id = query_param("request").unwrap_or_default();
let request_mode = !request_id.is_empty();
let request_id_for_load = request_id.clone();
let request_key_id = create_signal(String::new());
let name = create_signal(String::new());
let comment = create_signal(String::new());
@@ -90,6 +96,7 @@ pub fn CertificateNewPage(vault_id: String, from_cert: Option<String>) -> View {
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 template_id = create_signal(String::new());
let template_apply_mode = create_signal("all".to_string());
let ca_default_template = create_signal(None::<String>);
@@ -256,6 +263,9 @@ pub fn CertificateNewPage(vault_id: String, from_cert: Option<String>) -> View {
if let Some(size) = key.get("size").and_then(|v| v.as_i64()) {
key_size.set(size.to_string());
}
if let Some(value) = key.get("curve").and_then(|v| v.as_str()) {
curve.set(value.to_string());
}
}
if let Some(items) = settings.get("explicit_dn").and_then(|v| v.as_array()) {
let names = items
@@ -277,8 +287,9 @@ pub fn CertificateNewPage(vault_id: String, from_cert: Option<String>) -> View {
}
}
}
if let Ok(options) = api::options(&id).await {
dn_attributes.set(options.dn_attributes);
if let Ok(page) = api::options(id).await {
dn_attributes.set(page.dn_attributes.clone());
options.set(Some(page));
}
if !request_mode {
if let Some(cert_id) = from_cert.clone() {
@@ -311,6 +322,7 @@ pub fn CertificateNewPage(vault_id: String, from_cert: Option<String>) -> View {
if request_mode {
if let Ok(value) = api::request(&id, &request_id_for_load).await {
name.set(format!("{} certificate", value.summary.name));
request_key_id.set(value.summary.key_id.clone().unwrap_or_default());
let subject_json =
serde_json::to_value(&value.subject_entries).unwrap_or(json!([]));
reactive_root.run_in(|| apply_subject_json(&form, &subject_json));
@@ -318,6 +330,34 @@ pub fn CertificateNewPage(vault_id: String, from_cert: Option<String>) -> View {
}
});
let signing_key_type = move || {
let keys = keys.get_clone();
let signer = signer_id.get_clone();
if signer != "self" {
let issuer_key_id = signers
.get_clone()
.into_iter()
.find(|cert| cert.id == signer)
.and_then(|cert| cert.key_id);
return issuer_key_id.and_then(|key_id| {
keys.into_iter()
.find(|key| key.id == key_id)
.map(|key| key.key_type)
});
}
if !request_mode && new_key.get() {
return Some(key_type.get_clone());
}
let subject_key_id = if request_mode {
request_key_id.get_clone()
} else {
key_id.get_clone()
};
keys.into_iter()
.find(|key| key.id == subject_key_id)
.map(|key| key.key_type)
};
let apply_template_id = move |template: String, mode: String| {
if template.is_empty() {
return;
@@ -430,10 +470,14 @@ pub fn CertificateNewPage(vault_id: String, from_cert: Option<String>) -> View {
"comment": comment.get_clone(),
"subject": subject,
"extensions": extensions,
"signature_hash": signature_hash.get_clone(),
"signer_id": if signer == "self" { serde_json::Value::Null } else { json!(signer) },
"validity": validity_value(&form),
});
apply_signature_hash(
&mut payload,
signing_key_type().as_deref(),
&signature_hash.get_clone(),
);
if validity_mode.get_clone() == "absolute" && !no_expiration.get() {
payload["not_before"] = json!(not_before_text.get_clone());
payload["not_after"] = json!(not_after_text.get_clone());
@@ -443,11 +487,17 @@ pub fn CertificateNewPage(vault_id: String, from_cert: Option<String>) -> View {
payload["modify_subject"] = json!(true);
payload["copy_request_extensions"] = json!(false);
} else if new_key.get() {
payload["new_key"] = json!({
let mut generated_key = json!({
"key_type": key_type.get_clone(),
"size": key_size.get_clone().parse::<i64>().unwrap_or(2048),
"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 {
@@ -494,15 +544,18 @@ pub fn CertificateNewPage(vault_id: String, from_cert: Option<String>) -> View {
}
};
let signer = signer_id.get_clone();
let payload = json!({
let mut payload = json!({
"name": if name.get_clone().is_empty() { "preview".to_string() } else { name.get_clone() },
"subject": subject,
"extensions": extensions,
"signature_hash": signature_hash.get_clone(),
"signer_id": if signer == "self" { serde_json::Value::Null } else { json!(signer) },
"validity": validity_value(&form),
});
let mut payload = payload;
apply_signature_hash(
&mut payload,
signing_key_type().as_deref(),
&signature_hash.get_clone(),
);
if validity_mode.get_clone() == "absolute" && !no_expiration.get() {
payload["not_before"] = json!(not_before_text.get_clone());
payload["not_after"] = json!(not_after_text.get_clone());
@@ -602,13 +655,12 @@ pub fn CertificateNewPage(vault_id: String, from_cert: Option<String>) -> View {
}
}
div(class="col-md-6") {
label(class="form-label") { "Signature hash" }
select(class="form-select", bind:value=signature_hash) {
option(value="sha256") { "SHA-256" }
option(value="sha384") { "SHA-384" }
option(value="sha512") { "SHA-512" }
option(value="sha1") { "SHA-1" }
}
(signature_hash_input(
"Signature hash",
options,
signing_key_type,
signature_hash,
))
}
div(class="col-md-6") {
label(class="form-label") { "Template" }
@@ -661,15 +713,10 @@ pub fn CertificateNewPage(vault_id: String, from_cert: Option<String>) -> View {
div(class="row g-3") {
div(class="col-md-4") {
label(class="form-label") { "Key type" }
select(class="form-select", bind:value=key_type) {
option(value="rsa") { "RSA" }
option(value="ec") { "EC" }
option(value="ed25519") { "ED25519" }
}
(key_type_select(options, key_type))
}
div(class="col-md-4") {
label(class="form-label") { "Key size" }
input(class="form-control", bind:value=key_size)
(key_parameter_input(options, key_type, key_size, curve))
}
}
}
+18 -59
View File
@@ -4,25 +4,15 @@ use sycamore::prelude::*;
use wasm_bindgen_futures::spawn_local;
use crate::api;
use crate::components::crypto::{add_key_parameters, key_parameter_input, key_type_select};
use crate::components::nav::{app_nav, whoami};
use crate::components::ui::{page_header, toast_error};
use crate::model::Me;
const RSA_SIZES: &[&str] = &["1024", "2048", "3072", "4096", "8192"];
const DSA_SIZES: &[&str] = &["1024", "2048", "3072"];
const CURVES: &[&str] = &[
"secp256r1",
"secp384r1",
"secp521r1",
"secp256k1",
"brainpoolP256r1",
"brainpoolP384r1",
"brainpoolP512r1",
];
use crate::model::{Me, OptionsPage};
pub fn KeyNewPage(vault_id: String) -> View {
let id: &'static str = Box::leak(vault_id.clone().into_boxed_str());
let me = create_signal(None::<Me>);
let options = create_signal(None::<OptionsPage>);
let error = create_signal(None::<String>);
let name = create_signal(String::new());
let comment = create_signal(String::new());
@@ -37,6 +27,11 @@ pub fn KeyNewPage(vault_id: String) -> View {
me.set(Some(user));
}
}
if let Ok(page) = api::options(id).await {
if options.is_alive() {
options.set(Some(page));
}
}
});
let submit = move |_| {
@@ -51,11 +46,12 @@ pub fn KeyNewPage(vault_id: String) -> View {
"comment": comment.get_clone(),
"key_type": key_type_value,
});
if key_type_value == "ec" {
payload["curve"] = json!(curve.get_clone());
} else if key_type_value == "rsa" || key_type_value == "dsa" {
payload["size"] = json!(size.get_clone().parse::<i64>().unwrap_or(2048));
}
add_key_parameters(
&mut payload,
&key_type_value,
&size.get_clone(),
&curve.get_clone(),
);
saving.set(true);
let id = id.clone();
spawn_local(async move {
@@ -74,45 +70,6 @@ pub fn KeyNewPage(vault_id: String) -> View {
});
};
let type_options = move || {
["rsa", "dsa", "ec", "ed25519"]
.iter()
.map(|value| {
view! {
option(value=*value) {
(if *value == "ed25519" { "ED25519".to_string() } else { value.to_uppercase() })
}
}
})
.collect::<Vec<View>>()
};
let parameter_input = move || {
let kind = key_type.get_clone();
if kind == "ec" {
view! {
label(class="form-label") { "Curve" }
select(class="form-select", bind:value=curve) {
(CURVES.iter().map(|value| view! {
option(value=*value) { (*value) }
}).collect::<Vec<View>>())
}
}
} else if kind == "ed25519" {
view! { div(class="alert alert-info py-2 mb-0") { "Ed25519 keys have a fixed 256-bit size." } }
} else {
let sizes = if kind == "dsa" { DSA_SIZES } else { RSA_SIZES };
view! {
label(class="form-label") { "Key size (bits)" }
select(class="form-select", bind:value=size) {
(sizes.iter().map(|value| view! {
option(value=*value) { (*value) }
}).collect::<Vec<View>>())
}
}
}
};
let nav = move || match me.get_clone() {
Some(user) => whoami(&user),
None => view! {},
@@ -134,9 +91,11 @@ pub fn KeyNewPage(vault_id: String) -> View {
div(class="row g-3 mb-3") {
div(class="col-md-4") {
label(class="form-label") { "Key type" }
select(class="form-select", bind:value=key_type) { (type_options()) }
(key_type_select(options, key_type))
}
div(class="col-md-4") {
(key_parameter_input(options, key_type, size, curve))
}
div(class="col-md-4") { (move || parameter_input()) }
}
div(class="mb-3") {
label(class="form-label") { "Comment" }
+44 -18
View File
@@ -4,13 +4,18 @@ 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};
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::<Me>);
let options = create_signal(None::<OptionsPage>);
let keys = create_signal(Vec::<KeySummary>::new());
let error = create_signal(None::<String>);
let saving = create_signal(false);
@@ -21,6 +26,7 @@ pub fn RequestNewPage(vault_id: String) -> View {
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());
@@ -54,8 +60,24 @@ pub fn RequestNewPage(vault_id: String) -> View {
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() {
@@ -113,14 +135,24 @@ pub fn RequestNewPage(vault_id: String) -> View {
"comment": comment.get_clone(),
"subject": subject,
"extensions": extensions,
"signature_hash": signature_hash.get_clone(),
});
apply_signature_hash(
&mut payload,
signing_key_type().as_deref(),
&signature_hash.get_clone(),
);
if new_key.get() {
payload["new_key"] = json!({
let mut generated_key = json!({
"key_type": key_type.get_clone(),
"size": key_size.get_clone().parse::<i64>().unwrap_or(2048),
"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 {
@@ -180,13 +212,12 @@ pub fn RequestNewPage(vault_id: String) -> View {
input(class="form-control", bind:value=name)
}
div(class="col-md-6") {
label(class="form-label") { "Signature hash" }
select(class="form-select", bind:value=signature_hash) {
option(value="sha256") { "SHA-256" }
option(value="sha384") { "SHA-384" }
option(value="sha512") { "SHA-512" }
option(value="sha1") { "SHA-1" }
}
(signature_hash_input(
"Signature hash",
options,
signing_key_type,
signature_hash,
))
}
div(class="col-12") {
label(class="form-label") { "Comment" }
@@ -217,15 +248,10 @@ pub fn RequestNewPage(vault_id: String) -> View {
div(class="row g-3") {
div(class="col-md-4") {
label(class="form-label") { "Key type" }
select(class="form-select", bind:value=key_type) {
option(value="rsa") { "RSA" }
option(value="ec") { "EC" }
option(value="ed25519") { "ED25519" }
}
(key_type_select(options, key_type))
}
div(class="col-md-4") {
label(class="form-label") { "Key size" }
input(class="form-control", bind:value=key_size)
(key_parameter_input(options, key_type, key_size, curve))
}
}
}
+28 -17
View File
@@ -4,13 +4,15 @@ use sycamore::prelude::*;
use wasm_bindgen_futures::spawn_local;
use crate::api;
use crate::components::crypto::{key_parameter_input, key_type_select, signature_hash_input};
use crate::components::nav::{app_nav, spinner, whoami};
use crate::components::ui::{page_header, toast_error};
use crate::model::Me;
use crate::model::{Me, OptionsPage};
pub fn SettingsPage(vault_id: String) -> View {
let id: &'static str = Box::leak(vault_id.clone().into_boxed_str());
let me = create_signal(None::<Me>);
let options = create_signal(None::<OptionsPage>);
let error = create_signal(None::<String>);
let info = create_signal(None::<String>);
let loading = create_signal(true);
@@ -25,6 +27,7 @@ pub fn SettingsPage(vault_id: String) -> View {
let hide_unusable = create_signal(false);
let default_key_type = create_signal("rsa".to_string());
let default_key_size = create_signal("2048".to_string());
let default_key_curve = create_signal("secp256r1".to_string());
let default_validity = create_signal("365".to_string());
let default_validity_unit = create_signal("days".to_string());
@@ -32,6 +35,9 @@ pub fn SettingsPage(vault_id: String) -> View {
if let Ok(user) = api::me().await {
me.set(Some(user));
}
if let Ok(page) = api::options(id).await {
options.set(Some(page));
}
match api::vault_settings(&id).await {
Ok(value) => {
version.set(value.get("version").and_then(|v| v.as_i64()).unwrap_or(0));
@@ -57,6 +63,9 @@ pub fn SettingsPage(vault_id: String) -> View {
if let Some(v) = key.get("size").and_then(|v| v.as_i64()) {
default_key_size.set(v.to_string());
}
if let Some(v) = key.get("curve").and_then(|v| v.as_str()) {
default_key_curve.set(v.to_string());
}
}
if let Some(validity) = settings.get("default_validity") {
if let Some(v) = validity.get("amount").and_then(|v| v.as_i64()) {
@@ -87,7 +96,11 @@ pub fn SettingsPage(vault_id: String) -> View {
"default_key": {
"type": default_key_type.get_clone(),
"size": default_key_size.get_clone().parse::<i64>().unwrap_or(2048),
"curve": null,
"curve": if default_key_type.get_clone() == "ec" {
json!(default_key_curve.get_clone())
} else {
serde_json::Value::Null
},
},
"default_validity": {
"amount": default_validity.get_clone().parse::<i64>().unwrap_or(365),
@@ -130,14 +143,12 @@ pub fn SettingsPage(vault_id: String) -> View {
div(class="form-section-title") { "Cryptography" }
div(class="row g-3") {
div(class="col-md-4") {
label(class="form-label") { "Default signature hash" }
select(class="form-select", bind:value=default_hash) {
option(value="sha256") { "SHA-256" }
option(value="sha384") { "SHA-384" }
option(value="sha512") { "SHA-512" }
option(value="sha224") { "SHA-224" }
option(value="sha1") { "SHA-1" }
}
(signature_hash_input(
"Default signature hash",
options,
|| None,
default_hash,
))
}
div(class="col-md-4") {
label(class="form-label") { "Serial number length (bits)" }
@@ -175,15 +186,15 @@ pub fn SettingsPage(vault_id: String) -> View {
div(class="row g-3") {
div(class="col-md-4") {
label(class="form-label") { "Default key type" }
select(class="form-select", bind:value=default_key_type) {
option(value="rsa") { "RSA" }
option(value="ec") { "EC" }
option(value="ed25519") { "ED25519" }
}
(key_type_select(options, default_key_type))
}
div(class="col-md-4") {
label(class="form-label") { "Default key size" }
input(class="form-control", bind:value=default_key_size)
(key_parameter_input(
options,
default_key_type,
default_key_size,
default_key_curve,
))
}
div(class="col-md-4 d-flex align-items-end") {
button(class="btn btn-primary", disabled=saving.get(), on:click=save) {