keyfork-shard: add shard_and_encrypt

This commit is contained in:
Ryan Heywood 2024-02-15 03:01:23 -05:00
parent 3b5c1340db
commit 2541d49fb8
Signed by: ryan
GPG Key ID: 8E401478A3FBEF72
5 changed files with 165 additions and 115 deletions

View File

@ -2,14 +2,12 @@
use std::{env, path::PathBuf, process::ExitCode, str::FromStr};
use keyfork_shard::openpgp::{discover_certs, openpgp::Cert, split};
use keyfork_shard::{Format, openpgp::OpenPGP};
#[derive(Clone, Debug)]
enum Error {
Usage(String),
Input,
Threshold(u8, u8),
InvalidCertCount(usize, u8),
}
impl std::fmt::Display for Error {
@ -19,15 +17,6 @@ impl std::fmt::Display for Error {
write!(f, "Usage: {program_name} threshold max key_discovery")
}
Error::Input => f.write_str("Expected hex encoded input"),
Error::Threshold(threshold, max) => {
write!(
f,
"Invalid threshold: 0 < threshold {threshold} <= max {max} < 256"
)
}
Error::InvalidCertCount(count, max) => {
write!(f, "Invalid cert count: count {count} != max {max}")
}
}
}
}
@ -36,31 +25,20 @@ impl std::error::Error for Error {}
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
fn validate(threshold: &str, max: &str, key_discovery: &str) -> Result<(u8, Vec<Cert>)> {
fn validate(threshold: &str, max: &str, key_discovery: &str) -> Result<(u8, u8, PathBuf)> {
let threshold = u8::from_str(threshold)?;
let max = u8::from_str(max)?;
let key_discovery = PathBuf::from(key_discovery);
if threshold > max {
return Err(Error::Threshold(threshold, max).into());
}
// Verify path exists
std::fs::metadata(&key_discovery)?;
// Load certs from path
let certs = discover_certs(key_discovery)?;
if certs.len() != max.into() {
return Err(Error::InvalidCertCount(certs.len(), max).into());
}
Ok((threshold, certs))
Ok((threshold, max, key_discovery))
}
fn run() -> Result<()> {
let mut args = env::args();
let program_name = args.next().expect("program name");
let args = args.collect::<Vec<_>>();
let (threshold, cert_list) = match args.as_slice() {
let (threshold, max, key_discovery) = match args.as_slice() {
[threshold, max, key_discovery] => validate(threshold, max, key_discovery)?,
_ => return Err(Error::Usage(program_name).into()),
};
@ -72,8 +50,9 @@ fn run() -> Result<()> {
smex::decode(line?)?
};
split(threshold, cert_list, &input, std::io::stdout())?;
let openpgp = OpenPGP;
openpgp.shard_and_encrypt(threshold, max, &input, key_discovery, std::io::stdout())?;
Ok(())
}

View File

@ -30,9 +30,6 @@ pub trait Format {
/// The error type returned from any failed operations.
type Error: std::error::Error + 'static;
/// A type encapsulating the public key recipients of shards.
type PublicKeyData: IntoIterator<Item = Self::PublicKey>;
/// A type encapsulating a single public key recipient.
type PublicKey;
@ -43,7 +40,7 @@ pub trait Format {
type SigningKey;
/// A type representing the parsed, but encrypted, Shard data.
type ShardData;
type EncryptedData;
/// Parse the public key data from a readable type.
///
@ -54,7 +51,7 @@ pub trait Format {
fn parse_public_key_data(
&self,
key_data_path: impl AsRef<Path>,
) -> Result<Self::PublicKeyData, Self::Error>;
) -> Result<Vec<Self::PublicKey>, Self::Error>;
/// Derive a signer
fn derive_signing_key(&self, seed: &[u8]) -> Self::SigningKey;
@ -70,9 +67,9 @@ pub trait Format {
fn format_encrypted_header(
&self,
signing_key: &Self::SigningKey,
key_data: &Self::PublicKeyData,
key_data: &[Self::PublicKey],
threshold: u8,
) -> Result<Vec<u8>, Self::Error>;
) -> Result<Self::EncryptedData, Self::Error>;
/// Format a shard encrypted to the given public key, signing with the private key.
///
@ -84,7 +81,7 @@ pub trait Format {
shard: &[u8],
public_key: &Self::PublicKey,
signing_key: &mut Self::SigningKey,
) -> Result<Vec<u8>, Self::Error>;
) -> Result<Self::EncryptedData, Self::Error>;
/// Parse the private key data from a readable type. The private key may not be accessible (it
/// may be hardware only, such as a smartcard), for which this method may return None.
@ -105,7 +102,7 @@ pub trait Format {
fn parse_shard_file(
&self,
shard_file: impl Read + Send + Sync,
) -> Result<Self::ShardData, Self::Error>;
) -> Result<Vec<Self::EncryptedData>, Self::Error>;
/// Write the Shard data to a Shard file.
///
@ -114,8 +111,8 @@ pub trait Format {
/// Shard file could not be written to.
fn format_shard_file(
&self,
shard_data: Self::ShardData,
shard_file: impl Write,
encrypted_data: &[Self::EncryptedData],
shard_file: impl Write + Send + Sync,
) -> Result<(), Self::Error>;
/// Decrypt shares and associated metadata from a readable input. For the current version of
@ -128,7 +125,7 @@ pub trait Format {
fn decrypt_all_shards(
&self,
private_keys: Option<Self::PrivateKeyData>,
shard_data: Self::ShardData,
encrypted_messages: &[Self::EncryptedData],
) -> Result<(Vec<Share>, u8), Self::Error>;
/// Decrypt a single share and associated metadata from a reaable input. For the current
@ -141,7 +138,7 @@ pub trait Format {
fn decrypt_one_shard(
&self,
private_keys: Option<Self::PrivateKeyData>,
shard_data: Self::ShardData,
encrypted_data: &[Self::EncryptedData],
) -> Result<(Share, u8), Self::Error>;
/// Decrypt multiple shares and combine them to recreate a secret.
@ -157,8 +154,8 @@ pub trait Format {
let private_keys = private_key_data_path
.map(|p| self.parse_private_key_data(p))
.transpose()?;
let shard_data = self.parse_shard_file(reader)?;
let (shares, threshold) = self.decrypt_all_shards(private_keys, shard_data)?;
let encrypted_messages = self.parse_shard_file(reader)?;
let (shares, threshold) = self.decrypt_all_shards(private_keys, &encrypted_messages)?;
let secret = Sharks(threshold)
.recover(&shares)
@ -186,7 +183,7 @@ pub trait Format {
let private_keys = private_key_data_path
.map(|p| self.parse_private_key_data(p))
.transpose()?;
let shard_data = self.parse_shard_file(reader)?;
let encrypted_messages = self.parse_shard_file(reader)?;
// establish AES-256-GCM key via ECDH
let mut nonce_data: Option<[u8; 12]> = None;
@ -246,7 +243,7 @@ pub trait Format {
let shared_key = Aes256Gcm::new_from_slice(&hkdf_output)?;
// decrypt a single shard and create the payload
let (share, threshold) = self.decrypt_one_shard(private_keys, shard_data)?;
let (share, threshold) = self.decrypt_one_shard(private_keys, &encrypted_messages)?;
let mut payload = Vec::from(&share);
payload.insert(0, HUNK_VERSION);
payload.insert(1, threshold);
@ -313,6 +310,49 @@ pub trait Format {
Ok(())
}
/// Split a secret into a shard for every shard in keys, with the given Shamir's Secret Sharing
/// threshold.
///
/// # Errors
/// The method may return an error if the shares can't be encrypted.
fn shard_and_encrypt(
&self,
threshold: u8,
max: u8,
secret: &[u8],
public_key_data_path: impl AsRef<Path>,
writer: impl Write + Send + Sync,
) -> Result<(), Box<dyn std::error::Error>> {
let mut signing_key = self.derive_signing_key(secret);
let sharks = Sharks(threshold);
let dealer = sharks.dealer(secret);
let public_keys = self.parse_public_key_data(public_key_data_path)?;
assert!(
public_keys.len() < u8::MAX as usize,
"must have less than u8::MAX public keys"
);
assert_eq!(
max,
public_keys.len() as u8,
"max must be equal to amount of public keys"
);
let max = public_keys.len() as u8;
assert!(max >= threshold, "threshold must not exceed max keys");
let header = self.format_encrypted_header(&signing_key, &public_keys, threshold)?;
let mut messages = vec![header];
for (pk, share) in public_keys.iter().zip(dealer) {
let shard = Vec::from(&share);
messages.push(self.encrypt_shard(&shard, pk, &mut signing_key)?);
}
self.format_shard_file(&messages, writer)?;
Ok(())
}
}
/// Errors encountered while creating or combining shares using Shamir's Secret Sharing.

View File

@ -163,15 +163,52 @@ impl EncryptedMessage {
}
}
/// Parse OpenPGP packets for encrypted messages.
pub fn from_reader(input: impl Read + Send + Sync) -> openpgp::Result<Vec<Self>> {
let mut pkesks = Vec::new();
let mut encrypted_messages = vec![];
for packet in PacketPile::from_reader(input)
.map_err(Error::Sequoia)?
.into_children()
{
match packet {
Packet::PKESK(p) => pkesks.push(p),
Packet::SEIP(s) => {
encrypted_messages.push(EncryptedMessage::new(&mut pkesks, s));
}
s => {
panic!("Invalid variant found: {}", s.tag());
}
}
}
Ok(encrypted_messages)
}
/// Serialize all contents of the message to a writer.
///
/// # Errors
/// The function may error for any condition in Sequoia's Serialize trait.
pub fn serialize(&self, o: &mut dyn std::io::Write) -> openpgp::Result<()> {
fn serialize(&self, mut o: impl std::io::Write + Send + Sync) -> openpgp::Result<()> {
for pkesk in &self.pkesks {
pkesk.serialize(o)?;
let mut packet = vec![];
pkesk.serialize(&mut packet).map_err(Error::Sequoia)?;
let message = Message::new(&mut o);
let mut message = ArbitraryWriter::new(message, Tag::PKESK).map_err(Error::Sequoia)?;
message.write_all(&packet).map_err(Error::SequoiaIo)?;
message.finalize().map_err(Error::Sequoia)?;
}
self.message.serialize(o)?;
let mut packet = vec![];
self.message
.serialize(&mut packet)
.map_err(Error::Sequoia)?;
let message = Message::new(&mut o);
let mut message = ArbitraryWriter::new(message, Tag::SEIP).map_err(Error::Sequoia)?;
message.write_all(&packet).map_err(Error::SequoiaIo)?;
message.finalize().map_err(Error::Sequoia)?;
Ok(())
}
@ -188,23 +225,8 @@ impl EncryptedMessage {
H: VerificationHelper + DecryptionHelper,
{
let mut packets = vec![];
for pkesk in &self.pkesks {
let mut packet = vec![];
pkesk.serialize(&mut packet).map_err(Error::Sequoia)?;
let message = Message::new(&mut packets);
let mut message = ArbitraryWriter::new(message, Tag::PKESK).map_err(Error::Sequoia)?;
message.write_all(&packet).map_err(Error::SequoiaIo)?;
message.finalize().map_err(Error::Sequoia)?;
}
let mut packet = vec![];
self.message
.serialize(&mut packet)
self.serialize(&mut packets)
.map_err(Error::Sequoia)?;
let message = Message::new(&mut packets);
let mut message = ArbitraryWriter::new(message, Tag::SEIP).map_err(Error::Sequoia)?;
message.write_all(&packet).map_err(Error::SequoiaIo)?;
message.finalize().map_err(Error::Sequoia)?;
let mut decryptor = DecryptorBuilder::from_bytes(&packets)
.map_err(Error::Sequoia)?
@ -256,15 +278,14 @@ impl OpenPGP {
impl Format for OpenPGP {
type Error = Error;
type PublicKey = Cert;
type PublicKeyData = Vec<Cert>;
type PrivateKeyData = Vec<Cert>;
type SigningKey = Cert;
type ShardData = Vec<EncryptedMessage>;
type EncryptedData = EncryptedMessage;
fn parse_public_key_data(
&self,
key_data_path: impl AsRef<Path>,
) -> std::result::Result<Self::PublicKeyData, Self::Error> {
) -> std::result::Result<Vec<Self::PublicKey>, Self::Error> {
Self::discover_certs(key_data_path)
}
@ -288,9 +309,9 @@ impl Format for OpenPGP {
fn format_encrypted_header(
&self,
signing_key: &Self::SigningKey,
key_data: &Self::PublicKeyData,
key_data: &[Self::PublicKey],
threshold: u8,
) -> Result<Vec<u8>, Self::Error> {
) -> Result<Self::EncryptedData, Self::Error> {
let policy = StandardPolicy::new();
let mut pp = vec![SHARD_METADATA_VERSION, threshold];
// Note: Sequoia does not export private keys on a Cert, only on a TSK
@ -348,7 +369,22 @@ impl Format for OpenPGP {
literal_message.write_all(&pp).map_err(Error::SequoiaIo)?;
literal_message.finalize().map_err(Error::Sequoia)?;
Ok(output)
// Parse it into an EncryptedMessage. Yes, this takes a serialized message
// and deserializes it. Don't think about it too hard. It's easier this way.
let mut pkesks = vec![];
for packet in PacketPile::from_reader(output.as_slice())
.map_err(Error::Sequoia)?
.into_children()
{
match packet {
Packet::PKESK(p) => pkesks.push(p),
Packet::SEIP(s) => return Ok(EncryptedMessage::new(&mut pkesks, s)),
s => panic!("Invalid variant found: {}", s.tag()),
}
}
panic!("Unable to build EncryptedMessage from PacketPile");
}
fn encrypt_shard(
@ -356,7 +392,7 @@ impl Format for OpenPGP {
shard: &[u8],
public_key: &Cert,
signing_key: &mut Self::SigningKey,
) -> Result<Vec<u8>> {
) -> Result<EncryptedMessage> {
let policy = StandardPolicy::new();
let valid_cert = public_key
.with_policy(&policy, None)
@ -404,7 +440,13 @@ impl Format for OpenPGP {
message.write_all(shard).map_err(Error::SequoiaIo)?;
message.finalize().map_err(Error::Sequoia)?;
Ok(message_output)
let message = EncryptedMessage::from_reader(message_output.as_slice())
.map_err(Error::Sequoia)?
.into_iter()
.next()
.expect("serialized message should be parseable");
Ok(message)
}
fn parse_private_key_data(
@ -417,35 +459,17 @@ impl Format for OpenPGP {
fn parse_shard_file(
&self,
shard_file: impl Read + Send + Sync,
) -> Result<Self::ShardData, Self::Error> {
let mut pkesks = Vec::new();
let mut encrypted_messages = vec![];
for packet in PacketPile::from_reader(shard_file)
.map_err(Error::Sequoia)?
.into_children()
{
match packet {
Packet::PKESK(p) => pkesks.push(p),
Packet::SEIP(s) => {
encrypted_messages.push(EncryptedMessage::new(&mut pkesks, s));
}
s => {
panic!("Invalid variant found: {}", s.tag());
}
}
}
Ok(encrypted_messages)
) -> Result<Vec<Self::EncryptedData>, Self::Error> {
EncryptedMessage::from_reader(shard_file).map_err(Error::Sequoia)
}
fn format_shard_file(
&self,
shard_data: Self::ShardData,
shard_file: impl Write,
encrypted_data: &[Self::EncryptedData],
shard_file: impl Write + Send + Sync,
) -> Result<(), Self::Error> {
let mut writer = Writer::new(shard_file, Kind::Message).map_err(Error::SequoiaIo)?;
for message in shard_data {
for message in encrypted_data {
message.serialize(&mut writer).map_err(Error::Sequoia)?;
}
writer.finalize().map_err(Error::SequoiaIo)?;
@ -455,7 +479,7 @@ impl Format for OpenPGP {
fn decrypt_all_shards(
&self,
private_keys: Option<Self::PrivateKeyData>,
mut shard_data: Self::ShardData,
encrypted_data: &[Self::EncryptedData],
) -> std::result::Result<(Vec<Share>, u8), Self::Error> {
// Be as liberal as possible when decrypting.
// We don't want to invalidate someone's keys just because the old sig expired.
@ -463,8 +487,10 @@ impl Format for OpenPGP {
let mut keyring = Keyring::new(private_keys.unwrap_or_default())?;
let mut manager = SmartcardManager::new()?;
let metadata = shard_data.remove(0);
let metadata_content = decrypt_metadata(&metadata, &policy, &mut keyring, &mut manager)?;
let mut encrypted_messages = encrypted_data.iter();
let metadata = encrypted_messages.next().expect("metdata");
let metadata_content = decrypt_metadata(metadata, &policy, &mut keyring, &mut manager)?;
let (threshold, root_cert, certs) = decode_metadata_v1(&metadata_content)?;
@ -475,8 +501,11 @@ impl Format for OpenPGP {
// because we control the order packets are encrypted and certificates are stored.
// TODO: remove alloc, convert EncryptedMessage to &EncryptedMessage
let mut messages: HashMap<KeyID, EncryptedMessage> =
certs.iter().map(Cert::keyid).zip(shard_data).collect();
let mut messages: HashMap<KeyID, EncryptedMessage> = certs
.iter()
.map(Cert::keyid)
.zip(encrypted_messages.cloned())
.collect();
let mut decrypted_messages =
decrypt_with_keyring(&mut messages, &certs, &policy, &mut keyring)?;
@ -507,21 +536,26 @@ impl Format for OpenPGP {
fn decrypt_one_shard(
&self,
private_keys: Option<Self::PrivateKeyData>,
mut shard_data: Self::ShardData,
encrypted_data: &[Self::EncryptedData],
) -> std::result::Result<(Share, u8), Self::Error> {
let policy = NullPolicy::new();
let mut keyring = Keyring::new(private_keys.unwrap_or_default())?;
let mut manager = SmartcardManager::new()?;
let metadata = shard_data.remove(0);
let metadata_content = decrypt_metadata(&metadata, &policy, &mut keyring, &mut manager)?;
let mut encrypted_messages = encrypted_data.iter();
let metadata = encrypted_messages.next().expect("metadata");
let metadata_content = decrypt_metadata(metadata, &policy, &mut keyring, &mut manager)?;
let (threshold, root_cert, certs) = decode_metadata_v1(&metadata_content)?;
keyring.set_root_cert(root_cert.clone());
manager.set_root_cert(root_cert.clone());
let mut messages: HashMap<KeyID, EncryptedMessage> =
certs.iter().map(Cert::keyid).zip(shard_data).collect();
let mut messages: HashMap<KeyID, EncryptedMessage> = certs
.iter()
.map(Cert::keyid)
.zip(encrypted_messages.cloned())
.collect();
let decrypted_messages =
decrypt_with_keyring(&mut messages, &certs, &policy, &mut keyring)?;
@ -936,6 +970,7 @@ pub fn decrypt(
/// # Errors
/// The function may return an error if an error occurs while decrypting shards, parsing shards, or
/// combining the shards into a secret.
#[deprecated]
pub fn combine(
certs: Vec<Cert>,
metadata: &EncryptedMessage,
@ -1023,6 +1058,7 @@ pub fn combine(
///
/// The function may panic if the metadata can't properly store the certificates used to generate
/// the encrypted shares.
#[deprecated]
pub fn split(threshold: u8, certs: Vec<Cert>, secret: &[u8], output: impl Write) -> Result<()> {
let seed = VariableLengthSeed::new(secret);
// build cert to sign encrypted shares

View File

@ -34,7 +34,7 @@ trait ShardExec {
max: u8,
key_discovery: impl AsRef<Path>,
secret: &[u8],
output: &mut impl Write,
output: &mut (impl Write + Send + Sync),
) -> Result<(), Box<dyn std::error::Error>>;
fn combine<T>(
@ -65,17 +65,10 @@ impl ShardExec for OpenPGP {
max: u8,
key_discovery: impl AsRef<Path>,
secret: &[u8],
output: &mut impl Write,
output: &mut (impl Write + Send + Sync),
) -> Result<(), Box<dyn std::error::Error>> {
// Get certs and input
let certs = keyfork_shard::openpgp::discover_certs(key_discovery.as_ref())?;
assert_eq!(
certs.len(),
max.into(),
"cert count {} != max {max}",
certs.len()
);
keyfork_shard::openpgp::split(threshold, certs, secret, output).map_err(Into::into)
let opgp = keyfork_shard::openpgp::OpenPGP;
opgp.shard_and_encrypt(threshold, max, secret, key_discovery, output)
}
fn combine<T>(

View File

@ -165,8 +165,10 @@ fn generate_shard_secret(
if let Some(output_file) = output_file {
let output = File::create(output_file)?;
#[allow(deprecated)]
keyfork_shard::openpgp::split(threshold, certs, &seed, output)?;
} else {
#[allow(deprecated)]
keyfork_shard::openpgp::split(threshold, certs, &seed, std::io::stdout())?;
}
Ok(())