M9: Sign with macOS Keychain identities (keys stay in the Keychain): listed in Sign with a Digital ID; sign_keychain_ids, sign_document id keychain:…

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas
2026-10-02 07:55:07 -04:00
co-authored by Claude Opus 5.5
parent 294cbd6716
commit f58570699e
16 changed files with 273 additions and 20 deletions
Generated
+37 -3
View File
@@ -840,6 +840,16 @@ dependencies = [
"libc",
]
[[package]]
name = "core-foundation"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
@@ -853,7 +863,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
"core-foundation 0.9.4",
"core-graphics-types",
"foreign-types",
"libc",
@@ -866,7 +876,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
"core-foundation 0.9.4",
"libc",
]
@@ -3573,6 +3583,7 @@ dependencies = [
"printcraft-fonts",
"rc2",
"rsa",
"security-framework",
"sha1",
"sha2",
"thiserror 2.0.21",
@@ -4091,6 +4102,29 @@ dependencies = [
"zeroize",
]
[[package]]
name = "security-framework"
version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
dependencies = [
"bitflags 2.13.2",
"core-foundation 0.10.1",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "self_cell"
version = "1.3.0"
@@ -5650,7 +5684,7 @@ dependencies = [
"calloop 0.13.0",
"cfg_aliases",
"concurrent-queue",
"core-foundation",
"core-foundation 0.9.4",
"core-graphics",
"cursor-icon",
"dpi",
+1
View File
@@ -51,6 +51,7 @@ fn main() -> eframe::Result {
app.restore(&json);
}
app.integrated_titlebar = integrated;
app.keychain_ids = cfg!(target_os = "macos");
if let Some(file) = &control_file {
let client = app.attach_control(&cc.egui_ctx);
match printcraft_ui_egui::control::serve(client).and_then(|ep| write_control_file(file, ep.port, &ep.token).map(|()| ep.port)) {
+11
View File
@@ -343,6 +343,17 @@ impl Automation {
"accessibility_set_alt" => self.accessibility_set_alt(&a)?,
"sign_id_create" => self.sign_id_create(&a)?,
"sign_document" => self.sign_document(&a)?,
"sign_keychain_ids" => {
#[cfg(target_os = "macos")]
let ids: Vec<Value> = printcraft_engine::sign::keychain::identities(None)
.map_err(failed)?
.iter()
.map(|id| json!({ "id": printcraft_engine::sign::keychain::reference(&id.certificate), "certificate": signing::cert_json(&id.certificate) }))
.collect();
#[cfg(not(target_os = "macos"))]
let ids: Vec<Value> = Vec::new();
json!({ "count": ids.len(), "ids": ids })
}
"sign_trust" => self.sign_trust(&a)?,
"comment_mark" => self.comment_mark(&a)?,
"comment_lock" => self.comment_lock(&a)?,
+7
View File
@@ -117,6 +117,13 @@ impl Automation {
}
fn open_id(&self, a: &Args) -> Result<DigitalId> {
// A macOS Keychain identity: "keychain:<fingerprint>" or "keychain:<common name>".
if let Some(r) = a.str("id")?.strip_prefix("keychain:") {
#[cfg(target_os = "macos")]
return sign::keychain::find(&format!("keychain:{r}")).map_err(failed);
#[cfg(not(target_os = "macos"))]
return Err(failed(format!("keychain:{r}: Keychain identities are only available on macOS")));
}
let path = self.resolve(a.str("id")?, false)?;
let bytes = std::fs::read(&path).map_err(|e| failed(format!("{}: {e}", path.display())))?;
sign::pkcs12::open(&bytes, a.opt_str("password")?.unwrap_or("")).map_err(|e| match e {
+5 -1
View File
@@ -539,7 +539,11 @@ pub fn tools() -> Vec<ToolDef> {
}),
&["name", "password", "path"],
)),
t("sign_document", "Sign a document", "Sign with a digital ID (.p12/.pfx) and save the signed file to `out` (signing always saves, as in Acrobat; the document then shows the signed file). Sign an existing empty signature field (`field`), or a new one on `page` at `rect` (omit rect for an invisible signature). certify: no_changes, form_fill or comments makes a certification signature. PAdES B-B, SHA-256 (SHA-384 for P-384 keys).")
t("sign_keychain_ids", "List Keychain digital IDs", "macOS: the signing identities in the user's keychains (certificate details and the keychain: reference sign_document takes). The private keys stay in the Keychain, which may ask the user to allow their use.")
.ro()
.cmd("sign.digital")
.with(schema(json!({}), &[])),
t("sign_document", "Sign a document", "Sign with a digital ID (a .p12/.pfx path, or on macOS a Keychain identity: \"keychain:<common name or fingerprint>\" from sign_keychain_ids) and save the signed file to `out` (signing always saves, as in Acrobat; the document then shows the signed file). Sign an existing empty signature field (`field`), or a new one on `page` at `rect` (omit rect for an invisible signature). certify: no_changes, form_fill or comments makes a certification signature. PAdES B-B, SHA-256 (SHA-384 for P-384 keys).")
.cmd("sign.digital")
.with(schema(
json!({
+2
View File
@@ -1131,6 +1131,8 @@ fn digital_ids_signing_and_validation_through_tools() {
let doc = ok(&mut a, "doc_open", json!({ "path": "a.pdf" }))["doc"].as_u64().unwrap();
assert_eq!(ok(&mut a, "sign_list", json!({ "doc": doc }))["count"], 0);
// A Keychain identity that doesn't exist (macOS) or Keychains at all (elsewhere).
assert!(matches!(a.call("sign_document", &json!({ "doc": doc, "id": "keychain:No Such Signer", "out": "k.pdf" })), Err(ToolError::Failed(_))));
assert!(matches!(
a.call("sign_document", &json!({ "doc": doc, "id": "ada.p12", "password": "wrong!", "out": "signed.pdf" })),
Err(ToolError::InvalidArgs(_))
+4
View File
@@ -32,6 +32,10 @@ rc2 = "0.9"
aws-lc-rs = { version = "1.18", default-features = false, features = ["aws-lc-sys", "prebuilt-nasm"] }
getrandom = "0.4"
[target.'cfg(target_os = "macos")'.dependencies]
# Signing with Keychain identities (MIT OR Apache-2.0).
security-framework = "3.7"
[target.'cfg(target_arch = "wasm32")'.dependencies]
# crypto-bigint asks for randomness; in the browser it comes from Web Crypto.
getrandom = { version = "0.4", features = ["wasm_js"] }
+78
View File
@@ -0,0 +1,78 @@
//! Digital IDs from the macOS Keychain: identities (a certificate with its private key) that
//! sign through the Security framework. The private key never leaves the Keychain; macOS may
//! ask the user to allow PrintCraft to use it.
use std::path::Path;
use std::sync::Arc;
use security_framework::item::{ItemClass, ItemSearchOptions, Limit, Reference, SearchResult};
use security_framework::key::{Algorithm, SecKey};
use security_framework::os::macos::keychain::SecKeychain;
use crate::keys::{DigestAlg, ExternalKey, PrivateKey, PublicKey};
use crate::{Certificate, DigitalId, SignError};
struct KeychainKey {
key: SecKey,
rsa: bool,
}
impl ExternalKey for KeychainKey {
fn sign(&self, alg: DigestAlg, msg: &[u8]) -> Result<Vec<u8>, SignError> {
let a = match (self.rsa, alg) {
(true, DigestAlg::Sha384) => Algorithm::RSASignatureMessagePKCS1v15SHA384,
(true, DigestAlg::Sha512) => Algorithm::RSASignatureMessagePKCS1v15SHA512,
(true, _) => Algorithm::RSASignatureMessagePKCS1v15SHA256,
(false, DigestAlg::Sha384) => Algorithm::ECDSASignatureMessageX962SHA384,
(false, DigestAlg::Sha512) => Algorithm::ECDSASignatureMessageX962SHA512,
(false, _) => Algorithm::ECDSASignatureMessageX962SHA256,
};
self.key.create_signature(a, msg).map_err(|e| SignError::Crypto(format!("the Keychain didn't sign: {e}")))
}
}
/// The `keychain:<SHA-256 of the certificate>` reference PrintCraft keeps for an identity.
pub fn reference(c: &Certificate) -> String {
let d = DigestAlg::Sha256.digest(&[&c.raw]);
format!("keychain:{}", d.iter().map(|b| format!("{b:02x}")).collect::<String>())
}
/// The identity a reference (or a certificate common name) names.
pub fn find(reference_or_name: &str) -> Result<DigitalId, SignError> {
identities(None)?
.into_iter()
.find(|id| {
reference(&id.certificate) == reference_or_name
|| id.certificate.subject.common_name() == Some(reference_or_name.trim_start_matches("keychain:"))
})
.ok_or_else(|| SignError::Crypto(format!("no Keychain identity {reference_or_name}")))
}
/// The signing identities in the user's keychains (or only in the keychain file `keychain`).
/// Identities with keys PrintCraft can't use (other curves, Ed25519) are left out.
pub fn identities(keychain: Option<&Path>) -> Result<Vec<DigitalId>, SignError> {
let mut search = ItemSearchOptions::new();
search.class(ItemClass::identity()).load_refs(true).limit(Limit::All);
let kc;
if let Some(p) = keychain {
kc = SecKeychain::open(p).map_err(|e| SignError::Crypto(format!("{}: {e}", p.display())))?;
search.keychains(std::slice::from_ref(&kc));
}
let found = match search.search() {
Ok(f) => f,
// "The specified item could not be found in the keychain."
Err(e) if e.code() == -25300 => return Ok(Vec::new()),
Err(e) => return Err(SignError::Crypto(format!("the Keychain couldn't be searched: {e}"))),
};
let mut out = Vec::new();
for r in found {
let SearchResult::Ref(Reference::Identity(id)) = r else { continue };
let (Ok(cert), Ok(key)) = (id.certificate(), id.private_key()) else { continue };
let Ok(certificate) = Certificate::parse(&cert.to_der()) else { continue };
let rsa = matches!(certificate.public_key, PublicKey::Rsa { .. });
let key = PrivateKey::external(certificate.public_key.clone(), Arc::new(KeychainKey { key, rsa }));
let friendly_name = Some(certificate.display_name());
out.push(DigitalId { key, certificate, chain: Vec::new(), friendly_name });
}
Ok(out)
}
+20
View File
@@ -223,6 +223,14 @@ enum Inner {
Rsa,
P256(p256::ecdsa::SigningKey),
P384(p384::ecdsa::SigningKey),
/// A key held elsewhere (the macOS Keychain, a token) that signs on request.
External(std::sync::Arc<dyn ExternalKey>),
}
/// A private key PrintCraft can't read, only ask to sign (OS key stores, tokens).
pub trait ExternalKey: Send + Sync {
/// Sign `msg`, hashing it with `alg` (PKCS #1 v1.5 for RSA, DER-encoded ECDSA).
fn sign(&self, alg: DigestAlg, msg: &[u8]) -> Result<Vec<u8>, SignError>;
}
/// A private key that can sign.
@@ -240,6 +248,17 @@ impl std::fmt::Debug for PrivateKey {
}
impl PrivateKey {
/// A key held outside PrintCraft's memory (its public half is `public`). It can't be
/// exported to a PKCS #12 file.
pub fn external(public: PublicKey, key: std::sync::Arc<dyn ExternalKey>) -> PrivateKey {
PrivateKey { inner: Inner::External(key), public, pkcs8: Vec::new() }
}
/// Whether the key lives outside the app (and so can't be exported).
pub fn is_external(&self) -> bool {
matches!(self.inner, Inner::External(_))
}
/// From a PKCS#8 `PrivateKeyInfo`.
pub fn from_pkcs8(info: &[u8]) -> Result<PrivateKey, SignError> {
let t = Tlv::parse_all(info)?.children()?;
@@ -387,6 +406,7 @@ impl PrivateKey {
let s: p256::ecdsa::Signature = k.sign_prehash(&alg.digest(&[msg])).map_err(|e| SignError::Crypto(e.to_string()))?;
Ok(s.to_der().as_bytes().to_vec())
}
Inner::External(k) => k.sign(alg, msg),
Inner::P384(k) => {
use p384::ecdsa::signature::hazmat::PrehashSigner;
let s: p384::ecdsa::Signature = k.sign_prehash(&alg.digest(&[msg])).map_err(|e| SignError::Crypto(e.to_string()))?;
+2
View File
@@ -7,6 +7,8 @@
pub mod cms;
pub mod der;
#[cfg(target_os = "macos")]
pub mod keychain;
pub mod keys;
pub mod pdf;
pub mod pkcs12;
+2
View File
@@ -8,6 +8,7 @@ The password of every `.p12` is `test`.
| `rsa-aes.p12` | RSA 2048 self-signed "Test Signer RSA"; PBES2 AES-256-CBC, SHA-256 MAC (OpenSSL 3 default) |
| `rsa-legacy.p12` | the same key and certificate; RC2-40 certificates, 3DES key, SHA-1 MAC (`-legacy`) |
| `ec-p256.p12` | P-256 self-signed "Test Signer EC", no friendly name |
| `ec-legacy.p12` | `ec-p256.p12` re-exported with `-legacy` (3DES, SHA-1 MAC): the format macOS `security import` accepts |
| `ec-p384.p12` | P-384 self-signed "Test Signer P384"; AES-128-CBC, SHA-1 MAC |
| `chain.p12` | RSA 2048 "Ada Lovelace" issued by a P-256 test root, chain included |
| `rsa.crt.pem`, `ca.crt.pem` | the RSA certificate and the test root alone |
@@ -20,6 +21,7 @@ openssl pkcs12 -export -inkey rsa.key -in rsa.crt -out rsa-aes.p12 -passout pass
openssl pkcs12 -export -legacy -inkey rsa.key -in rsa.crt -out rsa-legacy.p12 -passout pass:test -name "Test Signer RSA"
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -keyout ec.key -out ec.crt -days 3650 -subj "/CN=Test Signer EC/O=PrintCraft Tests" -set_serial 2002
openssl pkcs12 -export -inkey ec.key -in ec.crt -out ec-p256.p12 -passout pass:test
openssl pkcs12 -export -legacy -inkey ec.key -in ec.crt -out ec-legacy.p12 -passout pass:test -name "Test Signer EC"
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-384 -nodes -keyout ec3.key -out ec3.crt -days 3650 -subj "/CN=Test Signer P384" -set_serial 3003
openssl pkcs12 -export -inkey ec3.key -in ec3.crt -out ec-p384.p12 -passout pass:test -certpbe AES-128-CBC -keypbe AES-128-CBC -macalg sha1
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -keyout ca.key -out ca.crt -days 3650 -subj "/CN=PrintCraft Test Root CA/O=PrintCraft Tests" -set_serial 1 -addext "basicConstraints=critical,CA:TRUE" -addext "keyUsage=critical,keyCertSign,cRLSign"
Binary file not shown.
+46
View File
@@ -206,3 +206,49 @@ trailer << /Root 1 0 R >>
assert_eq!((s.status, s.certify, s.modification.clone()), (Status::Unknown, certify, Modification::None), "{:?}", s.details);
}
}
/// Signing with Keychain identities, in a throwaway keychain file. Ignored by default: creating
/// a keychain touches the user's keychain search list (restored afterwards).
#[cfg(target_os = "macos")]
#[test]
#[ignore = "creates a temporary macOS keychain; run with --ignored"]
fn signing_with_keychain_identities() {
use std::process::Command;
let dir = std::env::temp_dir().join(format!("printcraft-keychain-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let kc = dir.join("test.keychain-db");
let sec = |args: &[&str]| Command::new("security").args(args).output().unwrap();
let list = String::from_utf8(sec(&["list-keychains", "-d", "user"]).stdout).unwrap();
let saved: Vec<String> = list.lines().map(|l| l.trim().trim_matches('"').to_string()).filter(|l| !l.is_empty()).collect();
let k = kc.to_str().unwrap();
assert!(sec(&["create-keychain", "-p", "pc-test", k]).status.success());
let restore = || {
let mut args = vec!["list-keychains", "-d", "user", "-s"];
args.extend(saved.iter().map(String::as_str));
sec(&args);
};
restore();
// Everything that can fail runs before the clean-up below.
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
sec(&["unlock-keychain", "-p", "pc-test", k]);
// macOS imports only legacy-format PKCS #12 (SHA-1 MAC).
for f in ["rsa-legacy.p12", "ec-legacy.p12"] {
let p = format!("{}/tests/data/{f}", env!("CARGO_MANIFEST_DIR"));
let out = sec(&["import", &p, "-k", k, "-P", "test", "-A"]);
assert!(out.status.success(), "{f}: {}", String::from_utf8_lossy(&out.stderr));
}
let ids = printcraft_sign::keychain::identities(Some(&kc)).unwrap();
assert_eq!(ids.len(), 2, "{ids:?}");
for id in &ids {
assert!(id.key.is_external());
let signed = printcraft_sign::sign(&open(&fixture()), id, &opts()).unwrap();
let trusted = signatures(&open(&signed), &signed, &TrustStore { certs: vec![id.certificate.clone()] });
let s = trusted.iter().find(|s| s.signed).unwrap();
assert_eq!(s.status, Status::Valid, "{:?}", s.details);
}
}));
sec(&["delete-keychain", k]);
restore();
let _ = std::fs::remove_dir_all(&dir);
result.unwrap();
}
+5 -1
View File
@@ -292,6 +292,8 @@ pub struct PrintCraftApp {
pub a11y: a11y_ui::A11yState,
pub a11y_skipped: std::collections::BTreeSet<printcraft_engine::a11y::Rule>,
pub alt_draft: a11y_ui::AltDraft,
/// List the macOS Keychain's signing identities among the digital IDs (the desktop app).
pub keychain_ids: bool,
/// The last space audit.
pub space_audit: Vec<printcraft_engine::optimize::SpaceUse>,
/// Combine files: the files staged so far.
@@ -422,6 +424,7 @@ impl PrintCraftApp {
a11y: a11y_ui::A11yState::default(),
a11y_skipped: Default::default(),
alt_draft: Default::default(),
keychain_ids: false,
space_audit: Vec::new(),
combine_draft: Vec::new(),
custom_stamps: Vec::new(),
@@ -745,7 +748,8 @@ impl PrintCraftApp {
"recent": self.recent,
"theme": self.theme,
"signature": self.signature,
"digital_ids": self.digital_ids,
// Keychain identities are read from macOS each time.
"digital_ids": self.digital_ids.iter().filter(|e| !e.path.starts_with("keychain:")).collect::<Vec<_>>(),
"trusted": trusted,
"custom_stamps": stamps_ui::encode(&self.custom_stamps),
})
+48 -10
View File
@@ -162,6 +162,14 @@ fn id_dir() -> Option<PathBuf> {
crate::recovery::RecoveryStore::default_dir().and_then(|d| d.parent().map(|p| p.join("Digital IDs")))
}
/// A Keychain identity by its `keychain:` reference.
fn keychain_id(reference: &str) -> Result<DigitalId, String> {
#[cfg(target_os = "macos")]
return sign::keychain::find(reference).map_err(|e| e.to_string());
#[cfg(not(target_os = "macos"))]
Err(format!("{reference}: Keychain identities are only available on macOS"))
}
pub fn entry_for(path: &str, c: &Certificate) -> DigitalIdEntry {
DigitalIdEntry {
path: path.to_string(),
@@ -175,10 +183,27 @@ pub fn entry_for(path: &str, c: &Certificate) -> DigitalIdEntry {
impl PrintCraftApp {
/// Start signing: the rectangle (or field) is known; show Sign with a Digital ID.
pub fn start_signing(&mut self, page: usize, rect: Option<[f64; 4]>, field: Option<String>, certify: Option<u8>) {
self.refresh_keychain_ids();
self.sign_draft = Some(SignDraft::new(page, rect, field, certify, self.digital_ids.len()));
self.dialog = Some(crate::Dialog::Sign);
}
/// List the macOS Keychain's signing identities (after the file-based IDs).
fn refresh_keychain_ids(&mut self) {
self.digital_ids.retain(|e| !e.path.starts_with("keychain:"));
#[cfg(target_os = "macos")]
if self.keychain_ids {
match sign::keychain::identities(None) {
Ok(ids) => {
for id in ids {
self.digital_ids.push(entry_for(&sign::keychain::reference(&id.certificate), &id.certificate));
}
}
Err(e) => self.notify(format!("The Keychain's digital IDs couldn't be listed: {e}")),
}
}
}
/// Add a digital ID file to the list (or select it if it's there).
pub fn add_digital_id(&mut self, path: &str, cert: &Certificate) -> usize {
if let Some(i) = self.digital_ids.iter().position(|e| e.path == path) {
@@ -246,11 +271,15 @@ impl PrintCraftApp {
let Some((_, doc_id)) = self.active_ids() else { return Err("no document".into()) };
let d = self.sign_draft.clone().ok_or("nothing to sign")?;
let entry = d.selected.and_then(|i| self.digital_ids.get(i)).cloned().ok_or("Choose a digital ID.")?;
let bytes = std::fs::read(&entry.path).map_err(|e| format!("{}: {e}", entry.path))?;
let id = sign::pkcs12::open(&bytes, &d.password).map_err(|e| match e {
sign::SignError::WrongPassword => "The password is incorrect.".to_string(),
e => e.to_string(),
})?;
let id = if entry.path.starts_with("keychain:") {
keychain_id(&entry.path)?
} else {
let bytes = std::fs::read(&entry.path).map_err(|e| format!("{}: {e}", entry.path))?;
sign::pkcs12::open(&bytes, &d.password).map_err(|e| match e {
sign::SignError::WrongPassword => "The password is incorrect.".to_string(),
e => e.to_string(),
})?
};
let some = |s: &str| (!s.trim().is_empty()).then(|| s.trim().to_string());
let opts = SignOptions {
field: d.field.clone(),
@@ -370,7 +399,8 @@ fn choose(ui: &mut egui::Ui, app: &mut PrintCraftApp, t: &Tokens) -> bool {
icons::paint(ui, Rect::from_min_size(rect.min + vec2(10.0, 15.0), vec2(20.0, 20.0)), "badge-check", 18.0, t.accent);
ui.painter().text(rect.min + vec2(40.0, 9.0), egui::Align2::LEFT_TOP, &e.name, theme::semibold(13.0), t.text);
let sub = format!(
"{}Issued by: {}, Expires: {}",
"{}{}Issued by: {}, Expires: {}",
if e.path.starts_with("keychain:") { "Keychain · " } else { "" },
if e.email.is_empty() { String::new() } else { format!("{} · ", e.email) },
e.issuer,
e.expires
@@ -538,6 +568,7 @@ fn sign_as(ui: &mut egui::Ui, app: &mut PrintCraftApp, t: &Tokens) -> bool {
return false;
};
title(ui, &format!("{} as \"{}\"", if d.certify.is_some() { "Certify" } else { "Sign" }, entry.name));
let in_keychain = entry.path.starts_with("keychain:");
let mut close = false;
let mut go = false;
if d.rect.is_some() || d.field.is_some() {
@@ -579,10 +610,17 @@ fn sign_as(ui: &mut egui::Ui, app: &mut PrintCraftApp, t: &Tokens) -> bool {
let l = ui.label("Location");
ui.add(egui::TextEdit::singleline(&mut d.location).hint_text("Optional").desired_width(260.0)).labelled_by(l.id);
ui.end_row();
let l = ui.label("Digital ID password");
let r = ui.add(egui::TextEdit::singleline(&mut d.password).password(true).desired_width(200.0)).labelled_by(l.id);
if r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
go = true;
if in_keychain {
ui.label("");
ui.label(
egui::RichText::new("The key is in the macOS Keychain, which may ask to allow PrintCraft to use it.").small().color(t.text_muted),
);
} else {
let l = ui.label("Digital ID password");
let r = ui.add(egui::TextEdit::singleline(&mut d.password).password(true).desired_width(200.0)).labelled_by(l.id);
if r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
go = true;
}
}
ui.end_row();
});
+5 -5
View File
@@ -5759,11 +5759,11 @@ title = "Sign with macOS Keychain identities"
area = "H"
tier = "P0"
milestone = "M9"
status = "planned"
commands = []
tools = []
evidence = []
notes = ""
status = "shipped"
commands = ["sign.digital"]
tools = ["sign_keychain_ids", "sign_document"]
evidence = ["crates/sign/tests/pdf.rs::signing_with_keychain_identities", "crates/automation/tests/automation.rs::digital_ids_signing_and_validation_through_tools"]
notes = "Security framework identities (RSA PKCS #1 v1.5, ECDSA P-256/P-384) listed with the digital IDs in Sign with a Digital ID; the key never leaves the Keychain (ExternalKey); keychain:<fingerprint|name> for agents. The test uses a throwaway keychain file (run with --ignored)"
[[feature]]
id = "sign.windows-cert-store"