miniquorum: initial commit
This commit is contained in:
parent
2ed1e64db8
commit
b5034586cc
File diff suppressed because it is too large
Load Diff
|
@ -8,6 +8,7 @@ members = [
|
||||||
"crates/builtins/icepick-internal",
|
"crates/builtins/icepick-internal",
|
||||||
"crates/by-chain/icepick-solana",
|
"crates/by-chain/icepick-solana",
|
||||||
"crates/by-chain/icepick-cosmos",
|
"crates/by-chain/icepick-cosmos",
|
||||||
|
"crates/miniquorum",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
[package]
|
||||||
|
name = "miniquorum"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["clap"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
bincode = "1.3.3"
|
||||||
|
card-backend-pcsc = "0.5.0"
|
||||||
|
clap = { version = "4.5.27", features = ["derive", "wrap_help"], optional = true }
|
||||||
|
keyfork-prompt = { version = "0.2.0", registry = "distrust", default-features = false }
|
||||||
|
openpgp-card = "0.4"
|
||||||
|
openpgp-card-sequoia = "0.2.2"
|
||||||
|
sequoia-openpgp = "1.22.0"
|
||||||
|
serde = { workspace = true, features = ["derive"] }
|
||||||
|
serde_json.workspace = true
|
||||||
|
sha3 = "0.10.8"
|
||||||
|
thiserror = "2.0.11"
|
|
@ -0,0 +1,475 @@
|
||||||
|
use keyfork_prompt::{
|
||||||
|
default_handler, prompt_validated_passphrase,
|
||||||
|
validators::{PinValidator, Validator},
|
||||||
|
};
|
||||||
|
use openpgp_card::{Error as CardError, StatusBytes};
|
||||||
|
use openpgp_card_sequoia::{state::Open, Card};
|
||||||
|
use sequoia_openpgp::{
|
||||||
|
self as openpgp,
|
||||||
|
armor::{Kind, Writer},
|
||||||
|
crypto::hash::Digest,
|
||||||
|
packet::{signature::SignatureBuilder, Packet},
|
||||||
|
parse::Parse,
|
||||||
|
serialize::Serialize as _,
|
||||||
|
types::SignatureType,
|
||||||
|
Cert, Fingerprint,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
|
use std::{collections::BTreeMap, fs::File, io::Read, path::Path};
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
/// An error with a [`PayloadVerification`] policy.
|
||||||
|
#[error("{error} (policy: {policy:?})")]
|
||||||
|
pub struct Error {
|
||||||
|
error: BaseError,
|
||||||
|
policy: PayloadVerification,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum BaseError {
|
||||||
|
/// In the given certificate keyring, the provided fingerprint was not found.
|
||||||
|
#[error("fingerprint not found: {0}")]
|
||||||
|
FingerprintNotFound(Fingerprint),
|
||||||
|
|
||||||
|
/// No smartcard was found.
|
||||||
|
#[error("no smartcard found")]
|
||||||
|
NoSmartcard,
|
||||||
|
|
||||||
|
/// None of the certificates in the given certificate keyring matched any plugged-in smartcard.
|
||||||
|
#[error("no certs found matching any available smartcard")]
|
||||||
|
NoCertMatchedSmartcard,
|
||||||
|
|
||||||
|
/// The certificate was not trusted by the root of trust.
|
||||||
|
#[error("untrusted certificate: {0} has not signed {1:?}")]
|
||||||
|
UntrustedCertificates(Fingerprint, Vec<Fingerprint>),
|
||||||
|
|
||||||
|
/// No certificate in the given certificate keyring matched the signature.
|
||||||
|
#[error("no public key matched signature")]
|
||||||
|
NoPublicKeyMatchedSignature,
|
||||||
|
|
||||||
|
/// Not enough signatures matched based on the given threshold
|
||||||
|
#[error("not enough signatures: {0} < {1}")]
|
||||||
|
NotEnoughSignatures(u8, u8),
|
||||||
|
|
||||||
|
/// A Payload was provided when an inner [`serde_json::Value`] was expected.
|
||||||
|
#[error("a payload was provided when a non-payload JSON value was expected")]
|
||||||
|
UnexpectedPayloadProvided,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BaseError {
|
||||||
|
fn with_policy(self, policy: &PayloadVerification) -> Error {
|
||||||
|
Error {
|
||||||
|
error: self,
|
||||||
|
policy: policy.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn canonicalize(value: Value) -> Value {
|
||||||
|
match value {
|
||||||
|
Value::Array(vec) => {
|
||||||
|
let values = vec.into_iter().map(canonicalize).collect();
|
||||||
|
Value::Array(values)
|
||||||
|
}
|
||||||
|
Value::Object(map) => {
|
||||||
|
// this sorts the values
|
||||||
|
let map: BTreeMap<String, Value> =
|
||||||
|
map.into_iter().map(|(k, v)| (k, canonicalize(v))).collect();
|
||||||
|
let sorted: Vec<Value> = map
|
||||||
|
.into_iter()
|
||||||
|
.map(|(k, v)| Value::Array(vec![Value::String(k), v]))
|
||||||
|
.collect();
|
||||||
|
Value::Array(sorted)
|
||||||
|
}
|
||||||
|
value => value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unhashed(value: Value) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
|
||||||
|
let value = canonicalize(value);
|
||||||
|
let bincoded = bincode::serialize(&value)?;
|
||||||
|
Ok(bincoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hash(value: Value) -> Result<Box<dyn Digest>, Box<dyn std::error::Error>> {
|
||||||
|
let value = canonicalize(value);
|
||||||
|
let bincoded = bincode::serialize(&value)?;
|
||||||
|
let mut digest = openpgp::types::HashAlgorithm::SHA512.context()?;
|
||||||
|
digest.update(&bincoded);
|
||||||
|
|
||||||
|
Ok(digest)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct Payload {
|
||||||
|
values: Value,
|
||||||
|
signatures: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct PayloadVerification {
|
||||||
|
threshold: u8,
|
||||||
|
error_on_invalid: bool,
|
||||||
|
error_on_missing_key: bool,
|
||||||
|
one_each: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::default::Default for PayloadVerification {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
threshold: 0,
|
||||||
|
error_on_invalid: true,
|
||||||
|
error_on_missing_key: true,
|
||||||
|
one_each: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
impl PayloadVerification {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Default::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_one_per_key(self, one_each: bool) -> Self {
|
||||||
|
Self { one_each, ..self }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_threshold(self, threshold: u8) -> Self {
|
||||||
|
Self { threshold, ..self }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_any_valid(self) -> Self {
|
||||||
|
Self {
|
||||||
|
threshold: 1,
|
||||||
|
error_on_invalid: false,
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_all_valid(self, threshold: u8) -> Self {
|
||||||
|
Self {
|
||||||
|
threshold,
|
||||||
|
error_on_invalid: true,
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ignoring_invalid_signatures(self) -> Self {
|
||||||
|
Self {
|
||||||
|
error_on_invalid: false,
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ignoring_missing_keys(self) -> Self {
|
||||||
|
Self {
|
||||||
|
error_on_missing_key: true,
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format a name from an OpenPGP card.
|
||||||
|
fn format_name(input: impl AsRef<str>) -> String {
|
||||||
|
let mut n = input
|
||||||
|
.as_ref()
|
||||||
|
.split("<<")
|
||||||
|
.take(2)
|
||||||
|
.map(|s| s.replace('<', " "))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
n.reverse();
|
||||||
|
n.join(" ")
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Payload {
|
||||||
|
/// Load a Payload and the relevant certificates.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// The constructor may error if either file can't be read or if either file has invalid data.
|
||||||
|
pub fn load(
|
||||||
|
payload_path: impl AsRef<Path>,
|
||||||
|
keyring_path: impl AsRef<Path>,
|
||||||
|
) -> Result<(Self, Vec<Cert>), Box<dyn std::error::Error>> {
|
||||||
|
let payload_file = File::open(payload_path)?;
|
||||||
|
let cert_file = File::open(keyring_path)?;
|
||||||
|
|
||||||
|
Self::from_readers(payload_file, cert_file)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_readers(
|
||||||
|
payload: impl Read,
|
||||||
|
keyring: impl Read + Send + Sync,
|
||||||
|
) -> Result<(Self, Vec<Cert>), Box<dyn std::error::Error>> {
|
||||||
|
let payload: Payload = serde_json::from_reader(payload)?;
|
||||||
|
let certs =
|
||||||
|
openpgp::cert::CertParser::from_reader(keyring)?.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
Ok((payload, certs))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an unsigned Payload from a [`serde_json::Value`].
|
||||||
|
pub fn new(value: serde_json::Value) -> Self {
|
||||||
|
Self {
|
||||||
|
values: value,
|
||||||
|
signatures: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attach a signature from an OpenPGP card.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// The method may error if a signature could not be created.
|
||||||
|
pub fn add_signature(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let unhashed = unhashed(self.values.clone())?;
|
||||||
|
let builder = SignatureBuilder::new(SignatureType::Binary);
|
||||||
|
let mut prompt_handler = default_handler()?;
|
||||||
|
let pin_validator = PinValidator {
|
||||||
|
min_length: Some(6),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
for backend in card_backend_pcsc::PcscBackend::cards(None)? {
|
||||||
|
let mut card = Card::<Open>::new(backend?)?;
|
||||||
|
let mut transaction = card.transaction()?;
|
||||||
|
|
||||||
|
let cardholder_name = format_name(transaction.cardholder_name()?);
|
||||||
|
let card_id = transaction.application_identifier()?.ident();
|
||||||
|
let mut pin = None;
|
||||||
|
|
||||||
|
while transaction.pw_status_bytes()?.err_count_pw1() > 0 && pin.is_none() {
|
||||||
|
transaction.reload_ard()?;
|
||||||
|
let attempts = transaction.pw_status_bytes()?.err_count_pw1();
|
||||||
|
let rpea = "Remaining PIN entry attempts";
|
||||||
|
let message = if cardholder_name.is_empty() {
|
||||||
|
format!("Unlock card {card_id}\n{rpea}: {attempts}\n\nPIN: ")
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"Unlock card {card_id} ({cardholder_name})\n{rpea}: {attempts}\n\nPIN: "
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let temp_pin = prompt_validated_passphrase(
|
||||||
|
&mut *prompt_handler,
|
||||||
|
&message,
|
||||||
|
3,
|
||||||
|
pin_validator.to_fn(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let verification_status = transaction.verify_user_pin(temp_pin.as_str().trim());
|
||||||
|
match verification_status {
|
||||||
|
#[allow(clippy::ignored_unit_patterns)]
|
||||||
|
Ok(_) => {
|
||||||
|
pin.replace(temp_pin);
|
||||||
|
}
|
||||||
|
// NOTE: This should not be hit, because of the above validator.
|
||||||
|
Err(CardError::CardStatus(
|
||||||
|
StatusBytes::IncorrectParametersCommandDataField,
|
||||||
|
)) => {
|
||||||
|
prompt_handler.prompt_message(keyfork_prompt::Message::Text(
|
||||||
|
"Invalid PIN length entered.".to_string(),
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
Err(_) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut signer_card = transaction.to_signing_card(pin.expect("valid PIN").as_str())?;
|
||||||
|
// NOTE: Can't use a PromptHandler to prompt a message as it doesn't provide a way to
|
||||||
|
// cancel a prompt when in terminal mode. Just eprintln to stderr.
|
||||||
|
//
|
||||||
|
// We shouldn't be writing with a PromptHandler, so the terminal should be reset.
|
||||||
|
let mut signer =
|
||||||
|
signer_card.signer(&|| eprintln!("Touch confirmation needed for signing"))?;
|
||||||
|
let signature = builder.clone().sign_message(&mut signer, &unhashed)?;
|
||||||
|
let signature = Packet::from(signature);
|
||||||
|
|
||||||
|
let mut armored_signature = vec![];
|
||||||
|
let mut writer = Writer::new(&mut armored_signature, Kind::Signature)?;
|
||||||
|
signature.serialize(&mut writer)?;
|
||||||
|
writer.finalize()?;
|
||||||
|
|
||||||
|
self.signatures.push(String::from_utf8(armored_signature)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify the keychain and certificates using either a Key ID or an OpenPGP card.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// The method may error if no certificate could be verified or if any singatures are invalid.
|
||||||
|
pub fn verify_signatures(
|
||||||
|
&self,
|
||||||
|
certs: &[Cert],
|
||||||
|
verification_policy: &PayloadVerification,
|
||||||
|
fingerprint: Option<Fingerprint>,
|
||||||
|
) -> Result<&serde_json::Value, Box<dyn std::error::Error>> {
|
||||||
|
let policy = openpgp::policy::StandardPolicy::new();
|
||||||
|
let validated_cert = find_matching_certificate(fingerprint, certs, &policy)?;
|
||||||
|
let (certs, invalid_certs) = validate_cross_signed_certs(&validated_cert, certs, &policy)?;
|
||||||
|
|
||||||
|
if !invalid_certs.is_empty() {
|
||||||
|
return Err(BaseError::UntrustedCertificates(
|
||||||
|
validated_cert.fingerprint(),
|
||||||
|
invalid_certs.iter().map(Cert::fingerprint).collect(),
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let hashed = hash(self.values.clone())?;
|
||||||
|
|
||||||
|
let PayloadVerification {
|
||||||
|
mut threshold,
|
||||||
|
error_on_invalid,
|
||||||
|
error_on_missing_key,
|
||||||
|
one_each,
|
||||||
|
} = *verification_policy;
|
||||||
|
let mut matches = 0;
|
||||||
|
|
||||||
|
if one_each {
|
||||||
|
threshold = certs.len() as u8;
|
||||||
|
}
|
||||||
|
|
||||||
|
for signature in &self.signatures {
|
||||||
|
let packet = Packet::from_bytes(signature.as_bytes())?;
|
||||||
|
let Packet::Signature(signature) = packet else {
|
||||||
|
panic!("bad packet found: {}", packet.tag());
|
||||||
|
};
|
||||||
|
let mut signature_matched = false;
|
||||||
|
for issuer in signature.get_issuers() {
|
||||||
|
for cert in &certs {
|
||||||
|
match cert
|
||||||
|
.with_policy(&policy, None)?
|
||||||
|
.keys()
|
||||||
|
.alive()
|
||||||
|
.for_signing()
|
||||||
|
.key_handle(issuer.clone())
|
||||||
|
.next()
|
||||||
|
.map(|signing_key| signature.verify_hash(&signing_key, hashed.clone()))
|
||||||
|
{
|
||||||
|
Some(Ok(())) => {
|
||||||
|
// key found, signature matched
|
||||||
|
signature_matched = true;
|
||||||
|
}
|
||||||
|
Some(Err(e)) => {
|
||||||
|
if error_on_invalid {
|
||||||
|
return Err(e)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// key not found, but we have more certs to go through
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if signature_matched {
|
||||||
|
matches += 1;
|
||||||
|
} else if error_on_missing_key {
|
||||||
|
return Err(
|
||||||
|
BaseError::NoPublicKeyMatchedSignature.with_policy(verification_policy)
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if matches < threshold {
|
||||||
|
return Err(
|
||||||
|
BaseError::NotEnoughSignatures(matches, threshold).with_policy(verification_policy)
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(&self.values)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_matching_certificate(
|
||||||
|
fingerprint: Option<Fingerprint>,
|
||||||
|
certs: &[Cert],
|
||||||
|
policy: &sequoia_openpgp::policy::StandardPolicy<'_>,
|
||||||
|
) -> Result<Cert, Box<dyn std::error::Error>> {
|
||||||
|
if let Some(fingerprint) = fingerprint {
|
||||||
|
Ok(certs
|
||||||
|
.iter()
|
||||||
|
.find(|cert| cert.fingerprint() == fingerprint)
|
||||||
|
.ok_or(BaseError::FingerprintNotFound(fingerprint))?
|
||||||
|
.clone())
|
||||||
|
} else {
|
||||||
|
let mut any_smartcard = false;
|
||||||
|
for backend in card_backend_pcsc::PcscBackend::cards(None)? {
|
||||||
|
any_smartcard = true;
|
||||||
|
let mut card = Card::<Open>::new(backend?)?;
|
||||||
|
let mut transaction = card.transaction()?;
|
||||||
|
let signing_fingerprint = transaction
|
||||||
|
.fingerprint(openpgp_card::KeyType::Signing)?
|
||||||
|
.expect("smartcard signing key is unavailable");
|
||||||
|
for cert in certs {
|
||||||
|
let valid_cert = cert.with_policy(policy, None)?;
|
||||||
|
for key in valid_cert.keys().alive().for_signing() {
|
||||||
|
let fpr = key.fingerprint();
|
||||||
|
if fpr.as_bytes() == signing_fingerprint.as_bytes() {
|
||||||
|
return Ok(cert.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if any_smartcard {
|
||||||
|
Err(BaseError::NoCertMatchedSmartcard.into())
|
||||||
|
} else {
|
||||||
|
Err(BaseError::NoSmartcard.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate that `certs` are signed by `validated_cert`, either by a signature directly upon the
|
||||||
|
/// primary key of that certificate, or a signature on a user ID of the certificate.
|
||||||
|
///
|
||||||
|
/// Returns a list of trusted certs and a list of untrusted certs.
|
||||||
|
fn validate_cross_signed_certs(
|
||||||
|
validated_cert: &Cert,
|
||||||
|
certs: &[Cert],
|
||||||
|
policy: &sequoia_openpgp::policy::StandardPolicy,
|
||||||
|
) -> Result<(Vec<Cert>, Vec<Cert>), Box<dyn std::error::Error>> {
|
||||||
|
let our_pkey = validated_cert.primary_key();
|
||||||
|
let mut verified_certs = vec![validated_cert.clone()];
|
||||||
|
let mut unverified_certs = vec![];
|
||||||
|
|
||||||
|
for cert in certs
|
||||||
|
.iter()
|
||||||
|
.filter(|cert| cert.fingerprint() != validated_cert.fingerprint())
|
||||||
|
{
|
||||||
|
let mut has_valid_userid_signature = false;
|
||||||
|
let cert_pkey = cert.primary_key();
|
||||||
|
|
||||||
|
// check signatures on User IDs
|
||||||
|
let userids = cert
|
||||||
|
.userids()
|
||||||
|
.map(|ua| (ua.certifications(), ua.userid().clone()));
|
||||||
|
for (signatures, userid) in userids {
|
||||||
|
for signature in signatures {
|
||||||
|
if signature
|
||||||
|
.verify_userid_binding(&our_pkey, &*cert_pkey, &userid)
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
has_valid_userid_signature = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check signatures on the primary key itself
|
||||||
|
let has_valid_direct_signature = cert_pkey
|
||||||
|
.active_certifications_by_key(policy, None, &***our_pkey.role_as_unspecified())
|
||||||
|
.next()
|
||||||
|
.is_some();
|
||||||
|
|
||||||
|
if has_valid_userid_signature || has_valid_direct_signature {
|
||||||
|
verified_certs.push(cert.clone());
|
||||||
|
} else {
|
||||||
|
unverified_certs.push(cert.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((verified_certs, unverified_certs))
|
||||||
|
}
|
|
@ -0,0 +1,118 @@
|
||||||
|
use clap::Parser;
|
||||||
|
use miniquorum::{Payload, PayloadVerification};
|
||||||
|
use sequoia_openpgp::Fingerprint;
|
||||||
|
use std::{fs::File, path::PathBuf};
|
||||||
|
|
||||||
|
#[derive(clap::Parser)]
|
||||||
|
/// An Icepick-specific subset of the Quorum decision-making system.
|
||||||
|
enum MiniQuorum {
|
||||||
|
/// Verify signatures on an Icepick Payload file.
|
||||||
|
VerifySignatures {
|
||||||
|
/// The file containing OpenPGP Certificates used for verifying signatures.
|
||||||
|
keyring_file: PathBuf,
|
||||||
|
|
||||||
|
/// The file provided as input.
|
||||||
|
///
|
||||||
|
/// If no file is passed, standard input is used.
|
||||||
|
input_file: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// An OpenPGP Fingerprint to use in place of on-smartcard certificate detection.
|
||||||
|
///
|
||||||
|
/// This functionality is only recommended if verifying a payload without the physical
|
||||||
|
/// presence of any signer, and builds a web of trust from the signer fingerprint provided.
|
||||||
|
#[arg(long)]
|
||||||
|
fingerprint: Option<Fingerprint>,
|
||||||
|
|
||||||
|
/// The file to write the resulting payload to, if verification is successful.
|
||||||
|
#[arg(long)]
|
||||||
|
output_file: Option<PathBuf>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Add a signature to an Icepick Payload file.
|
||||||
|
AddSignature {
|
||||||
|
/// The file to use as input.
|
||||||
|
///
|
||||||
|
/// If no file is provided, standard input of a payload value is used. If a file is
|
||||||
|
/// provided and no output file is provided, it will be used in-place as the output file
|
||||||
|
/// with the additional signature added.
|
||||||
|
input_file: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// The file to use as output.
|
||||||
|
///
|
||||||
|
/// If no file is provided, but an input file is provided, the input file is used. If no
|
||||||
|
/// input file is provided, standard output is used.
|
||||||
|
#[arg(long)]
|
||||||
|
output_file: Option<PathBuf>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
match MiniQuorum::parse() {
|
||||||
|
MiniQuorum::VerifySignatures {
|
||||||
|
keyring_file,
|
||||||
|
input_file,
|
||||||
|
fingerprint,
|
||||||
|
output_file,
|
||||||
|
} => {
|
||||||
|
assert_ne!(
|
||||||
|
input_file, output_file,
|
||||||
|
"output is verified data; not overwriting signed input data"
|
||||||
|
);
|
||||||
|
let (payload, certs) = match input_file {
|
||||||
|
Some(input_file) => Payload::load(&input_file, &keyring_file)?,
|
||||||
|
None => {
|
||||||
|
let stdin = std::io::stdin();
|
||||||
|
let keyring_file = File::open(&keyring_file)?;
|
||||||
|
Payload::from_readers(stdin, keyring_file)?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let policy = PayloadVerification::new().with_threshold(certs.len().try_into()?);
|
||||||
|
let inner_value = payload.verify_signatures(&certs, &policy, fingerprint)?;
|
||||||
|
if let Some(output_file) = output_file {
|
||||||
|
let file = File::create(output_file)?;
|
||||||
|
serde_json::to_writer_pretty(file, inner_value)?;
|
||||||
|
} else {
|
||||||
|
let stdout = std::io::stdout();
|
||||||
|
serde_json::to_writer_pretty(stdout, inner_value)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MiniQuorum::AddSignature {
|
||||||
|
input_file,
|
||||||
|
output_file,
|
||||||
|
} => {
|
||||||
|
let mut payload = match &input_file {
|
||||||
|
Some(input_file) => {
|
||||||
|
let input_file = File::open(input_file)?;
|
||||||
|
let payload: Payload = serde_json::from_reader(input_file)?;
|
||||||
|
payload
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let stdin = std::io::stdin();
|
||||||
|
let value: serde_json::Value = serde_json::from_reader(stdin)?;
|
||||||
|
Payload::new(value)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
payload.add_signature()?;
|
||||||
|
|
||||||
|
if let Some(output_file) = output_file {
|
||||||
|
// write to output
|
||||||
|
let file = File::create(output_file)?;
|
||||||
|
serde_json::to_writer_pretty(file, &payload)?;
|
||||||
|
} else if let Some(input_file) = input_file {
|
||||||
|
// write to tempfile, move to input_file
|
||||||
|
let output_file = input_file.with_extension("tmp");
|
||||||
|
let mut file = File::create_new(&output_file)?;
|
||||||
|
serde_json::to_writer_pretty(&mut file, &payload)?;
|
||||||
|
drop(file);
|
||||||
|
std::fs::copy(&output_file, input_file)?;
|
||||||
|
std::fs::remove_file(output_file)?;
|
||||||
|
} else {
|
||||||
|
// write to standard output?
|
||||||
|
println!("{}", serde_json::to_string_pretty(&payload)?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
Loading…
Reference in New Issue