Skip to main content

pq_msg/exchange/
prekey.rs

1use sha2::{Digest, Sha256};
2
3use crate::{
4    errors::CryptoError,
5    exchange::pair::{self, KEMPair},
6    signatures::keypair::{self, PublicKeyBytes, SignerPair, VerifierPair, ViewOperations},
7};
8
9/// Label prepended to a prekey before it is signed, so a prekey signature can
10/// never be mistaken for a message signature
11const PREKEY_LABEL: &[u8] = b"pq-msg/v2 prekey";
12
13/// Size of a serialized signed prekey in bytes
14pub const SIGNED_PREKEY_BYTES: usize = pair::PUBLIC_KEY_BYTES + keypair::SIGNATURE_BYTES;
15
16/// Identifies a prekey: the SHA-256 hash of its public key
17pub type PrekeyId = [u8; 32];
18
19/// The public half of a prekey, signed by its owner's FN-DSA identity key
20///
21/// A prekey is an ordinary [`KEMPair`] that a responder creates ahead of time.
22/// The responder keeps the `KEMPair` and publishes this signed public part, for
23/// example on a server. An initiator starts a session by encapsulating to it.
24///
25/// There are two kinds, and the difference is only how the responder treats them:
26/// - **One-time prekeys** are used for a single session. Once the session is set
27///   up, the responder deletes the `KEMPair` everywhere it is stored. After that,
28///   nothing can decrypt that session's handshake, which gives forward secrecy.
29/// - A **last-resort prekey** is used whenever no one-time prekey is left. It is
30///   reused, so sessions using it only gain forward secrecy once the responder
31///   replaces it and deletes the old one. Rotate it regularly (for example weekly).
32#[derive(Clone)]
33pub struct SignedPrekey {
34    kem_pub_key: [u8; pair::PUBLIC_KEY_BYTES],
35    signature: keypair::SignatureBytes,
36}
37
38impl SignedPrekey {
39    /// Signs the public key of `prekey` with the owner's identity key
40    ///
41    /// # Arguments
42    /// * `owner` - The identity signer pair of the prekey's owner
43    /// * `prekey` - The prekey to publish
44    ///
45    /// # Returns
46    /// - `Result<SignedPrekey, CryptoError>`: The signed prekey or an error
47    pub fn new(owner: &mut SignerPair, prekey: &KEMPair) -> Result<Self, CryptoError> {
48        let kem_pub_key = prekey.pub_key_bytes();
49        let signature = owner.sign(&signed_data(&kem_pub_key))?;
50        Ok(Self {
51            kem_pub_key,
52            signature,
53        })
54    }
55
56    /// Checks that this prekey was signed by the owner of `identity`
57    ///
58    /// # Arguments
59    /// * `identity` - The FN-DSA public key of the claimed owner
60    ///
61    /// # Returns
62    /// True if the signature is valid for this identity, false otherwise
63    pub fn verify(&self, identity: &PublicKeyBytes) -> bool {
64        VerifierPair::new(identity)
65            .is_ok_and(|v| v.verify(&signed_data(&self.kem_pub_key), &self.signature))
66    }
67
68    /// Returns the identifier the responder uses to find the matching `KEMPair`
69    pub fn id(&self) -> PrekeyId {
70        Sha256::digest(self.kem_pub_key).into()
71    }
72
73    /// Returns the prekey's ML-KEM public key
74    pub fn kem_pub_key(&self) -> &[u8; pair::PUBLIC_KEY_BYTES] {
75        &self.kem_pub_key
76    }
77
78    /// Serializes the signed prekey: public key followed by signature
79    pub fn to_bytes(&self) -> [u8; SIGNED_PREKEY_BYTES] {
80        let mut bytes = [0u8; SIGNED_PREKEY_BYTES];
81        bytes[..pair::PUBLIC_KEY_BYTES].copy_from_slice(&self.kem_pub_key);
82        bytes[pair::PUBLIC_KEY_BYTES..].copy_from_slice(&self.signature);
83        bytes
84    }
85
86    /// Deserializes a signed prekey
87    ///
88    /// This only checks the length. Call [`SignedPrekey::verify`] (as
89    /// `MessageSession::new_initiator` does) before trusting it.
90    ///
91    /// # Arguments
92    /// * `bytes` - The serialized signed prekey
93    ///
94    /// # Returns
95    /// - `Result<SignedPrekey, CryptoError>`: The signed prekey or an error
96    pub fn from_bytes(bytes: &[u8]) -> Result<Self, CryptoError> {
97        if bytes.len() != SIGNED_PREKEY_BYTES {
98            return Err(CryptoError::IncongruentLength(
99                SIGNED_PREKEY_BYTES,
100                bytes.len(),
101            ));
102        }
103        let (kem_pub_key, signature) = bytes.split_at(pair::PUBLIC_KEY_BYTES);
104        Ok(Self {
105            kem_pub_key: kem_pub_key.try_into()?,
106            signature: signature.try_into()?,
107        })
108    }
109}
110
111/// The bytes signed for a prekey: label || public key
112fn signed_data(kem_pub_key: &[u8; pair::PUBLIC_KEY_BYTES]) -> Vec<u8> {
113    [PREKEY_LABEL, &kem_pub_key[..]].concat()
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn test_signed_prekey() {
122        let mut owner = SignerPair::create();
123        let prekey = KEMPair::create();
124        let signed = SignedPrekey::new(&mut owner, &prekey).unwrap();
125
126        assert!(signed.verify(owner.pub_key_bytes()));
127        assert!(!signed.verify(SignerPair::create().pub_key_bytes()));
128        let mut undecodable = *owner.pub_key_bytes();
129        undecodable[0] ^= 0xFF;
130        assert!(!signed.verify(&undecodable));
131        assert_eq!(signed.kem_pub_key(), &prekey.pub_key_bytes());
132
133        let restored = SignedPrekey::from_bytes(&signed.to_bytes()).unwrap();
134        assert!(restored.verify(owner.pub_key_bytes()));
135        assert_eq!(restored.id(), signed.id());
136        assert_ne!(signed.id(), {
137            let other = KEMPair::create();
138            SignedPrekey::new(&mut owner, &other).unwrap().id()
139        });
140    }
141
142    #[test]
143    fn test_swapped_kem_key_rejected() {
144        // Someone (e.g. a malicious server) swaps in their own KEM key under the owner's signature
145        let mut owner = SignerPair::create();
146        let signed = SignedPrekey::new(&mut owner, &KEMPair::create()).unwrap();
147
148        let mut bytes = signed.to_bytes();
149        bytes[..pair::PUBLIC_KEY_BYTES].copy_from_slice(&KEMPair::create().pub_key_bytes());
150        let swapped = SignedPrekey::from_bytes(&bytes).unwrap();
151        assert!(!swapped.verify(owner.pub_key_bytes()));
152
153        assert!(matches!(
154            SignedPrekey::from_bytes(&bytes[1..]),
155            Err(CryptoError::IncongruentLength(SIGNED_PREKEY_BYTES, _))
156        ));
157    }
158
159    #[test]
160    fn test_validly_signed_invalid_kem_key_rejected() {
161        // A malicious peer signs a KEM key that isn't valid ML-KEM; starting a
162        // session with it must fail cleanly
163        let mut owner = SignerPair::create();
164        let kem_pub_key = [0xFF; pair::PUBLIC_KEY_BYTES];
165        let bad = SignedPrekey {
166            kem_pub_key,
167            signature: owner.sign(&signed_data(&kem_pub_key)).unwrap(),
168        };
169        assert!(bad.verify(owner.pub_key_bytes()));
170
171        let result = crate::messaging::MessageSession::new_initiator(
172            SignerPair::create(),
173            [0u8; 24],
174            &bad,
175            owner.pub_key_bytes(),
176        );
177        assert!(matches!(result, Err(CryptoError::InvalidKey)));
178    }
179}