openpgp_card_rpgp/
rpgp.rs

1// SPDX-FileCopyrightText: Wiktor Kwapisiewicz <wiktor@metacode.biz>
2// SPDX-FileCopyrightText: Heiko Schaefer <heiko@schaefer.name>
3// SPDX-License-Identifier: Apache-2.0 OR MIT
4
5use chrono::{DateTime, SubsecRound, Utc};
6use openpgp_card::ocard::algorithm::{AlgorithmAttributes, Curve};
7use openpgp_card::ocard::crypto::{EccType, PublicKeyMaterial};
8use openpgp_card::ocard::data::{Fingerprint, KeyGenerationTime};
9use openpgp_card::ocard::KeyType;
10use pgp::composed::{SignedKeyDetails, SignedPublicKey, SignedPublicSubKey};
11use pgp::crypto::ecc_curve::ECCCurve;
12use pgp::crypto::hash::HashAlgorithm;
13use pgp::crypto::public_key::PublicKeyAlgorithm;
14use pgp::crypto::sym::SymmetricKeyAlgorithm;
15use pgp::packet::{
16    Features, KeyFlags, PacketHeader, PacketTrait, PubKeyInner, PublicKey, PublicSubkey,
17    SignatureConfig, SignatureType, Subpacket, SubpacketData, UserId,
18};
19use pgp::types::{
20    EcdhKdfType, EcdhPublicParams, EcdsaPublicParams, EddsaLegacyPublicParams, KeyDetails,
21    KeyVersion, PacketHeaderVersion, Password, PublicKeyTrait, PublicParams, RsaPublicParams,
22    SecretKeyTrait, SignedUser, Tag,
23};
24use rsa::RsaPublicKey;
25use secrecy::SecretString;
26
27use crate::{CardSlot, Error};
28
29/// value pairs that we'll consider in ECDH parameter auto-detection
30const ECDH_PARAM: &[(Option<HashAlgorithm>, Option<SymmetricKeyAlgorithm>)] = &[
31    (
32        Some(HashAlgorithm::Sha256),
33        Some(SymmetricKeyAlgorithm::AES128),
34    ),
35    (
36        Some(HashAlgorithm::Sha512),
37        Some(SymmetricKeyAlgorithm::AES256),
38    ),
39    (
40        Some(HashAlgorithm::Sha384),
41        Some(SymmetricKeyAlgorithm::AES256),
42    ),
43    (
44        Some(HashAlgorithm::Sha384),
45        Some(SymmetricKeyAlgorithm::AES192),
46    ),
47    (
48        Some(HashAlgorithm::Sha256),
49        Some(SymmetricKeyAlgorithm::AES256),
50    ),
51];
52
53fn pubkey(
54    algo: PublicKeyAlgorithm,
55    created: DateTime<Utc>,
56    param: PublicParams,
57) -> Result<PublicKey, Error> {
58    Ok(PublicKey::from_inner(PubKeyInner::new(
59        KeyVersion::V4, // FIXME: handle other OpenPGP key versions, later
60        algo,
61        created,
62        None,
63        param,
64    )?)?)
65}
66
67fn map_curve(c: &Curve) -> Result<ECCCurve, Error> {
68    Ok(match c {
69        Curve::NistP256r1 => ECCCurve::P256,
70        Curve::NistP384r1 => ECCCurve::P384,
71        Curve::NistP521r1 => ECCCurve::P521,
72        Curve::BrainpoolP256r1 => ECCCurve::BrainpoolP256r1,
73        Curve::BrainpoolP384r1 => ECCCurve::BrainpoolP384r1,
74        Curve::BrainpoolP512r1 => ECCCurve::BrainpoolP512r1,
75        Curve::Ed25519 => ECCCurve::Ed25519,
76        Curve::Curve25519 => ECCCurve::Curve25519,
77
78        _ => return Err(Error::Message(format!("Can't map curve {c:?}"))),
79    })
80}
81
82/// Get PublicKey for an openpgp-card PublicKeyMaterial, KeyGenerationTime and Fingerprint.
83///
84/// For ECC decryption keys, possible values for the parameters `hash` and `alg_sym` will be tested.
85/// If a key with matching fingerprint is found in this way, it is considered the correct key,
86/// and returned.
87///
88/// The Fingerprint of the retrieved PublicKey is always validated against the `Fingerprint` as
89/// stored on the card. If the fingerprints don't match, an Error is returned.
90pub fn public_key_material_and_fp_to_key(
91    pkm: &PublicKeyMaterial,
92    key_type: KeyType,
93    created: &KeyGenerationTime,
94    fingerprint: &Fingerprint,
95) -> Result<PublicKey, Error> {
96    // Possible hash/sym parameters based on statistics over 2019-12 SKS dump:
97    // https://gitlab.com/sequoia-pgp/sequoia/-/issues/838#note_909813463
98
99    let param: &[_] = match (pkm, key_type) {
100        (PublicKeyMaterial::E(_), KeyType::Decryption) => ECDH_PARAM,
101        _ => &[(None, None)],
102    };
103
104    for (hash, alg_sym) in param {
105        if let Ok(key) = public_key_material_to_key(pkm, key_type, created, *hash, *alg_sym) {
106            // check FP
107            if key.fingerprint().as_bytes() == fingerprint.as_bytes() {
108                // return if match
109                return Ok(key);
110            }
111        }
112    }
113
114    Err(Error::Message(
115        "Couldn't find key with matching fingerprint".to_string(),
116    ))
117}
118
119/// Transform an openpgp-card `PublicKeyMaterial` and `KeyGenerationTime`
120///  into an rpgp `PublicKey` representation.
121///
122/// For ECDH decryption keys, `hash` and `alg_sym` can be optionally specified.
123///
124/// If `hash` and `alg_sym` are required, and the caller passes `None`,
125///  curve-specific default parameters are used.
126pub fn public_key_material_to_key(
127    pkm: &PublicKeyMaterial,
128    key_type: KeyType,
129    created: &KeyGenerationTime,
130    hash: Option<HashAlgorithm>,
131    alg_sym: Option<SymmetricKeyAlgorithm>,
132) -> Result<PublicKey, Error> {
133    #[allow(clippy::expect_used)]
134    let created =
135        DateTime::<Utc>::from_timestamp(created.get() as i64, 0).expect("u32 time from card");
136
137    match pkm {
138        PublicKeyMaterial::R(rsa) => pubkey(
139            PublicKeyAlgorithm::RSA,
140            created,
141            PublicParams::RSA(RsaPublicParams {
142                key: RsaPublicKey::new(
143                    rsa::BigUint::from_bytes_be(rsa.n()),
144                    rsa::BigUint::from_bytes_be(rsa.v()),
145                )?,
146            }),
147        ),
148
149        PublicKeyMaterial::E(ecc) => match ecc.algo() {
150            AlgorithmAttributes::Ecc(ecc_attr) => {
151                let typ = ecc_attr.ecc_type();
152
153                let curve = map_curve(ecc_attr.curve())?;
154
155                let (pka, pp) = match typ {
156                    EccType::ECDH => {
157                        if key_type != KeyType::Decryption {
158                            return Err(Error::Message(format!(
159                                "ECDH is unsupported in key slot {key_type:?}"
160                            )));
161                        }
162
163                        let hash = hash.unwrap_or(curve.hash_algo()?);
164                        let alg_sym = alg_sym.unwrap_or(curve.sym_algo()?);
165
166                        let ecdh_pp = match curve {
167                            ECCCurve::Curve25519 => {
168                                if ecc.data().len() != 32 {
169                                    return Err(Error::Message(format!(
170                                        "x25519 public key unexpected length {}",
171                                        ecc.data().len()
172                                    )));
173                                }
174
175                                let mut public_key_arr = [0u8; 32];
176                                public_key_arr[..].copy_from_slice(ecc.data());
177
178                                let p = x25519_dalek::PublicKey::from(public_key_arr);
179                                EcdhPublicParams::Curve25519 {
180                                    p,
181                                    hash,
182                                    alg_sym,
183                                    ecdh_kdf_type: EcdhKdfType::Native,
184                                }
185                            }
186                            ECCCurve::P256 => {
187                                let p = p256::PublicKey::from_sec1_bytes(ecc.data())?;
188                                EcdhPublicParams::P256 { p, hash, alg_sym }
189                            }
190                            ECCCurve::P384 => {
191                                let p = p384::PublicKey::from_sec1_bytes(ecc.data())?;
192                                EcdhPublicParams::P384 { p, hash, alg_sym }
193                            }
194                            ECCCurve::P521 => {
195                                let p = p521::PublicKey::from_sec1_bytes(ecc.data())?;
196                                EcdhPublicParams::P521 { p, hash, alg_sym }
197                            }
198                            _ => {
199                                return Err(Error::Message(format!(
200                                    "Unsupported curve {:?} for ecdh",
201                                    curve.name()
202                                )))
203                            }
204                        };
205
206                        (PublicKeyAlgorithm::ECDH, PublicParams::ECDH(ecdh_pp))
207                    }
208
209                    EccType::ECDSA => {
210                        let ecdsa_pp = match curve {
211                            ECCCurve::P256 => {
212                                let key = p256::PublicKey::from_sec1_bytes(ecc.data())?;
213                                EcdsaPublicParams::P256 { key }
214                            }
215                            ECCCurve::P384 => {
216                                let key = p384::PublicKey::from_sec1_bytes(ecc.data())?;
217                                EcdsaPublicParams::P384 { key }
218                            }
219                            ECCCurve::P521 => {
220                                let key = p521::PublicKey::from_sec1_bytes(ecc.data())?;
221                                EcdsaPublicParams::P521 { key }
222                            }
223                            // FIXME: brainpool
224                            _ => {
225                                return Err(Error::Message(format!(
226                                    "Unsupported curve {:?} for ecdsa",
227                                    curve.name()
228                                )))
229                            }
230                        };
231
232                        (PublicKeyAlgorithm::ECDSA, PublicParams::ECDSA(ecdsa_pp))
233                    }
234
235                    EccType::EdDSA => {
236                        if curve != ECCCurve::Ed25519 {
237                            return Err(Error::Message(format!(
238                                "Inconsistent curve {} for EdDSA",
239                                curve.name()
240                            )));
241                        }
242
243                        let key: ed25519_dalek::VerifyingKey = ecc.data().try_into()?;
244
245                        (
246                            PublicKeyAlgorithm::EdDSALegacy,
247                            PublicParams::EdDSALegacy(EddsaLegacyPublicParams::Ed25519 { key }),
248                        )
249                    }
250                };
251
252                pubkey(pka, created, pp)
253            }
254
255            _ => Err(Error::Message(format!(
256                "Unexpected AlgorithmAttributes type in Ecc {:?}",
257                ecc.algo(),
258            ))),
259        },
260    }
261}
262
263pub(crate) fn pubkey_from_card(
264    tx: &mut openpgp_card::Card<openpgp_card::state::Transaction>,
265    key_type: KeyType,
266) -> Result<PublicKey, Error> {
267    let pkm = tx.public_key_material(key_type)?;
268
269    let Some(created) = tx.key_generation_time(key_type)? else {
270        // KeyGenerationTime is None
271        return Err(Error::Message(format!(
272            "No creation time set for OpenPGP card key type {key_type:?}",
273        )));
274    };
275
276    let Some(fingerprint) = tx.fingerprint(key_type)? else {
277        // Fingerprint is None
278        return Err(Error::Message(format!(
279            "No fingerprint found for key slot {key_type:?}"
280        )));
281    };
282
283    public_key_material_and_fp_to_key(&pkm, key_type, &created, &fingerprint)
284}
285
286/// Calculate the `Fingerprint` for `PublicKeyMaterial` and `KeyGenerationTime`
287pub fn public_to_fingerprint(
288    pkm: &PublicKeyMaterial,
289    kgt: KeyGenerationTime,
290    kt: KeyType,
291) -> Result<Fingerprint, openpgp_card::Error> {
292    let key = public_key_material_to_key(pkm, kt, &kgt, None, None).map_err(|e| {
293        openpgp_card::Error::InternalError(format!("public_key_material_to_key: {e}"))
294    })?;
295
296    let fp = key.fingerprint();
297
298    openpgp_card::ocard::data::Fingerprint::try_from(fp.as_bytes())
299}
300
301// FIXME: upstream?
302fn pri_to_sub(pubkey: PublicKey) -> pgp::errors::Result<PublicSubkey> {
303    // FIXME: this is a bit of a hack
304
305    let header = PacketHeader::from_parts(
306        pubkey.packet_header().version(),
307        Tag::PublicSubkey,
308        pubkey.packet_header().packet_length(),
309    )?;
310
311    PublicSubkey::new_with_header(
312        header,
313        pubkey.version(),
314        pubkey.algorithm(),
315        *pubkey.created_at(),
316        pubkey.expiration(),
317        pubkey.public_params().clone(),
318    )
319}
320
321/// Bind the component keys on a card into a `SignedPublicKey`.
322///
323/// NOTE: This function makes a number of assumptions that don't apply to all OpenPGP keys!
324/// The resulting OpenPGP public key object may be unfit for purpose!
325///
326/// This function assumes that the signing slot of the card serves as the
327/// primary key, and uses it to issue binding self-signatures.
328///
329/// This function sets the certification- and data-signature key flags on the `sig` component ke.
330///
331/// At least one User ID is required. The first User ID is marked as "primary user id".
332///
333///
334/// If `user_pin` is None, pinpad verification is attempted.
335/// `pinpad_prompt` is called to notify the user when pinpad input (of the
336///  User PIN) is required.
337///
338/// `touch_prompt` is called to notify the user when touch confirmation is
339/// required on the card for a signing operation.
340///
341/// FIXME: Accept optional metadata for user_id binding(s)?
342#[allow(clippy::too_many_arguments)]
343pub fn bind_into_certificate(
344    tx: &mut openpgp_card::Card<openpgp_card::state::Transaction>,
345    sig: PublicKey,
346    dec: Option<PublicKey>,
347    aut: Option<PublicKey>,
348    user_ids: &[String],
349    user_pin: Option<SecretString>,
350    pinpad_prompt: &dyn Fn(),
351    touch_prompt: &(dyn Fn() + Send + Sync),
352) -> Result<SignedPublicKey, Error> {
353    let primary = sig;
354
355    if user_ids.is_empty() {
356        return Err(Error::Message(
357            "At least one User ID must be added to create a valid certificate".to_string(),
358        ));
359    }
360
361    // helper: use the card to perform a signing operation
362    let verify_signing_pin =
363        |txx: &mut openpgp_card::Card<openpgp_card::state::Transaction>|
364         -> Result<(), openpgp_card::Error> {
365            // Allow signing on the card
366            if let Some(pw1) = user_pin.clone() {
367                txx.verify_user_signing_pin(pw1)?;
368            } else {
369                txx.verify_user_signing_pinpad(pinpad_prompt)?;
370            }
371            Ok(())
372        };
373
374    let mut subkeys = vec![];
375
376    if let Some(dec) = dec {
377        // add decryption key as subkey
378        let key = pri_to_sub(dec)?;
379
380        verify_signing_pin(tx)?;
381
382        // make binding signature, sign with cs
383        let cs = CardSlot::with_public_key(tx, KeyType::Signing, primary.clone(), touch_prompt)?;
384
385        let mut kf = KeyFlags::default();
386        kf.set_encrypt_comms(true);
387        kf.set_encrypt_storage(true);
388
389        let mut config =
390            SignatureConfig::v4(SignatureType::SubkeyBinding, cs.algorithm(), cs.hash_alg());
391
392        config.hashed_subpackets = vec![
393            Subpacket::critical(SubpacketData::SignatureCreationTime(
394                Utc::now().trunc_subsecs(0),
395            ))?,
396            Subpacket::critical(SubpacketData::KeyFlags(kf))?,
397            Subpacket::regular(SubpacketData::IssuerFingerprint(cs.fingerprint()))?,
398        ];
399        config.unhashed_subpackets = vec![Subpacket::regular(SubpacketData::Issuer(cs.key_id()))?];
400
401        let sig = config.sign_subkey_binding(&cs, cs.public_key(), &Password::empty(), &key)?;
402
403        let sps = SignedPublicSubKey {
404            key,
405            signatures: vec![sig],
406        };
407
408        subkeys.push(sps);
409    }
410
411    if let Some(aut) = aut {
412        // add decryption key as subkey
413        let key = pri_to_sub(aut)?;
414
415        verify_signing_pin(tx)?;
416
417        // make binding signature, sign with cs
418        let cs = CardSlot::with_public_key(tx, KeyType::Signing, primary.clone(), touch_prompt)?;
419
420        let mut kf = KeyFlags::default();
421        kf.set_authentication(true);
422
423        let mut config =
424            SignatureConfig::v4(SignatureType::SubkeyBinding, cs.algorithm(), cs.hash_alg());
425
426        config.hashed_subpackets = vec![
427            Subpacket::critical(SubpacketData::SignatureCreationTime(
428                Utc::now().trunc_subsecs(0),
429            ))?,
430            Subpacket::critical(SubpacketData::KeyFlags(kf))?,
431            Subpacket::regular(SubpacketData::IssuerFingerprint(cs.fingerprint()))?,
432        ];
433        config.unhashed_subpackets = vec![Subpacket::regular(SubpacketData::Issuer(cs.key_id()))?];
434
435        let sig = config.sign_subkey_binding(&cs, &cs.public_key(), &Password::empty(), &key)?;
436
437        let sps = SignedPublicSubKey {
438            key,
439            signatures: vec![sig],
440        };
441
442        subkeys.push(sps);
443    }
444
445    let mut users = vec![];
446
447    // add `user_ids`.
448    for (n, uid) in user_ids.iter().enumerate() {
449        let uid = UserId::from_str(PacketHeaderVersion::New, uid)?;
450
451        let mut kf = KeyFlags::default();
452        kf.set_certify(true);
453        kf.set_sign(true);
454
455        verify_signing_pin(tx)?;
456
457        let cs = CardSlot::with_public_key(tx, KeyType::Signing, primary.clone(), touch_prompt)?;
458        let mut config =
459            SignatureConfig::v4(SignatureType::CertPositive, cs.algorithm(), cs.hash_alg());
460
461        config.hashed_subpackets = vec![
462            // make this primary user id binding valid at certificate creation time
463            Subpacket::critical(SubpacketData::SignatureCreationTime(*primary.created_at()))?,
464            Subpacket::critical(SubpacketData::KeyFlags(kf))?,
465            // the first user id is marked as primary
466            Subpacket::regular(SubpacketData::IsPrimary(n == 0))?,
467            // SEIPDv1
468            Subpacket::regular(SubpacketData::Features(Features::from(&[0x01][..])))?,
469        ];
470        config.unhashed_subpackets = vec![Subpacket::regular(SubpacketData::Issuer(cs.key_id()))?];
471
472        let sig =
473            config.sign_certification(&cs, cs.public_key(), &Password::empty(), uid.tag(), &uid)?;
474
475        let suid = SignedUser::new(uid, vec![sig]);
476
477        users.push(suid);
478    }
479
480    // TODO: consider generating a direct key signature?
481
482    let details = SignedKeyDetails::new(vec![], vec![], users, vec![]);
483
484    let spk = SignedPublicKey::new(primary, details, subkeys);
485
486    Ok(spk)
487}