475 lines
17 KiB
Rust
475 lines
17 KiB
Rust
// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
|
|
|
//! Odds and ends, will most likely be restructured.
|
|
|
|
use std::convert::TryFrom;
|
|
use std::convert::TryInto;
|
|
use std::io;
|
|
use std::time::SystemTime;
|
|
|
|
use anyhow::{Context, Result};
|
|
|
|
use openpgp::armor;
|
|
use openpgp::cert::amalgamation::key::ValidErasedKeyAmalgamation;
|
|
use openpgp::crypto::mpi;
|
|
use openpgp::packet::{
|
|
key::{Key4, KeyRole, PrimaryRole, SecretParts, SubordinateRole},
|
|
signature::SignatureBuilder,
|
|
Key, UserID,
|
|
};
|
|
use openpgp::parse::{stream::DecryptorBuilder, Parse};
|
|
use openpgp::policy::Policy;
|
|
use openpgp::serialize::stream::{Message, Signer};
|
|
use openpgp::types::{KeyFlags, PublicKeyAlgorithm, SignatureType, Timestamp};
|
|
use openpgp::{Cert, Packet};
|
|
use sequoia_openpgp as openpgp;
|
|
use sequoia_openpgp::types::{HashAlgorithm, SymmetricAlgorithm};
|
|
|
|
use openpgp_card::algorithm::{Algo, Curve};
|
|
use openpgp_card::card_do::{Fingerprint, KeyGenerationTime};
|
|
use openpgp_card::crypto_data::{CardUploadableKey, PublicKeyMaterial};
|
|
use openpgp_card::{Error, KeyType, OpenPgpTransaction};
|
|
|
|
use crate::card::Open;
|
|
use crate::privkey::SequoiaKey;
|
|
use crate::{decryptor, signer, PublicKey};
|
|
|
|
/// Create a Cert from the three subkeys on a card.
|
|
/// (Calling this multiple times will result in different Certs!)
|
|
///
|
|
/// When pw1 is None, attempt to verify via pinpad.
|
|
///
|
|
/// `prompt` notifies the user when a pinpad needs the user pin as input.
|
|
///
|
|
/// FIXME: accept optional metadata for user_id(s)?
|
|
pub fn make_cert<'app>(
|
|
open: &mut Open<'app>,
|
|
key_sig: PublicKey,
|
|
key_dec: Option<PublicKey>,
|
|
key_aut: Option<PublicKey>,
|
|
pw1: Option<&[u8]>,
|
|
pinpad_prompt: &dyn Fn(),
|
|
touch_prompt: &(dyn Fn() + Send + Sync),
|
|
) -> Result<Cert> {
|
|
let mut pp = vec![];
|
|
|
|
// 1) use the signing key as primary key
|
|
let pri = PrimaryRole::convert_key(key_sig.clone());
|
|
pp.push(Packet::from(pri));
|
|
|
|
if let Some(key_dec) = key_dec {
|
|
// 2) add decryption key as subkey
|
|
let sub_dec = SubordinateRole::convert_key(key_dec);
|
|
pp.push(Packet::from(sub_dec.clone()));
|
|
|
|
// Temporary version of the cert
|
|
let cert = Cert::try_from(pp.clone())?;
|
|
|
|
// 3) make binding, sign with card -> add
|
|
{
|
|
let signing_builder = SignatureBuilder::new(SignatureType::SubkeyBinding)
|
|
.set_signature_creation_time(SystemTime::now())?
|
|
.set_key_validity_period(std::time::Duration::new(0, 0))?
|
|
.set_key_flags(
|
|
KeyFlags::empty()
|
|
.set_storage_encryption()
|
|
.set_transport_encryption(),
|
|
)?;
|
|
|
|
// Allow signing on the card
|
|
if let Some(pw1) = pw1 {
|
|
open.verify_user_for_signing(pw1)?;
|
|
} else {
|
|
open.verify_user_for_signing_pinpad(pinpad_prompt)?;
|
|
}
|
|
if let Some(mut sign) = open.signing_card() {
|
|
// Card-backed signer for bindings
|
|
let mut card_signer = sign.signer_from_pubkey(key_sig.clone(), touch_prompt);
|
|
|
|
let signing_bsig: Packet = sub_dec
|
|
.bind(&mut card_signer, &cert, signing_builder)?
|
|
.into();
|
|
|
|
pp.push(signing_bsig);
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(key_aut) = key_aut {
|
|
// 4) add auth subkey
|
|
let sub_aut = SubordinateRole::convert_key(key_aut);
|
|
pp.push(Packet::from(sub_aut.clone()));
|
|
|
|
// 5) make, sign binding -> add
|
|
{
|
|
let signing_builder = SignatureBuilder::new(SignatureType::SubkeyBinding)
|
|
.set_signature_creation_time(SystemTime::now())?
|
|
.set_key_validity_period(std::time::Duration::new(0, 0))?
|
|
.set_key_flags(KeyFlags::empty().set_authentication())?;
|
|
|
|
// Allow signing on the card
|
|
if let Some(pw1) = pw1 {
|
|
open.verify_user_for_signing(pw1)?;
|
|
} else {
|
|
open.verify_user_for_signing_pinpad(pinpad_prompt)?;
|
|
}
|
|
if let Some(mut sign) = open.signing_card() {
|
|
// Card-backed signer for bindings
|
|
let mut card_signer = sign.signer_from_pubkey(key_sig.clone(), touch_prompt);
|
|
|
|
// Temporary version of the cert
|
|
let cert = Cert::try_from(pp.clone())?;
|
|
|
|
let signing_bsig: Packet = sub_aut
|
|
.bind(&mut card_signer, &cert, signing_builder)?
|
|
.into();
|
|
|
|
pp.push(signing_bsig);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 6) add user id from cardholder name (if a name is set)
|
|
let cardholder = open.cardholder_related_data()?;
|
|
|
|
// FIXME: accept user id/email as argument?!
|
|
|
|
if let Some(name) = cardholder.name() {
|
|
let uid: UserID = name.into();
|
|
|
|
pp.push(uid.clone().into());
|
|
|
|
// 7) make, sign binding -> add
|
|
{
|
|
let signing_builder = SignatureBuilder::new(SignatureType::PositiveCertification)
|
|
.set_signature_creation_time(SystemTime::now())?
|
|
.set_key_validity_period(std::time::Duration::new(0, 0))?
|
|
.set_key_flags(
|
|
// Flags for primary key
|
|
KeyFlags::empty().set_signing().set_certification(),
|
|
)?;
|
|
|
|
// Allow signing on the card
|
|
if let Some(pw1) = pw1 {
|
|
open.verify_user_for_signing(pw1)?;
|
|
} else {
|
|
open.verify_user_for_signing_pinpad(pinpad_prompt)?;
|
|
}
|
|
|
|
if let Some(mut sign) = open.signing_card() {
|
|
// Card-backed signer for bindings
|
|
let mut card_signer = sign.signer_from_pubkey(key_sig, touch_prompt);
|
|
|
|
// Temporary version of the cert
|
|
let cert = Cert::try_from(pp.clone())?;
|
|
|
|
let signing_bsig: Packet =
|
|
uid.bind(&mut card_signer, &cert, signing_builder)?.into();
|
|
|
|
pp.push(signing_bsig);
|
|
}
|
|
}
|
|
}
|
|
|
|
Cert::try_from(pp)
|
|
}
|
|
|
|
/// Meta-Helper fn: get a Sequoia PublicKey from an openpgp-card PublicKeyMaterial, timestamp and
|
|
/// card-Fingerprint.
|
|
///
|
|
/// For ECC decryption keys, possible values for the parameters `hash` and `sym` will be tested.
|
|
/// Once a key with matching fingerprint is found in this way, it is considered the correct key,
|
|
/// and returned.
|
|
///
|
|
/// The Fingerprint of the retrieved PublicKey is always validated against the `Fingerprint` as
|
|
/// stored on the card. If the fingerprints doesn't match, an Error is returned.
|
|
pub fn public_key_material_and_fp_to_key(
|
|
pkm: &PublicKeyMaterial,
|
|
key_type: KeyType,
|
|
time: &KeyGenerationTime,
|
|
fingerprint: &Fingerprint,
|
|
) -> Result<PublicKey, Error> {
|
|
// Possible hash/sym parameters based on statistics over 2019-12 SKS dump:
|
|
// https://gitlab.com/sequoia-pgp/sequoia/-/issues/838#note_909813463
|
|
|
|
// We try these parameters in descending order of occurrence and return the PublicKey
|
|
// once the Fingerprint matches.
|
|
|
|
let param: &[_] = match (pkm, key_type) {
|
|
(PublicKeyMaterial::E(_), KeyType::Decryption) => &[
|
|
(
|
|
Some(HashAlgorithm::SHA256),
|
|
Some(SymmetricAlgorithm::AES128),
|
|
),
|
|
(
|
|
Some(HashAlgorithm::SHA512),
|
|
Some(SymmetricAlgorithm::AES256),
|
|
),
|
|
(
|
|
Some(HashAlgorithm::SHA384),
|
|
Some(SymmetricAlgorithm::AES256),
|
|
),
|
|
(
|
|
Some(HashAlgorithm::SHA384),
|
|
Some(SymmetricAlgorithm::AES192),
|
|
),
|
|
(
|
|
Some(HashAlgorithm::SHA256),
|
|
Some(SymmetricAlgorithm::AES256),
|
|
),
|
|
],
|
|
_ => &[(None, None)],
|
|
};
|
|
|
|
for (hash, sym) in param {
|
|
if let Ok(key) = public_key_material_to_key(pkm, key_type, time, *hash, *sym) {
|
|
// check FP
|
|
if key.fingerprint().as_bytes() == fingerprint.as_bytes() {
|
|
// return if match
|
|
return Ok(key);
|
|
}
|
|
}
|
|
}
|
|
|
|
Err(Error::InternalError(
|
|
"Couldn't find key with matching fingerprint".to_string(),
|
|
))
|
|
}
|
|
|
|
/// Get a PublicKey representation for a key slot on the card
|
|
pub fn key_slot(open: &mut Open, kt: KeyType) -> Result<Option<PublicKey>> {
|
|
// FIXME: only read these once, if multiple subkeys are retrieved from the card
|
|
let times = open.key_generation_times()?;
|
|
let fps = open.fingerprints()?;
|
|
|
|
match kt {
|
|
KeyType::Signing => {
|
|
if let Ok(pkm) = open.public_key(KeyType::Signing) {
|
|
if let Some(ts) = times.signature() {
|
|
return Ok(Some(public_key_material_and_fp_to_key(
|
|
&pkm,
|
|
KeyType::Signing,
|
|
ts,
|
|
fps.signature().expect("Signature fingerprint is unset"),
|
|
)?));
|
|
}
|
|
}
|
|
Ok(None)
|
|
}
|
|
KeyType::Decryption => {
|
|
if let Ok(pkm) = open.public_key(KeyType::Decryption) {
|
|
if let Some(ts) = times.decryption() {
|
|
return Ok(Some(public_key_material_and_fp_to_key(
|
|
&pkm,
|
|
KeyType::Decryption,
|
|
ts,
|
|
fps.decryption().expect("Decryption fingerprint is unset"),
|
|
)?));
|
|
}
|
|
}
|
|
Ok(None)
|
|
}
|
|
KeyType::Authentication => {
|
|
if let Ok(pkm) = open.public_key(KeyType::Authentication) {
|
|
if let Some(ts) = times.authentication() {
|
|
return Ok(Some(public_key_material_and_fp_to_key(
|
|
&pkm,
|
|
KeyType::Authentication,
|
|
ts,
|
|
fps.authentication()
|
|
.expect("Authentication fingerprint is unset"),
|
|
)?));
|
|
}
|
|
}
|
|
Ok(None)
|
|
}
|
|
_ => unimplemented!(),
|
|
}
|
|
}
|
|
|
|
/// Helper fn: get a Sequoia PublicKey from an openpgp-card PublicKeyMaterial.
|
|
///
|
|
/// For ECC decryption keys, `hash` and `sym` can be optionally specified.
|
|
pub fn public_key_material_to_key(
|
|
pkm: &PublicKeyMaterial,
|
|
key_type: KeyType,
|
|
time: &KeyGenerationTime,
|
|
hash: Option<HashAlgorithm>,
|
|
sym: Option<SymmetricAlgorithm>,
|
|
) -> Result<PublicKey, Error> {
|
|
let time = Timestamp::from(time.get()).into();
|
|
|
|
match pkm {
|
|
PublicKeyMaterial::R(rsa) => {
|
|
let k4 = Key4::import_public_rsa(rsa.v(), rsa.n(), Some(time)).map_err(|e| {
|
|
Error::InternalError(format!("sequoia Key4::import_public_rsa failed: {:?}", e))
|
|
})?;
|
|
|
|
Ok(k4.into())
|
|
}
|
|
PublicKeyMaterial::E(ecc) => {
|
|
let algo = ecc.algo().clone(); // FIXME?
|
|
if let Algo::Ecc(algo_ecc) = algo {
|
|
let curve = match algo_ecc.curve() {
|
|
Curve::NistP256r1 => openpgp::types::Curve::NistP256,
|
|
Curve::NistP384r1 => openpgp::types::Curve::NistP384,
|
|
Curve::NistP521r1 => openpgp::types::Curve::NistP521,
|
|
Curve::Ed25519 => openpgp::types::Curve::Ed25519,
|
|
Curve::Cv25519 => openpgp::types::Curve::Cv25519,
|
|
c => unimplemented!("unhandled curve: {:?}", c),
|
|
};
|
|
|
|
match key_type {
|
|
KeyType::Authentication | KeyType::Signing => {
|
|
if algo_ecc.curve() == Curve::Ed25519 {
|
|
// EdDSA
|
|
let k4 =
|
|
Key4::import_public_ed25519(ecc.data(), time).map_err(|e| {
|
|
Error::InternalError(format!(
|
|
"sequoia Key4::import_public_ed25519 failed: {:?}",
|
|
e
|
|
))
|
|
})?;
|
|
|
|
Ok(Key::from(k4))
|
|
} else {
|
|
// ECDSA
|
|
let k4 = Key4::new(
|
|
time,
|
|
PublicKeyAlgorithm::ECDSA,
|
|
mpi::PublicKey::ECDSA {
|
|
curve,
|
|
q: mpi::MPI::new(ecc.data()),
|
|
},
|
|
)
|
|
.map_err(|e| {
|
|
Error::InternalError(format!(
|
|
"sequoia Key4::new for ECDSA failed: {:?}",
|
|
e
|
|
))
|
|
})?;
|
|
|
|
Ok(k4.into())
|
|
}
|
|
}
|
|
KeyType::Decryption => {
|
|
if algo_ecc.curve() == Curve::Cv25519 {
|
|
// EdDSA
|
|
let k4 = Key4::import_public_cv25519(ecc.data(), hash, sym, time)
|
|
.map_err(|e| {
|
|
Error::InternalError(format!(
|
|
"sequoia Key4::import_public_cv25519 failed: {:?}",
|
|
e
|
|
))
|
|
})?;
|
|
|
|
Ok(k4.into())
|
|
} else {
|
|
// ECDH
|
|
let k4 = Key4::new(
|
|
time,
|
|
PublicKeyAlgorithm::ECDH,
|
|
mpi::PublicKey::ECDH {
|
|
curve,
|
|
q: mpi::MPI::new(ecc.data()),
|
|
hash: hash.unwrap_or_default(),
|
|
sym: sym.unwrap_or_default(),
|
|
},
|
|
)
|
|
.map_err(|e| {
|
|
Error::InternalError(format!(
|
|
"sequoia Key4::new for ECDH failed: {:?}",
|
|
e
|
|
))
|
|
})?;
|
|
|
|
Ok(k4.into())
|
|
}
|
|
}
|
|
_ => unimplemented!("Unsupported KeyType"),
|
|
}
|
|
} else {
|
|
panic!("unexpected algo {:?}", algo);
|
|
}
|
|
}
|
|
_ => unimplemented!("Unexpected PublicKeyMaterial type"),
|
|
}
|
|
}
|
|
|
|
/// Mapping function to get a fingerprint from "PublicKeyMaterial +
|
|
/// timestamp + KeyType" (intended for use with `CardApp.generate_key()`).
|
|
///
|
|
/// For ECC decryption keys, `hash` and `sym` can be optionally specified.
|
|
pub(crate) fn public_to_fingerprint(
|
|
pkm: &PublicKeyMaterial,
|
|
time: &KeyGenerationTime,
|
|
kt: KeyType,
|
|
hash: Option<HashAlgorithm>,
|
|
sym: Option<SymmetricAlgorithm>,
|
|
) -> Result<Fingerprint, Error> {
|
|
// Transform PublicKeyMaterial into a Sequoia Key
|
|
let key = public_key_material_to_key(pkm, kt, time, hash, sym)?;
|
|
|
|
// Get fingerprint from the Sequoia Key
|
|
let fp = key.fingerprint();
|
|
fp.as_bytes().try_into()
|
|
}
|
|
|
|
/// Helper fn: get a CardUploadableKey for a ValidErasedKeyAmalgamation
|
|
pub fn vka_as_uploadable_key(
|
|
vka: ValidErasedKeyAmalgamation<SecretParts>,
|
|
password: Option<String>,
|
|
) -> Box<dyn CardUploadableKey> {
|
|
let sqk = SequoiaKey::new(vka, password);
|
|
Box::new(sqk)
|
|
}
|
|
|
|
/// FIXME: this fn is used in card_functionality, but should be removed
|
|
pub fn sign(
|
|
card_tx: &'_ mut OpenPgpTransaction<'_>,
|
|
cert: &Cert,
|
|
input: &mut dyn io::Read,
|
|
touch_prompt: &(dyn Fn() + Send + Sync),
|
|
) -> Result<String> {
|
|
let mut armorer = armor::Writer::new(vec![], armor::Kind::Signature)?;
|
|
{
|
|
let s = signer::CardSigner::new(card_tx, cert, touch_prompt)?;
|
|
|
|
let message = Message::new(&mut armorer);
|
|
let mut message = Signer::new(message, s).detached().build()?;
|
|
|
|
// Process input data, via message
|
|
io::copy(input, &mut message)?;
|
|
|
|
message.finalize()?;
|
|
}
|
|
|
|
let buffer = armorer.finalize()?;
|
|
|
|
String::from_utf8(buffer).context("Failed to convert signature to utf8")
|
|
}
|
|
|
|
/// FIXME: this fn is used in card_functionality, but should be removed
|
|
pub fn decrypt(
|
|
card_tx: &'_ mut OpenPgpTransaction<'_>,
|
|
cert: &Cert,
|
|
msg: Vec<u8>,
|
|
touch_prompt: &(dyn Fn() + Send + Sync),
|
|
p: &dyn Policy,
|
|
) -> Result<Vec<u8>> {
|
|
let mut decrypted = Vec::new();
|
|
{
|
|
let reader = io::BufReader::new(&msg[..]);
|
|
|
|
let d = decryptor::CardDecryptor::new(card_tx, cert, touch_prompt)?;
|
|
|
|
let db = DecryptorBuilder::from_reader(reader)?;
|
|
let mut decryptor = db.with_policy(p, None, d)?;
|
|
|
|
// Read all data from decryptor and store in decrypted
|
|
io::copy(&mut decryptor, &mut decrypted)?;
|
|
}
|
|
|
|
Ok(decrypted)
|
|
}
|