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