If secret key material is passed via --keyring, use it.

- If the user supplies a keyring using `--keyring`, and it contains
    the secret key material that we are looking for, use it.
This commit is contained in:
Neal H. Walfield 2024-06-09 01:23:14 +02:00
parent cb13f883be
commit 47e076d890
No known key found for this signature in database
GPG Key ID: 6863C9AD5B4D22D3
4 changed files with 125 additions and 8 deletions

View File

@ -366,6 +366,7 @@ fn main() -> Result<()> {
cert_store_path: c.cert_store.clone(),
pep_cert_store_path: c.pep_cert_store.clone(),
keyrings: c.keyring.clone(),
keyring_tsks: Default::default(),
cert_store: OnceCell::new(),
trust_roots: c.trust_roots.clone(),
trust_root_local: Default::default(),

View File

@ -1,4 +1,5 @@
use std::borrow::Borrow;
use std::collections::BTreeMap;
use std::fmt;
use std::path::{Path, PathBuf};
use std::sync::Arc;
@ -85,6 +86,10 @@ pub struct Sq<'store, 'rstore>
pub cert_store_path: Option<PathBuf>,
pub pep_cert_store_path: Option<PathBuf>,
pub keyrings: Vec<PathBuf>,
// Map from key fingerprint to cert fingerprint and the key.
pub keyring_tsks: OnceCell<BTreeMap<
Fingerprint,
(Fingerprint, Key<key::PublicParts, key::UnspecifiedRole>)>>,
// This will be set if --no-cert-store is not passed, OR --keyring
// is passed.
pub cert_store: OnceCell<WotStore<'store, 'rstore>>,
@ -194,6 +199,7 @@ impl<'store: 'rstore, 'rstore> Sq<'store, 'rstore> {
};
let keyring = cert_store::store::Certs::empty();
let mut tsks = BTreeMap::new();
let mut error = None;
for filename in self.keyrings.iter() {
let f = std::fs::File::open(filename)
@ -204,6 +210,14 @@ impl<'store: 'rstore, 'rstore> Sq<'store, 'rstore> {
for cert in parser {
match cert {
Ok(cert) => {
for key in cert.keys() {
if key.has_secret() {
tsks.insert(
key.fingerprint(),
(cert.fingerprint(), key.clone()));
}
}
keyring.update(Arc::new(cert.into()))
.expect("implementation doesn't fail");
}
@ -216,6 +230,8 @@ impl<'store: 'rstore, 'rstore> Sq<'store, 'rstore> {
}
}
self.keyring_tsks.set(tsks).expect("uninitialized");
if let Some(err) = error {
return Err(err).context("Parsing keyrings");
}
@ -394,6 +410,23 @@ impl<'store: 'rstore, 'rstore> Sq<'store, 'rstore> {
}))
}
/// Returns the secret keys found in any specified keyrings.
pub fn keyring_tsks(&self)
-> &BTreeMap<Fingerprint,
(Fingerprint, Key<key::PublicParts, key::UnspecifiedRole>)>
{
if let Some(keyring_tsks) = self.keyring_tsks.get() {
keyring_tsks
} else {
// This also initializes keyring_tsks.
let _ = self.cert_store();
// If something went wrong, we just set it to an empty
// map.
self.keyring_tsks.get_or_init(|| BTreeMap::new())
}
}
/// Looks up an identifier.
///
/// This matches on both the primary key and the subkeys.
@ -1037,15 +1070,15 @@ impl<'store: 'rstore, 'rstore> Sq<'store, 'rstore> {
-> Result<(Box<dyn crypto::Signer + Send + Sync>, Option<Password>)>
where P: key::KeyParts + Clone, R: key::KeyRole + Clone
{
let try_tsk = |ka: &KeyAmalgamation<_, _, R2>|
let try_tsk = |cert: &Cert, key: &Key<_, _>|
-> Result<(_, _)>
{
if let Some(secret) = ka.key().optional_secret() {
if let Some(secret) = key.optional_secret() {
let (unencrypted, password) = match secret {
SecretKeyMaterial::Encrypted(ref e) => {
// try passwords from already existing keys
match self.cached_passwords().find_map(|password| {
e.decrypt(ka.pk_algo(), &password).ok()
e.decrypt(key.pk_algo(), &password).ok()
.map(|key| (key, password.clone()))
}) {
Some((unencrypted, password)) =>
@ -1053,10 +1086,10 @@ impl<'store: 'rstore, 'rstore> Sq<'store, 'rstore> {
None => {
let password = password::prompt_to_unlock(
&format!("key {}/{}",
ka.cert().keyid(),
ka.keyid()))?;
cert.keyid(),
key.keyid()))?;
let key = e.decrypt(ka.pk_algo(), &password)
let key = e.decrypt(key.pk_algo(), &password)
.map_err(|_| anyhow!("Incorrect password."))?;
self.cache_password(password.clone());
@ -1071,7 +1104,7 @@ impl<'store: 'rstore, 'rstore> Sq<'store, 'rstore> {
Ok((
Box::new(
crypto::KeyPair::new(
ka.key().clone()
key.clone()
.parts_into_public()
.role_into_unspecified(),
unencrypted).unwrap()
@ -1082,6 +1115,20 @@ impl<'store: 'rstore, 'rstore> Sq<'store, 'rstore> {
Err(anyhow!("No secret key material."))
}
};
let try_keyrings = |cert: &Cert, key: &Key<_, _>|
-> Result<(_, _)>
{
let keyring_tsks = self.keyring_tsks();
if let Some((cert_fpr, key))
= keyring_tsks.get(&key.fingerprint())
{
if cert_fpr == &cert.fingerprint() {
return try_tsk(cert, key);
}
}
Err(anyhow!("No secret key material."))
};
let try_keystore = |ka: &KeyAmalgamation<_, _, R2>|
-> Result<(_, _)>
{
@ -1140,7 +1187,11 @@ impl<'store: 'rstore, 'rstore> Sq<'store, 'rstore> {
Err(anyhow!("Key not managed by keystore."))
};
if let Ok((key, password)) = try_tsk(ka) {
let key = ka.key().parts_as_public().role_as_unspecified();
if let Ok((key, password)) = try_tsk(ka.cert(), key) {
Ok((key, password))
} else if let Ok((key, password)) = try_keyrings(ka.cert(), key) {
Ok((key, password))
} else if let Ok((key, password)) = try_keystore(ka) {
Ok((key, password))

View File

@ -545,6 +545,36 @@ impl Sq {
extra_args, certifier, cert, userid, output_file, true)
.expect("success")
}
// Strips the secret key material from input. Writes it to
// `output_file`, if `Some`.
pub fn toolbox_extract_cert<'a, P, Q>(&self, input: P,
output_file: Q)
-> Cert
where P: AsRef<Path>,
Q: Into<Option<&'a Path>>,
{
let output_file = output_file.into();
let mut cmd = self.command();
cmd.args([ "toolbox", "extract-cert" ]);
cmd.arg(input.as_ref());
if let Some(output_file) = output_file {
cmd.arg("--output").arg(output_file);
}
let output = self.run(cmd, Some(true));
if let Some(output_file) = output_file {
if output_file != &PathBuf::from("-") {
return Cert::from_file(&output_file)
.expect("can parse certificate");
}
}
// Read from stdout.
Cert::from_bytes(&output.stdout)
.expect("can parse certificate")
}
}
/// Generate a new key in a temporary directory and return its TempDir,

View File

@ -20,6 +20,9 @@ use openpgp::policy::StandardPolicy;
use openpgp::serialize::stream::{Message, Signer, Compressor, LiteralWriter};
use openpgp::serialize::Serialize;
mod common;
use common::Sq;
const P: &StandardPolicy = &StandardPolicy::new();
fn artifact(filename: &str) -> String {
@ -1343,3 +1346,35 @@ fn sq_verify_wot() -> Result<()> {
Ok(())
}
#[test]
fn sq_sign_keyring() {
// Check that we can provide the secret key material via
// --keyring.
let sq = Sq::new();
let (_alice, alice_pgp, _alice_rev)
= sq.key_generate(&[], &["alice"]);
let mut alice_pub = alice_pgp.clone();
alice_pub.set_extension("pub");
sq.toolbox_extract_cert(&alice_pgp, Some(&*alice_pub));
// We pass the secret key material via --keyring. This should
// work.
let mut cmd = sq.command();
cmd.arg("--keyring").arg(&alice_pgp)
.arg("sign")
.arg("--signer-file").arg(&alice_pub);
sq.run(cmd, Some(true));
// If we don't pass the secret key material, this should fail.
let mut cmd = sq.command();
cmd.arg("sign")
.arg("--signer-file").arg(&alice_pub);
sq.run(cmd, Some(false));
}