openpgp_card_rpgp/
cardslot.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 std::fmt::{Debug, Formatter};
6use std::sync::Mutex;
7
8use chrono::{DateTime, SubsecRound, Utc};
9use openpgp_card::ocard::crypto::Hash;
10use openpgp_card::ocard::KeyType;
11use openpgp_card::state::Transaction;
12use openpgp_card::Card;
13use pgp::composed::{Esk, Message, PlainSessionKey};
14use pgp::crypto::checksum;
15use pgp::crypto::ecc_curve::ECCCurve;
16use pgp::crypto::hash::HashAlgorithm;
17use pgp::crypto::public_key::PublicKeyAlgorithm;
18use pgp::crypto::sym::SymmetricKeyAlgorithm;
19use pgp::packet::{PublicKey, Signature, SignatureConfig, SignatureType, Subpacket, SubpacketData};
20use pgp::types::{
21    EcdhPublicParams, EcdsaPublicParams, Fingerprint, KeyDetails, KeyId, KeyVersion, Mpi, Password,
22    PkeskBytes, PublicKeyTrait, PublicParams, SecretKeyTrait, SignatureBytes,
23};
24use rand::thread_rng;
25use rsa::traits::PublicKeyParts;
26
27use crate::Error;
28
29/// An individual OpenPGP card key slot, which can be used for private key operations.
30pub struct CardSlot<'cs, 't> {
31    tx: Mutex<&'cs mut Card<Transaction<'t>>>,
32
33    // Which key slot does this OpenPGP card operate on
34    key_type: KeyType,
35
36    // The public key material that corresponds to the key slot of this signer
37    //
38    // The distinction between primary and subkey is irrelevant here, but we have to use some type.
39    // So we model the key data as a public primary key packet.
40    public_key: PublicKey,
41
42    touch_prompt: &'cs (dyn Fn() + Send + Sync),
43}
44
45impl<'cs, 't> CardSlot<'cs, 't> {
46    /// Set up a CardSlot for the card behind `tx`, using the key slot for `key_type`.
47    ///
48    /// Initializes the CardSlot based on public key information obtained from `public_key`.
49    pub fn with_public_key(
50        tx: &'cs mut Card<Transaction<'t>>,
51        key_type: KeyType,
52        public_key: PublicKey,
53        touch_prompt: &'cs (dyn Fn() + Send + Sync),
54    ) -> Result<Self, crate::Error> {
55        // FIXME: compare the fingerprint between card slot and public_key?
56
57        Ok(Self {
58            tx: Mutex::new(tx),
59            public_key,
60            key_type,
61            touch_prompt,
62        })
63    }
64
65    /// Set up a CardSlot for the card behind `tx`, using the key slot for `key_type`.
66    ///
67    /// Initializes the CardSlot based on public key information obtained from the card.
68    pub fn init_from_card(
69        tx: &'cs mut Card<Transaction<'t>>,
70        key_type: KeyType,
71        touch_prompt: &'cs (dyn Fn() + Send + Sync),
72    ) -> Result<Self, crate::Error> {
73        let pk = crate::rpgp::pubkey_from_card(tx, key_type)?;
74
75        Self::with_public_key(tx, key_type, pk, touch_prompt)
76    }
77}
78
79impl CardSlot<'_, '_> {
80    /// The OpenPGP public key material that corresponds to the key in this CardSlot
81    pub fn public_key(&self) -> &PublicKey {
82        &self.public_key
83    }
84
85    /// The card slot that this CardSlot uses
86    pub fn key_type(&self) -> KeyType {
87        self.key_type
88    }
89
90    fn touch_required(&self, tx: &mut Card<Transaction<'_>>) -> bool {
91        // Touch is required if:
92        // - the card supports the feature
93        // - and the policy is set to a value other than 'Off'
94        if let Ok(Some(uif)) = tx.user_interaction_flag(self.key_type) {
95            uif.touch_policy().touch_required()
96        } else {
97            false
98        }
99    }
100
101    pub fn decrypt(&self, values: &PkeskBytes) -> Result<(Vec<u8>, SymmetricKeyAlgorithm), Error> {
102        #[allow(clippy::unwrap_used)]
103        let mut tx = self.tx.lock().unwrap();
104
105        let mut ecdh = |public_point: &[u8],
106                        encrypted_session_key: &[u8],
107                        hash: HashAlgorithm,
108                        alg_sym: SymmetricKeyAlgorithm,
109                        curve|
110         -> Result<Vec<u8>, pgp::errors::Error> {
111            let ciphertext = match curve {
112                ECCCurve::Curve25519 => &public_point[1..],
113                _ => public_point,
114            };
115
116            let cryptogram = openpgp_card::ocard::crypto::Cryptogram::ECDH(ciphertext);
117
118            if self.touch_required(&mut tx) {
119                (self.touch_prompt)();
120            }
121
122            let shared_secret: Vec<u8> = tx.card().decipher(cryptogram).map_err(|e| {
123                Error::Message(format!("ECDH decipher operation on card failed {e}"))
124            })?;
125
126            let encrypted_key_len = encrypted_session_key.len();
127
128            let decrypted_key: Vec<u8> = pgp::crypto::ecdh::derive_session_key(
129                &shared_secret,
130                encrypted_session_key,
131                encrypted_key_len,
132                curve,
133                hash,
134                alg_sym,
135                self.public_key.fingerprint().as_bytes(),
136            )?;
137
138            Ok(decrypted_key)
139        };
140
141        let decrypted_key = match (self.public_key.public_params(), values) {
142            (PublicParams::RSA(public), PkeskBytes::Rsa { mpi }) => {
143                let mut ciphertext = mpi.as_ref().to_vec();
144
145                // RSA modulus length. We use this length to pad the ciphertext, in case it was
146                // zero truncated.
147                //
148                // FIXME: There might be a problematic corner case when the modulus itself is
149                // zero-stripped, and as a consequence we don't pad enough?
150                // (We might want to use rounding to "round" to a typical RSA modulus size?)
151                let modulus_len = public.key.n().to_bytes_be().len();
152
153                // Left zero-pad `ciphertext` to the length of the RSA modulus
154                // (that is: if the ciphertext was zero-stripped, undo that stripping).
155                //
156                // This padding is not required in most cases. Typically, the ciphertext and
157                // modulus should be the same length. However, there is a 1:256 likelihood that
158                // the ciphertext happens to start with a 0x0 byte, which OpenPGP will truncate.
159                while modulus_len > ciphertext.len() {
160                    ciphertext.insert(0, 0u8);
161                }
162
163                let cryptogram = openpgp_card::ocard::crypto::Cryptogram::RSA(&ciphertext);
164
165                if self.touch_required(&mut tx) {
166                    (self.touch_prompt)();
167                }
168
169                tx.card().decipher(cryptogram).map_err(|e| {
170                    Error::Message(format!("RSA decipher operation on card failed: {e}"))
171                })?
172            }
173
174            (
175                PublicParams::ECDH(EcdhPublicParams::Curve25519 {
176                    p: _p,
177                    alg_sym,
178                    hash,
179                    ..
180                }),
181                PkeskBytes::Ecdh {
182                    public_point,
183                    encrypted_session_key,
184                },
185            ) => ecdh(
186                public_point.as_ref(),
187                encrypted_session_key,
188                *hash,
189                *alg_sym,
190                ECCCurve::Curve25519.clone(),
191            )?,
192
193            (
194                PublicParams::ECDH(EcdhPublicParams::P256 {
195                    p: _p,
196                    alg_sym,
197                    hash,
198                    ..
199                }),
200                PkeskBytes::Ecdh {
201                    public_point,
202                    encrypted_session_key,
203                },
204            ) => ecdh(
205                public_point.as_ref(),
206                encrypted_session_key,
207                *hash,
208                *alg_sym,
209                ECCCurve::P256.clone(),
210            )?,
211
212            (
213                PublicParams::ECDH(EcdhPublicParams::P384 {
214                    p: _p,
215                    alg_sym,
216                    hash,
217                    ..
218                }),
219                PkeskBytes::Ecdh {
220                    public_point,
221                    encrypted_session_key,
222                },
223            ) => ecdh(
224                public_point.as_ref(),
225                encrypted_session_key,
226                *hash,
227                *alg_sym,
228                ECCCurve::P384.clone(),
229            )?,
230
231            (
232                PublicParams::ECDH(EcdhPublicParams::P521 {
233                    p: _p,
234                    alg_sym,
235                    hash,
236                    ..
237                }),
238                PkeskBytes::Ecdh {
239                    public_point,
240                    encrypted_session_key,
241                },
242            ) => ecdh(
243                public_point.as_ref(),
244                encrypted_session_key,
245                *hash,
246                *alg_sym,
247                ECCCurve::P521.clone(),
248            )?,
249
250            pp => {
251                return Err(Error::Message(format!(
252                    "decrypt: Unsupported key type  {pp:?}"
253                )));
254            }
255        };
256
257        // strip off the leading session key algorithm octet, and the two trailing checksum octets
258        let dec_len = decrypted_key.len();
259        let (sessionkey, checksum) = (
260            &decrypted_key[1..dec_len - 2],
261            &decrypted_key[dec_len - 2..],
262        );
263
264        // ... check the checksum, while we have it at hand
265        checksum::simple(
266            #[allow(clippy::expect_used)]
267            checksum.try_into().expect("this is two bytes long"),
268            sessionkey,
269        )
270        .map_err(|_| Error::Message("checksum mismatch while decrypting".to_string()))?;
271
272        let session_key_algorithm = decrypted_key[0].into();
273        Ok((sessionkey.to_vec(), session_key_algorithm))
274    }
275
276    /// Try to unwrap an encryption layer by decrypting one of the PKESK in message
277    pub fn decrypt_message<'a>(&self, message: Message<'a>) -> Result<Message<'a>, Error> {
278        let Message::Encrypted { esk, mut edata, .. } = message else {
279            return Err(Error::Message(
280                "message must be Message::Encrypted".to_string(),
281            ));
282        };
283
284        // try to decrypt all pkesk
285        let mut sk = None;
286
287        for e in &esk {
288            if let Esk::PublicKeyEncryptedSessionKey(pkesk) = e {
289                if let Ok(v) = pkesk.values() {
290                    // FIXME: match id?
291                    if let Ok((key, algo)) = self.decrypt(v) {
292                        sk = Some((key, algo));
293                        break;
294                    }
295                }
296            }
297        }
298
299        match sk {
300            Some(sk) => {
301                let plain_session_key = PlainSessionKey::V3_4 {
302                    key: sk.0.into(),
303                    sym_alg: sk.1,
304                };
305
306                edata.decrypt(&plain_session_key)?;
307
308                Ok(Message::from_bytes(edata)?)
309            }
310            None => Err(Error::Message(
311                "Failed to decrypt any PublicKeyEncryptedSessionKey".to_string(),
312            )),
313        }
314    }
315
316    pub fn sign_data(
317        &self,
318        data: &[u8],
319        text_mode: bool,
320        key_pw: &Password,
321        hash_algorithm: HashAlgorithm,
322    ) -> Result<Signature, Error> {
323        // FIXME: is there an API for this upstream?
324
325        let algorithm = self.algorithm();
326
327        let rng = thread_rng();
328
329        let typ = if text_mode {
330            SignatureType::Text
331        } else {
332            SignatureType::Binary
333        };
334
335        let mut config = match self.version() {
336            KeyVersion::V4 => SignatureConfig::v4(typ, algorithm, hash_algorithm),
337            KeyVersion::V6 => SignatureConfig::v6(rng, typ, algorithm, hash_algorithm)?,
338            v => return Err(Error::Message(format!("Unsupported key version {v:?}"))),
339        };
340        config.hashed_subpackets = vec![
341            Subpacket::regular(SubpacketData::IssuerFingerprint(self.fingerprint()))?,
342            Subpacket::critical(SubpacketData::SignatureCreationTime(
343                Utc::now().trunc_subsecs(0),
344            ))?,
345        ];
346        config.unhashed_subpackets =
347            vec![Subpacket::regular(SubpacketData::Issuer(self.key_id()))?];
348
349        let signature = config.sign(self, key_pw, data)?;
350
351        Ok(signature)
352    }
353}
354
355impl Debug for CardSlot<'_, '_> {
356    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
357        // FIXME: also show card identifier
358        write!(f, "CardSlot for {:?}", self.public_key)?;
359
360        Ok(())
361    }
362}
363
364impl KeyDetails for CardSlot<'_, '_> {
365    fn version(&self) -> KeyVersion {
366        KeyVersion::V4 // FIXME?
367    }
368
369    fn fingerprint(&self) -> Fingerprint {
370        self.public_key.fingerprint()
371    }
372
373    fn key_id(&self) -> KeyId {
374        self.public_key.key_id()
375    }
376
377    fn algorithm(&self) -> PublicKeyAlgorithm {
378        self.public_key.algorithm()
379    }
380}
381
382impl PublicKeyTrait for CardSlot<'_, '_> {
383    fn created_at(&self) -> &DateTime<Utc> {
384        self.public_key.created_at()
385    }
386
387    fn expiration(&self) -> Option<u16> {
388        None
389    }
390
391    fn verify_signature(
392        &self,
393        hash: HashAlgorithm,
394        data: &[u8],
395        sig: &SignatureBytes,
396    ) -> pgp::errors::Result<()> {
397        self.public_key.verify_signature(hash, data, sig)
398    }
399
400    fn public_params(&self) -> &PublicParams {
401        self.public_key.public_params()
402    }
403}
404
405impl SecretKeyTrait for CardSlot<'_, '_> {
406    fn create_signature(
407        &self,
408        _key_pw: &Password,
409        hash: HashAlgorithm,
410        data: &[u8],
411    ) -> pgp::errors::Result<SignatureBytes> {
412        #[allow(clippy::unwrap_used)]
413        let mut tx = self.tx.lock().unwrap();
414
415        let hash = match self.public_key.algorithm() {
416            PublicKeyAlgorithm::RSA => to_hash_rsa(data, hash)?,
417            PublicKeyAlgorithm::ECDSA => Hash::ECDSA({
418                match self.public_key.public_params() {
419                    PublicParams::ECDSA(EcdsaPublicParams::P256 { .. }) => {
420                        if data.len() < 32 {
421                            return Err(
422                                Error::Message("hash too short for P256".to_string()).into()
423                            );
424                        }
425
426                        &data[..32]
427                    }
428                    PublicParams::ECDSA(EcdsaPublicParams::P384 { .. }) => {
429                        if data.len() < 48 {
430                            return Err(
431                                Error::Message("hash too short for P384".to_string()).into()
432                            );
433                        }
434
435                        &data[..48]
436                    }
437                    PublicParams::ECDSA(EcdsaPublicParams::P521 { .. }) => {
438                        if data.len() < 64 {
439                            return Err(
440                                Error::Message("hash too short for P521".to_string()).into()
441                            );
442                        }
443
444                        &data[..64]
445                    }
446                    _ => data,
447                }
448            }),
449            PublicKeyAlgorithm::EdDSALegacy => Hash::EdDSA(data),
450
451            _ => {
452                return Err(Error::Message(format!(
453                    "Unsupported PublicKeyAlgorithm for signature creation: {:?}",
454                    self.public_key.algorithm()
455                ))
456                .into())
457            }
458        };
459
460        if self.touch_required(&mut tx) {
461            (self.touch_prompt)();
462        }
463
464        let sig = match self.key_type {
465            KeyType::Signing => tx
466                .card()
467                .signature_for_hash(hash)
468                .map_err(|e| Error::Message(format!("openpgp-card error: {e:?}")))?,
469            KeyType::Authentication => tx
470                .card()
471                .authenticate_for_hash(hash)
472                .map_err(|e| Error::Message(format!("openpgp-card error: {e:?}")))?,
473            _ => {
474                return Err(Error::Message(format!(
475                    "Unsupported KeyType for signature creation: {:?}",
476                    self.key_type
477                ))
478                .into())
479            }
480        };
481
482        let mpis = match self.public_key.algorithm() {
483            PublicKeyAlgorithm::RSA => vec![Mpi::from_slice(&sig)],
484
485            PublicKeyAlgorithm::ECDSA => {
486                let mid = sig.len() / 2;
487
488                vec![Mpi::from_slice(&sig[..mid]), Mpi::from_slice(&sig[mid..])]
489            }
490            PublicKeyAlgorithm::EdDSALegacy => {
491                // FIXME: check curve?
492                if sig.len() != 64 {
493                    return Err(Error::Message(format!(
494                        "Unexpected signature length {} for EdDSA",
495                        sig.len()
496                    ))
497                    .into());
498                }
499
500                vec![Mpi::from_slice(&sig[..32]), Mpi::from_slice(&sig[32..])]
501            }
502
503            alg => {
504                return Err(Error::Message(format!(
505                    "Unsupported algorithm for signature creation: {alg:?}"
506                ))
507                .into())
508            }
509        };
510
511        Ok(SignatureBytes::Mpis(mpis))
512    }
513
514    fn hash_alg(&self) -> HashAlgorithm {
515        self.public_key.public_params().hash_alg()
516    }
517}
518
519fn to_hash_rsa(data: &[u8], hash: HashAlgorithm) -> Result<Hash<'_>, Error> {
520    match hash {
521        HashAlgorithm::Sha256 => {
522            if data.len() == 0x20 {
523                #[allow(clippy::unwrap_used)]
524                Ok(Hash::SHA256(data.try_into().unwrap()))
525            } else {
526                Err(Error::Message(format!(
527                    "Illegal digest len for SHA256: {}",
528                    data.len()
529                )))
530            }
531        }
532        HashAlgorithm::Sha384 => {
533            if data.len() == 0x30 {
534                #[allow(clippy::unwrap_used)]
535                Ok(Hash::SHA384(data.try_into().unwrap()))
536            } else {
537                Err(Error::Message(format!(
538                    "Illegal digest len for SHA384: {}",
539                    data.len()
540                )))
541            }
542        }
543        HashAlgorithm::Sha512 => {
544            if data.len() == 0x40 {
545                #[allow(clippy::unwrap_used)]
546                Ok(Hash::SHA512(data.try_into().unwrap()))
547            } else {
548                Err(Error::Message(format!(
549                    "Illegal digest len for SHA512: {}",
550                    data.len()
551                )))
552            }
553        }
554        _ => Err(Error::Message(format!(
555            "Unsupported HashAlgorithm for RSA: {hash:?}"
556        ))),
557    }
558}