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:
parent
0e59f2f560
commit
8cf08e2470
2
NEWS
2
NEWS
@ -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
112
src/sq.rs
@ -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(),
|
||||
};
|
||||
|
@ -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",
|
||||
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user