Add full XCA curve and digest parity
This commit is contained in:
@@ -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(¤t, &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(¤t, &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(¤t, &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,3 +1,4 @@
|
||||
pub mod crypto;
|
||||
pub mod extension_form;
|
||||
pub mod list_editor;
|
||||
pub mod nav;
|
||||
|
||||
+43
-1
@@ -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)]
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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
@@ -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" }
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user