Combine AIA fields into XCA-style editor
This commit is contained in:
@@ -37,7 +37,7 @@ test("advanced extensions, validation and vault export", async ({ page }) => {
|
|||||||
await (await field(page, "Country (C)")).fill("DE");
|
await (await field(page, "Country (C)")).fill("DE");
|
||||||
await page.locator("#new-key").check();
|
await page.locator("#new-key").check();
|
||||||
await page.locator("textarea[placeholder='http://example.com/ca.crl']").fill("http://example.com/ca.crl");
|
await page.locator("textarea[placeholder='http://example.com/ca.crl']").fill("http://example.com/ca.crl");
|
||||||
await page.locator("textarea[placeholder='http://ocsp.example.com']").fill("http://ocsp.example.com");
|
await page.locator("#aia").fill("OCSP;URI:http://ocsp.example.com");
|
||||||
await page.locator("textarea[placeholder*='raw']").fill(
|
await page.locator("textarea[placeholder*='raw']").fill(
|
||||||
'{"raw":[{"oid":"1.2.3.4","critical":false,"der_hex":"0c03616263"}]}'
|
'{"raw":[{"oid":"1.2.3.4","critical":false,"der_hex":"0c03616263"}]}'
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -38,6 +38,11 @@ test("advanced extension list editors", async ({ page }) => {
|
|||||||
const vaultName = unique("List Editors");
|
const vaultName = unique("List Editors");
|
||||||
const certName = unique("constraints.example");
|
const certName = unique("constraints.example");
|
||||||
const crlUrls = ["https://ca.example.com/root.crl", "https://backup.example.com/root.crl"];
|
const crlUrls = ["https://ca.example.com/root.crl", "https://backup.example.com/root.crl"];
|
||||||
|
const aiaEntries = [
|
||||||
|
["OCSP;URI", "https://ocsp.example.com"],
|
||||||
|
["caIssuers;URI", "https://ca.example.com/issuer.crt"],
|
||||||
|
];
|
||||||
|
const aiaText = aiaEntries.map(([type, value]) => `${type}:${value}`).join(", ");
|
||||||
const cpsUrls = ["https://example.com/cps", "https://example.com/policy"];
|
const cpsUrls = ["https://example.com/cps", "https://example.com/policy"];
|
||||||
|
|
||||||
await login(page, user);
|
await login(page, user);
|
||||||
@@ -79,6 +84,24 @@ test("advanced extension list editors", async ({ page }) => {
|
|||||||
await expect(dialog.getByLabel("Critical", { exact: true })).toBeChecked();
|
await expect(dialog.getByLabel("Critical", { exact: true })).toBeChecked();
|
||||||
await dialog.getByRole("button", { name: "Cancel", exact: true }).click();
|
await dialog.getByRole("button", { name: "Cancel", exact: true }).click();
|
||||||
|
|
||||||
|
// AIA combines the access method and XCA general-name type in each row.
|
||||||
|
dialog = await openEditor(page, "#edit-aia", "Authority information access");
|
||||||
|
await expect(dialog.getByLabel("Critical", { exact: true })).not.toBeChecked();
|
||||||
|
for (const [type, value] of aiaEntries) {
|
||||||
|
await addRow(dialog, type, value);
|
||||||
|
}
|
||||||
|
await dialog.getByLabel("Critical", { exact: true }).check();
|
||||||
|
await dialog.getByRole("button", { name: "Apply", exact: true }).click();
|
||||||
|
await expect(page.locator("#aia")).toHaveValue(aiaText);
|
||||||
|
|
||||||
|
dialog = await openEditor(page, "#edit-aia", "Authority information access");
|
||||||
|
await expect(dialog.locator(".list-entry")).toHaveCount(2);
|
||||||
|
await expect(dialog.locator(".list-entry").nth(0).locator("select")).toHaveValue("OCSP;URI");
|
||||||
|
await expect(dialog.locator(".list-entry").nth(1).locator("select")).toHaveValue("caIssuers;URI");
|
||||||
|
await expect(dialog.locator(".list-entry").nth(1).locator(".list-value")).toHaveValue(aiaEntries[1][1]);
|
||||||
|
await expect(dialog.getByLabel("Critical", { exact: true })).toBeChecked();
|
||||||
|
await dialog.getByRole("button", { name: "Cancel", exact: true }).click();
|
||||||
|
|
||||||
// Name constraints use typed rows and share one extension-level critical flag.
|
// Name constraints use typed rows and share one extension-level critical flag.
|
||||||
dialog = await openEditor(page, "#edit-nc-permitted", "Name constraints · permitted");
|
dialog = await openEditor(page, "#edit-nc-permitted", "Name constraints · permitted");
|
||||||
await expect(dialog.getByLabel("Critical", { exact: true })).toBeChecked();
|
await expect(dialog.getByLabel("Critical", { exact: true })).toBeChecked();
|
||||||
@@ -117,6 +140,7 @@ test("advanced extension list editors", async ({ page }) => {
|
|||||||
await expect(page.locator("#nc-excluded")).toHaveValue("DNS:.bad.example");
|
await expect(page.locator("#nc-excluded")).toHaveValue("DNS:.bad.example");
|
||||||
await page.getByRole("button", { name: "Validate extensions" }).click();
|
await page.getByRole("button", { name: "Validate extensions" }).click();
|
||||||
await expect(page.getByText("Extension preview")).toBeVisible();
|
await expect(page.getByText("Extension preview")).toBeVisible();
|
||||||
|
await expect(page.locator("pre.pem")).toContainText("Authority Information Access");
|
||||||
await expect(page.locator("pre.pem")).toContainText("Name Constraints");
|
await expect(page.locator("pre.pem")).toContainText("Name Constraints");
|
||||||
await expect(page.locator("pre.pem")).toContainText("Certificate Policies");
|
await expect(page.locator("pre.pem")).toContainText("Certificate Policies");
|
||||||
|
|
||||||
@@ -129,6 +153,13 @@ test("advanced extension list editors", async ({ page }) => {
|
|||||||
}, certApiUrl);
|
}, certApiUrl);
|
||||||
const extensions = detail.extension_spec;
|
const extensions = detail.extension_spec;
|
||||||
expect(extensions.crl_distribution_points).toEqual({ critical: true, urls: crlUrls });
|
expect(extensions.crl_distribution_points).toEqual({ critical: true, urls: crlUrls });
|
||||||
|
expect(extensions.authority_information_access).toEqual({
|
||||||
|
critical: true,
|
||||||
|
descriptions: aiaEntries.map(([type, value]) => ({
|
||||||
|
method: type.startsWith("OCSP") ? "ocsp" : "ca_issuers",
|
||||||
|
location: { type: "uri", value },
|
||||||
|
})),
|
||||||
|
});
|
||||||
expect(extensions.name_constraints).toEqual({
|
expect(extensions.name_constraints).toEqual({
|
||||||
critical: true,
|
critical: true,
|
||||||
permitted: [
|
permitted: [
|
||||||
@@ -149,4 +180,9 @@ test("advanced extension list editors", async ({ page }) => {
|
|||||||
await addRow(dialog, "URI", "https://template.example.com/root.crl");
|
await addRow(dialog, "URI", "https://template.example.com/root.crl");
|
||||||
await dialog.getByRole("button", { name: "Apply", exact: true }).click();
|
await dialog.getByRole("button", { name: "Apply", exact: true }).click();
|
||||||
await expect(page.locator("#crl-urls")).toHaveValue("https://template.example.com/root.crl");
|
await expect(page.locator("#crl-urls")).toHaveValue("https://template.example.com/root.crl");
|
||||||
|
|
||||||
|
dialog = await openEditor(page, "#edit-aia", "Authority information access");
|
||||||
|
await addRow(dialog, "OCSP;URI", "https://template.example.com/ocsp");
|
||||||
|
await dialog.getByRole("button", { name: "Apply", exact: true }).click();
|
||||||
|
await expect(page.locator("#aia")).toHaveValue("OCSP;URI:https://template.example.com/ocsp");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,16 +9,17 @@ use serde_json::{json, Map, Value};
|
|||||||
use sycamore::prelude::*;
|
use sycamore::prelude::*;
|
||||||
|
|
||||||
use crate::components::list_editor::{
|
use crate::components::list_editor::{
|
||||||
general_name_entries_to_text, list_editor_dialog, open_list_editor, parse_other_name,
|
aia_entries_to_text, general_name_entries_to_text, list_editor_dialog, open_list_editor,
|
||||||
split_list, ListEditorConfig, ListEditorFormat, ListEditorSignals,
|
parse_aia_entries, parse_other_name, split_list, ListEditorConfig, ListEditorFormat,
|
||||||
|
ListEditorSignals,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub struct ExtensionSignals {
|
pub struct ExtensionSignals {
|
||||||
pub crl_urls: Signal<String>,
|
pub crl_urls: Signal<String>,
|
||||||
pub crl_critical: Signal<bool>,
|
pub crl_critical: Signal<bool>,
|
||||||
pub aia_ocsp: Signal<String>,
|
pub aia: Signal<String>,
|
||||||
pub aia_ca_issuers: Signal<String>,
|
pub aia_critical: Signal<bool>,
|
||||||
pub nc_permitted: Signal<String>,
|
pub nc_permitted: Signal<String>,
|
||||||
pub nc_excluded: Signal<String>,
|
pub nc_excluded: Signal<String>,
|
||||||
pub nc_critical: Signal<bool>,
|
pub nc_critical: Signal<bool>,
|
||||||
@@ -33,8 +34,7 @@ pub struct ExtensionSignals {
|
|||||||
|
|
||||||
pub fn extension_fields(s: &ExtensionSignals) -> View {
|
pub fn extension_fields(s: &ExtensionSignals) -> View {
|
||||||
let crl_urls = s.crl_urls;
|
let crl_urls = s.crl_urls;
|
||||||
let aia_ocsp = s.aia_ocsp;
|
let aia = s.aia;
|
||||||
let aia_ca_issuers = s.aia_ca_issuers;
|
|
||||||
let nc_permitted = s.nc_permitted;
|
let nc_permitted = s.nc_permitted;
|
||||||
let nc_excluded = s.nc_excluded;
|
let nc_excluded = s.nc_excluded;
|
||||||
let policy_oids = s.policy_oids;
|
let policy_oids = s.policy_oids;
|
||||||
@@ -45,6 +45,7 @@ pub fn extension_fields(s: &ExtensionSignals) -> View {
|
|||||||
let advanced_json = s.advanced_json;
|
let advanced_json = s.advanced_json;
|
||||||
|
|
||||||
let crl_editor = ListEditorSignals::new();
|
let crl_editor = ListEditorSignals::new();
|
||||||
|
let aia_editor = ListEditorSignals::new();
|
||||||
let nc_permitted_editor = ListEditorSignals::new();
|
let nc_permitted_editor = ListEditorSignals::new();
|
||||||
let nc_excluded_editor = ListEditorSignals::new();
|
let nc_excluded_editor = ListEditorSignals::new();
|
||||||
let policy_cps_editor = ListEditorSignals::new();
|
let policy_cps_editor = ListEditorSignals::new();
|
||||||
@@ -55,6 +56,13 @@ pub fn extension_fields(s: &ExtensionSignals) -> View {
|
|||||||
critical: Some(s.crl_critical),
|
critical: Some(s.crl_critical),
|
||||||
copy_common_name: false,
|
copy_common_name: false,
|
||||||
};
|
};
|
||||||
|
let aia_config = ListEditorConfig {
|
||||||
|
id: "aia",
|
||||||
|
title: "Authority information access",
|
||||||
|
format: ListEditorFormat::AuthorityInformationAccess,
|
||||||
|
critical: Some(s.aia_critical),
|
||||||
|
copy_common_name: false,
|
||||||
|
};
|
||||||
let nc_permitted_config = ListEditorConfig {
|
let nc_permitted_config = ListEditorConfig {
|
||||||
id: "nc-permitted",
|
id: "nc-permitted",
|
||||||
title: "Name constraints · permitted",
|
title: "Name constraints · permitted",
|
||||||
@@ -77,6 +85,7 @@ pub fn extension_fields(s: &ExtensionSignals) -> View {
|
|||||||
copy_common_name: false,
|
copy_common_name: false,
|
||||||
};
|
};
|
||||||
let edit_crl = move |_| open_list_editor(crl_editor, crl_urls, crl_config);
|
let edit_crl = move |_| open_list_editor(crl_editor, crl_urls, crl_config);
|
||||||
|
let edit_aia = move |_| open_list_editor(aia_editor, aia, aia_config);
|
||||||
let edit_nc_permitted =
|
let edit_nc_permitted =
|
||||||
move |_| open_list_editor(nc_permitted_editor, nc_permitted, nc_permitted_config);
|
move |_| open_list_editor(nc_permitted_editor, nc_permitted, nc_permitted_config);
|
||||||
let edit_nc_excluded =
|
let edit_nc_excluded =
|
||||||
@@ -95,13 +104,12 @@ pub fn extension_fields(s: &ExtensionSignals) -> View {
|
|||||||
button(id="edit-crl-urls", class="btn btn-outline-light", r#type="button", on:click=edit_crl) { "Edit" }
|
button(id="edit-crl-urls", class="btn btn-outline-light", r#type="button", on:click=edit_crl) { "Edit" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
div(class="col-md-3") {
|
div(class="col-md-6") {
|
||||||
label(class="form-label") { "AIA · OCSP" }
|
label(class="form-label") { "Authority information access" }
|
||||||
textarea(class="form-control", rows="2", placeholder="http://ocsp.example.com", bind:value=aia_ocsp) {}
|
div(class="input-group") {
|
||||||
}
|
textarea(id="aia", class="form-control", rows="2", placeholder="OCSP;URI:http://ocsp.example.com, caIssuers;URI:http://example.com/ca.crt", bind:value=aia) {}
|
||||||
div(class="col-md-3") {
|
button(id="edit-aia", class="btn btn-outline-light", r#type="button", on:click=edit_aia) { "Edit" }
|
||||||
label(class="form-label") { "AIA · caIssuers" }
|
}
|
||||||
textarea(class="form-control", rows="2", placeholder="http://example.com/ca.crt", bind:value=aia_ca_issuers) {}
|
|
||||||
}
|
}
|
||||||
div(class="col-md-6") {
|
div(class="col-md-6") {
|
||||||
label(class="form-label") { "Name constraints · permitted" }
|
label(class="form-label") { "Name constraints · permitted" }
|
||||||
@@ -152,6 +160,7 @@ pub fn extension_fields(s: &ExtensionSignals) -> View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
(list_editor_dialog(crl_editor, crl_urls, crl_config))
|
(list_editor_dialog(crl_editor, crl_urls, crl_config))
|
||||||
|
(list_editor_dialog(aia_editor, aia, aia_config))
|
||||||
(list_editor_dialog(
|
(list_editor_dialog(
|
||||||
nc_permitted_editor,
|
nc_permitted_editor,
|
||||||
nc_permitted,
|
nc_permitted,
|
||||||
@@ -181,17 +190,11 @@ pub fn build_extra_extensions(s: &ExtensionSignals) -> Result<Value, String> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut aia = Vec::new();
|
let aia = parse_aia_entries(&s.aia.get_clone())?;
|
||||||
for url in split_list(&s.aia_ocsp.get_clone()) {
|
|
||||||
aia.push(json!({"method": "ocsp", "location": {"type": "uri", "value": url}}));
|
|
||||||
}
|
|
||||||
for url in split_list(&s.aia_ca_issuers.get_clone()) {
|
|
||||||
aia.push(json!({"method": "ca_issuers", "location": {"type": "uri", "value": url}}));
|
|
||||||
}
|
|
||||||
if !aia.is_empty() {
|
if !aia.is_empty() {
|
||||||
extensions.insert(
|
extensions.insert(
|
||||||
"authority_information_access".to_string(),
|
"authority_information_access".to_string(),
|
||||||
json!({"critical": false, "descriptions": aia}),
|
json!({"critical": s.aia_critical.get(), "descriptions": aia}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,34 +271,18 @@ pub fn load_extra_extensions(s: &ExtensionSignals, spec: &Value) {
|
|||||||
.and_then(|v| v.as_bool())
|
.and_then(|v| v.as_bool())
|
||||||
.unwrap_or(false),
|
.unwrap_or(false),
|
||||||
);
|
);
|
||||||
let descriptions = spec
|
let aia = spec.get("authority_information_access");
|
||||||
.get("authority_information_access")
|
let descriptions = aia
|
||||||
.and_then(|v| v.get("descriptions"))
|
.and_then(|value| value.get("descriptions"))
|
||||||
.and_then(|v| v.as_array())
|
.and_then(|value| value.as_array())
|
||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let ocsp = descriptions
|
s.aia.set(aia_entries_to_text(&descriptions));
|
||||||
.iter()
|
s.aia_critical.set(
|
||||||
.filter(|item| item.get("method").and_then(|v| v.as_str()) == Some("ocsp"))
|
aia.and_then(|value| value.get("critical"))
|
||||||
.filter_map(|item| {
|
.and_then(|value| value.as_bool())
|
||||||
item.get("location")
|
.unwrap_or(false),
|
||||||
.and_then(|v| v.get("value"))
|
);
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("\n");
|
|
||||||
let ca_issuers = descriptions
|
|
||||||
.iter()
|
|
||||||
.filter(|item| item.get("method").and_then(|v| v.as_str()) == Some("ca_issuers"))
|
|
||||||
.filter_map(|item| {
|
|
||||||
item.get("location")
|
|
||||||
.and_then(|v| v.get("value"))
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("\n");
|
|
||||||
s.aia_ocsp.set(ocsp);
|
|
||||||
s.aia_ca_issuers.set(ca_issuers);
|
|
||||||
|
|
||||||
let nc = spec.get("name_constraints");
|
let nc = spec.get("name_constraints");
|
||||||
s.nc_critical.set(
|
s.nc_critical.set(
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ impl ListEditorSignals {
|
|||||||
pub enum ListEditorFormat {
|
pub enum ListEditorFormat {
|
||||||
GeneralNames,
|
GeneralNames,
|
||||||
UriList(&'static str),
|
UriList(&'static str),
|
||||||
|
AuthorityInformationAccess,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
@@ -88,6 +89,25 @@ pub fn open_list_editor(editor: ListEditorSignals, text: Signal<String>, config:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ListEditorFormat::AuthorityInformationAccess => {
|
||||||
|
for entry in parse_aia_entries(&text.get_clone()).unwrap_or_default() {
|
||||||
|
let method =
|
||||||
|
if entry.get("method").and_then(|value| value.as_str()) == Some("ca_issuers") {
|
||||||
|
"caIssuers"
|
||||||
|
} else {
|
||||||
|
"OCSP"
|
||||||
|
};
|
||||||
|
let Some(location) = entry.get("location") else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let location = general_name_entries_to_text(std::slice::from_ref(location));
|
||||||
|
let Some((kind, value)) = location.split_once(':') else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
rows.push(ListRow::new(next_id, &format!("{method};{kind}"), value));
|
||||||
|
next_id += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
editor.rows.set(rows);
|
editor.rows.set(rows);
|
||||||
editor.next_id.set(next_id);
|
editor.next_id.set(next_id);
|
||||||
@@ -120,6 +140,7 @@ pub fn list_editor_dialog(
|
|||||||
let kind = match config.format {
|
let kind = match config.format {
|
||||||
ListEditorFormat::GeneralNames => "DNS",
|
ListEditorFormat::GeneralNames => "DNS",
|
||||||
ListEditorFormat::UriList(_) => "URI",
|
ListEditorFormat::UriList(_) => "URI",
|
||||||
|
ListEditorFormat::AuthorityInformationAccess => "OCSP;URI",
|
||||||
};
|
};
|
||||||
rows.update(|rows| rows.push(ListRow::new(id, kind, "")));
|
rows.update(|rows| rows.push(ListRow::new(id, kind, "")));
|
||||||
};
|
};
|
||||||
@@ -139,11 +160,15 @@ pub fn list_editor_dialog(
|
|||||||
format!("{}:{}", row.kind.get_clone(), value)
|
format!("{}:{}", row.kind.get_clone(), value)
|
||||||
}
|
}
|
||||||
ListEditorFormat::UriList(_) => value.to_string(),
|
ListEditorFormat::UriList(_) => value.to_string(),
|
||||||
|
ListEditorFormat::AuthorityInformationAccess => {
|
||||||
|
format!("{}:{}", row.kind.get_clone(), value)
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let separator = match config.format {
|
let separator = match config.format {
|
||||||
ListEditorFormat::GeneralNames => ", ",
|
ListEditorFormat::GeneralNames => ", ",
|
||||||
ListEditorFormat::UriList(separator) => separator,
|
ListEditorFormat::UriList(separator) => separator,
|
||||||
|
ListEditorFormat::AuthorityInformationAccess => ", ",
|
||||||
};
|
};
|
||||||
text.set(entries.join(separator));
|
text.set(entries.join(separator));
|
||||||
if let Some(target) = config.critical {
|
if let Some(target) = config.critical {
|
||||||
@@ -252,6 +277,12 @@ pub fn list_editor_dialog(
|
|||||||
"For otherName, use <OID>;UTF8:<content> or <OID>;DER:<hex>."
|
"For otherName, use <OID>;UTF8:<content> or <OID>;DER:<hex>."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if matches!(config.format, ListEditorFormat::AuthorityInformationAccess) {
|
||||||
|
view! {
|
||||||
|
div(class="form-text mt-2") {
|
||||||
|
"Use OCSP or caIssuers with a URI, email, RID, DNS or IP location."
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
view! {}
|
view! {}
|
||||||
})
|
})
|
||||||
@@ -286,9 +317,82 @@ fn list_type_options(format: ListEditorFormat) -> View {
|
|||||||
ListEditorFormat::UriList(_) => view! {
|
ListEditorFormat::UriList(_) => view! {
|
||||||
option(value="URI") { "URI" }
|
option(value="URI") { "URI" }
|
||||||
},
|
},
|
||||||
|
ListEditorFormat::AuthorityInformationAccess => view! {
|
||||||
|
option(value="OCSP;URI") { "OCSP;URI" }
|
||||||
|
option(value="OCSP;email") { "OCSP;email" }
|
||||||
|
option(value="OCSP;RID") { "OCSP;RID" }
|
||||||
|
option(value="OCSP;DNS") { "OCSP;DNS" }
|
||||||
|
option(value="OCSP;IP") { "OCSP;IP" }
|
||||||
|
option(value="caIssuers;URI") { "caIssuers;URI" }
|
||||||
|
option(value="caIssuers;email") { "caIssuers;email" }
|
||||||
|
option(value="caIssuers;RID") { "caIssuers;RID" }
|
||||||
|
option(value="caIssuers;DNS") { "caIssuers;DNS" }
|
||||||
|
option(value="caIssuers;IP") { "caIssuers;IP" }
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn parse_aia_entries(text: &str) -> Result<Vec<Value>, String> {
|
||||||
|
let mut entries = Vec::new();
|
||||||
|
for chunk in split_list(text) {
|
||||||
|
let (prefix, value) = chunk.split_once(':').ok_or_else(|| {
|
||||||
|
format!("authority information access entry {chunk:?} must use METHOD;TYPE:value")
|
||||||
|
})?;
|
||||||
|
let (method, kind) = prefix.split_once(';').ok_or_else(|| {
|
||||||
|
format!("authority information access entry {chunk:?} must use METHOD;TYPE:value")
|
||||||
|
})?;
|
||||||
|
let method = match method.trim().to_ascii_lowercase().as_str() {
|
||||||
|
"ocsp" => "ocsp",
|
||||||
|
"caissuers" | "ca_issuers" | "ca-issuers" => "ca_issuers",
|
||||||
|
other => {
|
||||||
|
return Err(format!(
|
||||||
|
"unknown authority information access method {other:?}"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let kind = match kind.trim().to_ascii_lowercase().as_str() {
|
||||||
|
"uri" => "uri",
|
||||||
|
"email" => "email",
|
||||||
|
"rid" => "rid",
|
||||||
|
"dns" => "dns",
|
||||||
|
"ip" => "ip",
|
||||||
|
other => {
|
||||||
|
return Err(format!(
|
||||||
|
"unknown authority information access location type {other:?}"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let location = parse_general_name_entries(&format!("{kind}:{value}"))
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| format!("authority information access entry {chunk:?} is invalid"))?;
|
||||||
|
entries.push(json!({"method": method, "location": location}));
|
||||||
|
}
|
||||||
|
Ok(entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn aia_entries_to_text(entries: &[Value]) -> String {
|
||||||
|
entries
|
||||||
|
.iter()
|
||||||
|
.filter_map(|entry| {
|
||||||
|
let method = match entry
|
||||||
|
.get("method")
|
||||||
|
.and_then(|value| value.as_str())?
|
||||||
|
.to_ascii_lowercase()
|
||||||
|
.as_str()
|
||||||
|
{
|
||||||
|
"ocsp" => "OCSP",
|
||||||
|
"caissuers" | "ca_issuers" | "ca-issuers" => "caIssuers",
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
let location =
|
||||||
|
general_name_entries_to_text(std::slice::from_ref(entry.get("location")?));
|
||||||
|
(!location.is_empty()).then(|| format!("{method};{location}"))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ")
|
||||||
|
}
|
||||||
|
|
||||||
pub fn split_list(text: &str) -> Vec<String> {
|
pub fn split_list(text: &str) -> Vec<String> {
|
||||||
text.split(|c| c == ',' || c == '\n')
|
text.split(|c| c == ',' || c == '\n')
|
||||||
.map(|item| item.trim().to_string())
|
.map(|item| item.trim().to_string())
|
||||||
@@ -425,4 +529,29 @@ mod tests {
|
|||||||
assert_eq!(uri_value(&items[0]), "https://one.example/crl");
|
assert_eq!(uri_value(&items[0]), "https://one.example/crl");
|
||||||
assert_eq!(uri_value(&items[1]), "https://two.example/crl");
|
assert_eq!(uri_value(&items[1]), "https://two.example/crl");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn aia_entries_round_trip_methods_and_location_types() {
|
||||||
|
let text = "OCSP;URI:https://ocsp.example.com, caIssuers;URI:https://example.com/ca.crt, OCSP;DNS:ocsp.example.com, caIssuers;email:pki@example.com, OCSP;RID:1.2.3.4.5, caIssuers;IP:192.0.2.1";
|
||||||
|
let entries = parse_aia_entries(text).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(entries.len(), 6);
|
||||||
|
assert_eq!(entries[1]["method"], "ca_issuers");
|
||||||
|
assert_eq!(
|
||||||
|
entries[1]["location"],
|
||||||
|
json!({"type": "uri", "value": "https://example.com/ca.crt"})
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
entries[4]["location"],
|
||||||
|
json!({"type": "rid", "value": "1.2.3.4.5"})
|
||||||
|
);
|
||||||
|
assert_eq!(aia_entries_to_text(&entries), text);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn aia_entries_reject_invalid_prefixes() {
|
||||||
|
assert!(parse_aia_entries("OCSP;URI").is_err());
|
||||||
|
assert!(parse_aia_entries("bogus;URI:https://example.com").is_err());
|
||||||
|
assert!(parse_aia_entries("OCSP;UPN:user@example.com").is_err());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ use crate::components::list_editor::{
|
|||||||
use crate::components::nav::{app_nav, whoami};
|
use crate::components::nav::{app_nav, whoami};
|
||||||
use crate::components::ui::{page_header, toast_error};
|
use crate::components::ui::{page_header, toast_error};
|
||||||
use crate::model::{
|
use crate::model::{
|
||||||
CertificateDetail, CertificateSummary, DnAttribute, KeySummary, Me, OptionsPage, TemplateSummary,
|
CertificateDetail, CertificateSummary, DnAttribute, KeySummary, Me, OptionsPage,
|
||||||
|
TemplateSummary,
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_EXPLICIT_DN: [&str; 7] = ["C", "ST", "L", "O", "OU", "CN", "emailAddress"];
|
const DEFAULT_EXPLICIT_DN: [&str; 7] = ["C", "ST", "L", "O", "OU", "CN", "emailAddress"];
|
||||||
@@ -134,8 +135,8 @@ pub fn CertificateNewPage(vault_id: String, from_cert: Option<String>) -> View {
|
|||||||
let ocsp_staple = create_signal(false);
|
let ocsp_staple = create_signal(false);
|
||||||
let crl_urls = create_signal(String::new());
|
let crl_urls = create_signal(String::new());
|
||||||
let crl_critical = create_signal(false);
|
let crl_critical = create_signal(false);
|
||||||
let aia_ocsp = create_signal(String::new());
|
let aia = create_signal(String::new());
|
||||||
let aia_ca_issuers = create_signal(String::new());
|
let aia_critical = create_signal(false);
|
||||||
let nc_permitted = create_signal(String::new());
|
let nc_permitted = create_signal(String::new());
|
||||||
let nc_excluded = create_signal(String::new());
|
let nc_excluded = create_signal(String::new());
|
||||||
let nc_critical = create_signal(true);
|
let nc_critical = create_signal(true);
|
||||||
@@ -149,8 +150,8 @@ pub fn CertificateNewPage(vault_id: String, from_cert: Option<String>) -> View {
|
|||||||
let ext = ExtensionSignals {
|
let ext = ExtensionSignals {
|
||||||
crl_urls,
|
crl_urls,
|
||||||
crl_critical,
|
crl_critical,
|
||||||
aia_ocsp,
|
aia,
|
||||||
aia_ca_issuers,
|
aia_critical,
|
||||||
nc_permitted,
|
nc_permitted,
|
||||||
nc_excluded,
|
nc_excluded,
|
||||||
nc_critical,
|
nc_critical,
|
||||||
|
|||||||
@@ -52,8 +52,8 @@ pub fn TemplateEditorPage(vault_id: String, template_id: Option<String>) -> View
|
|||||||
let aki = create_signal(true);
|
let aki = create_signal(true);
|
||||||
let crl_urls = create_signal(String::new());
|
let crl_urls = create_signal(String::new());
|
||||||
let crl_critical = create_signal(false);
|
let crl_critical = create_signal(false);
|
||||||
let aia_ocsp = create_signal(String::new());
|
let aia = create_signal(String::new());
|
||||||
let aia_ca_issuers = create_signal(String::new());
|
let aia_critical = create_signal(false);
|
||||||
let nc_permitted = create_signal(String::new());
|
let nc_permitted = create_signal(String::new());
|
||||||
let nc_excluded = create_signal(String::new());
|
let nc_excluded = create_signal(String::new());
|
||||||
let nc_critical = create_signal(true);
|
let nc_critical = create_signal(true);
|
||||||
@@ -67,8 +67,8 @@ pub fn TemplateEditorPage(vault_id: String, template_id: Option<String>) -> View
|
|||||||
let ext = ExtensionSignals {
|
let ext = ExtensionSignals {
|
||||||
crl_urls,
|
crl_urls,
|
||||||
crl_critical,
|
crl_critical,
|
||||||
aia_ocsp,
|
aia,
|
||||||
aia_ca_issuers,
|
aia_critical,
|
||||||
nc_permitted,
|
nc_permitted,
|
||||||
nc_excluded,
|
nc_excluded,
|
||||||
nc_critical,
|
nc_critical,
|
||||||
|
|||||||
Reference in New Issue
Block a user