Add --keyring to specify additional keyrings to search

- Add a new top-level option, `--keyring`, which allows users to
    specify additional keyrings to search.

  - When a lookup is performed, all keyrings are searched in addition
    to any certificate store, and the results are merged.

  - Keyrings are read only.
This commit is contained in:
Neal H. Walfield 2023-03-27 16:23:29 +02:00
parent 0e59f2f560
commit 8cf08e2470
No known key found for this signature in database
GPG Key ID: 6863C9AD5B4D22D3
4 changed files with 211 additions and 37 deletions

2
NEWS
View File

@ -49,6 +49,8 @@
certify the specified bindings.
- Add `sq link retract`, which retracts certifications made by the
local trust root on the specified bindings.
- Add a top-level option, `--keyring`, to allow the user to specify
additional keyrings to search for certificates.
* Deprecated functionality
- `sq key generate --creation-time TIME` is deprecated in favor of
`sq key generate --time TIME`.

112
src/sq.rs
View File

@ -341,8 +341,13 @@ pub struct Config<'a> {
/// Have we emitted the warning yet?
unstable_cli_warning_emitted: bool,
// --no-cert-store
no_rw_cert_store: bool,
cert_store_path: Option<PathBuf>,
cert_store: Option<OnceCell<cert_store::CertStore<'a>>>,
keyrings: Vec<PathBuf>,
// This will be set if the cert store is enabled (--no-cert-store
// is not passed), OR --keyring is passed.
cert_store: OnceCell<cert_store::CertStore<'a>>,
// The value of --trust-root.
trust_roots: Vec<Fingerprint>,
@ -424,14 +429,12 @@ impl<'store> Config<'store> {
/// If the cert store is disabled, returns `Ok(None)`. If it is not yet
/// open, opens it.
fn cert_store(&self) -> Result<Option<&cert_store::CertStore<'store>>> {
let cert_store = if let Some(cert_store) = self.cert_store.as_ref() {
cert_store
} else {
if self.no_rw_cert_store && self.keyrings.is_empty() {
// The cert store is disabled.
return Ok(None);
};
}
if let Some(cert_store) = cert_store.get() {
if let Some(cert_store) = self.cert_store.get() {
// The cert store is already initialized, return it.
return Ok(Some(cert_store));
}
@ -472,30 +475,66 @@ impl<'store> Config<'store> {
};
// We need to initialize the cert store.
let pathbuf;
let path = if let Some(path) = self.cert_store_path.as_ref() {
path
let mut cert_store = if ! self.no_rw_cert_store {
// Open the cert-d.
let pathbuf;
let path = if let Some(path) = self.cert_store_path.as_ref() {
path
} else {
// XXX: openpgp-cert-d doesn't yet export this:
// https://gitlab.com/sequoia-pgp/pgp-cert-d/-/issues/34
// Remove this when it does.
pathbuf = dirs::data_dir()
.expect("Unsupported platform")
.join("pgp.cert.d");
&pathbuf
};
create_dirs(path)
.and_then(|_| cert_store::CertStore::open(path))
.with_context(|| {
format!("While opening the certificate store at {:?}",
path)
})?
} else {
// XXX: openpgp-cert-d doesn't yet export this:
// https://gitlab.com/sequoia-pgp/pgp-cert-d/-/issues/34
// Remove this when it does.
pathbuf = dirs::data_dir()
.expect("Unsupported platform")
.join("pgp.cert.d");
&pathbuf
cert_store::CertStore::empty()
};
let instance = create_dirs(path)
.and_then(|_| cert_store::CertStore::open(path))
.with_context(|| {
format!("While opening the certificate store at {:?}",
path)
})?;
let mut keyring = cert_store::store::Certs::empty();
let mut error = None;
for filename in self.keyrings.iter() {
let f = std::fs::File::open(filename)
.with_context(|| format!("Open {:?}", filename))?;
let parser = RawCertParser::from_reader(f)
.with_context(|| format!("Parsing {:?}", filename))?;
let _ = cert_store.set(instance);
Ok(Some(self.cert_store
.as_ref().expect("enabled")
.get().expect("just configured")))
for cert in parser {
match cert {
Ok(cert) => {
keyring.update(Cow::Owned(cert.into()))
.expect("implementation doesn't fail");
}
Err(err) => {
eprint!("Parsing certificate in {:?}: {}",
filename, err);
error = Some(err);
}
}
}
}
if let Some(err) = error {
return Err(err).context("Parsing keyrings");
}
cert_store.add_backend(
Box::new(keyring),
cert_store::AccessMode::Always);
let _ = self.cert_store.set(cert_store);
Ok(Some(self.cert_store.get().expect("just configured")))
}
/// Returns the cert store.
@ -515,15 +554,16 @@ impl<'store> Config<'store> {
fn cert_store_mut(&mut self)
-> Result<Option<&mut cert_store::CertStore<'store>>>
{
if self.no_rw_cert_store {
return Err(anyhow::anyhow!(
"Operation requires a certificate store, \
but the certificate store is disabled"));
}
// self.cert_store() will do any required initialization, but
// it will return an immutable reference.
self.cert_store()?;
if let Some(cert_store) = self.cert_store.as_mut() {
Ok(cert_store.get_mut())
} else {
Ok(None)
}
Ok(self.cert_store.get_mut())
}
/// Returns a mutable reference to the cert store.
@ -1090,12 +1130,10 @@ fn main() -> Result<()> {
policy: policy.clone(),
time,
unstable_cli_warning_emitted: false,
no_rw_cert_store: c.no_cert_store,
cert_store_path: c.cert_store.clone(),
cert_store: if c.no_cert_store {
None
} else {
Some(OnceCell::new())
},
keyrings: c.keyring.clone(),
cert_store: OnceCell::new(),
trust_roots: c.trust_roots.clone(),
trust_root_local: Default::default(),
};

View File

@ -95,6 +95,18 @@ the OpenPGP certificate directory at `$HOME/.local/share/pgp.cert.d`, \
and creates it if it does not exist."
)]
pub cert_store: Option<PathBuf>,
#[clap(
long,
value_name = "PATH",
help = "Specifies the location of a keyring to use",
long_help = "\
Specifies the location of a keyring to use. Keyrings are used in \
addition to any certificate store. The content of the keyring is \
not imported into the certificate store. When a certificate is \
looked up, it is looked up in all keyrings and any certificate \
store, and the results are merged together."
)]
pub keyring: Vec<PathBuf>,
#[clap(
long = "output-format",
value_name = "FORMAT",

View File

@ -292,6 +292,128 @@ mod integration {
&[&bob_pgp]);
}
Ok(())
}
// Encrypt a message to two recipients: one whose certificate is
// in the certificate store, and one whose certificated is in a
// keyring.
#[test]
fn sq_encrypt_keyring() -> Result<()>
{
let dir = TempDir::new()?;
let certd = dir.path().join("cert.d").display().to_string();
std::fs::create_dir(&certd).expect("mkdir works");
let alice_pgp = dir.path().join("alice.pgp").display().to_string();
let bob_pgp = dir.path().join("bob.pgp").display().to_string();
// Generate the keys.
let mut cmd = Command::cargo_bin("sq")?;
cmd.args(["--cert-store", &certd,
"key", "generate",
"--expires", "never",
"--userid", "<alice@example.org>",
"--export", &alice_pgp]);
cmd.assert().success();
let alice = Cert::from_file(&alice_pgp)?;
let alice_fpr = alice.fingerprint().to_string();
let mut cmd = Command::cargo_bin("sq")?;
cmd.args(["--cert-store", &certd,
"key", "generate",
"--expires", "never",
"--userid", "<bob@example.org>",
"--export", &bob_pgp]);
cmd.assert().success();
let bob = Cert::from_file(&bob_pgp)?;
let bob_fpr = bob.keyid().to_string();
const MESSAGE: &[u8] = &[0x42; 24 * 1024 + 23];
let encrypt = |keyrings: &[&str],
recipients: &[&str],
decryption_keys: &[&str]|
{
let mut cmd = Command::cargo_bin("sq").unwrap();
cmd.args(["--cert-store", &certd]);
// Make a string for debugging.
let mut cmd_display = "sq".to_string();
for keyring in keyrings.iter() {
cmd.args(["--keyring", keyring]);
cmd_display.push_str(" --keyring ");
cmd_display.push_str(keyring);
}
cmd_display.push_str(" encrypt");
cmd.arg("encrypt");
for recipient in recipients.iter() {
cmd.args(["--recipient-cert", recipient]);
cmd_display.push_str(" --recipient-cert ");
cmd_display.push_str(recipient);
}
cmd.write_stdin(MESSAGE);
let output = cmd.output().expect("success");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
if decryption_keys.is_empty() {
assert!(! output.status.success(),
"'{}' should have failed\nstdout:\n{}\nstderr:\n{}",
cmd_display, stdout, stderr);
} else {
assert!(output.status.success(),
"'{}' should have succeeded\nstdout:\n{}\nstderr:\n{}",
cmd_display, stdout, stderr);
for key in decryption_keys.iter() {
let mut cmd = Command::cargo_bin("sq").unwrap();
cmd.args(["--no-cert-store",
"decrypt",
"--recipient-file",
&key])
.write_stdin(stdout.as_bytes());
let output = cmd.output().expect("success");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(output.status.success(),
"'{}' decryption should succeed\nstdout:\n{}\nstderr:\n{}",
cmd_display, stdout, stderr);
}
}
};
encrypt(&[&alice_pgp, &bob_pgp],
&[&alice_fpr, &bob_fpr],
&[&alice_pgp, &bob_pgp]);
// Import Alice's certificate.
let mut cmd = Command::cargo_bin("sq")?;
cmd.args(["--cert-store", &certd,
"import", &alice_pgp]);
let output = cmd.output().expect("success");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(output.status.success(),
"sq import should succeed\nstdout:\n{}\nstderr:\n{}",
stdout, stderr);
encrypt(&[&alice_pgp, &bob_pgp],
&[&alice_fpr, &bob_fpr],
&[&alice_pgp, &bob_pgp]);
encrypt(&[&bob_pgp],
&[&alice_fpr, &bob_fpr],
&[&alice_pgp, &bob_pgp]);
Ok(())
}
}