oc-crypto 0.0.2

Cryptographic primitives and key schemes for Open Crate containers
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
//! RSA-PSS-SHA256 signature verification: editing-device signature.
//!
//! # Why our own implementation rather than a crate
//!
//! Verification must live in a PURE crate: `oc-format` and `oc-protocol` verify signatures and
//! build for `wasm32-unknown-unknown`, ruling out
//! platform CNG. The dependency tree lacks the `rsa` crate, and taking it is undesirable:
//! it carries RUSTSEC-2023-0071 (Marvin); although only indirectly relevant
//! here (the attack concerns private operations, while the TPM signs), an exception
//! would have to be granted manually.
//!
//! `crypto-bigint` is already in the tree via `p256`. Beyond it, all we need
//! is modular exponentiation with a PUBLIC exponent and parsing
//! the PSS encoding. **The operation contains no secrets**, so constant
//! time is unnecessary; that is exactly why "our own crypto" is acceptable here,
//! not because it is cheaper.
//!
//! # The danger and its consequences
//!
//! Historically, VERIFIERS were what broke: Bleichenbacher's `e = 3` attack targeted
//! careless padding parsing, not RSA strength. Therefore:
//!
//! * **the exponent is not a parameter.** Fixed internally at 65537, with no way to pass `e = 3`.
//!   A verifier need not support anything our signer does not produce;
//! * **salt length is required, not recovered.** The format fixes it at 32 bytes
//!   (`docs/format.md`, "VERSION 3 OPENED", item 7), so another salt length means
//!   rejection, not "accept if it verifies". Recovering length from a delimiter
//!   accepts more than the format allows;
//! * **modulus length is specified by type.** RSA-2048 only.
//!
//! # What has been verified by execution
//!
//! The vector in `tests/kat/rsa_pss.kat` came from this machine's LIVE TPM
//! (`spikes/rsa-pss-tpm/`), not invention or documentation. It cannot be rederived
//! from the specification.

use crate::CryptoError;
use crypto_bigint::modular::{BoxedMontyForm, BoxedMontyParams};
use crypto_bigint::{BoxedUint, Odd};
use sha2::{Digest, Sha256};

/// RSA-2048 modulus length in bytes. Also the signature length and `EM` encoding length.
pub const MODULUS_LEN: usize = 256;

/// Modulus bit width.
const BITS: u32 = 2048;

/// SHA-256 hash length.
const HLEN: usize = 32;

/// Salt length fixed by the format.
const SLEN: usize = 32;

/// `DB` length: `emLen − hLen − 1` = 256 − 32 − 1.
const DB_LEN: usize = 223;

/// Zero padding before the delimiter: `emLen − sLen − hLen − 2` = 256 − 32 − 32 − 2.
const PS_LEN: usize = 190;

/// Exponent. Fixed: see module documentation.
const EXPONENT: [u8; 3] = [0x01, 0x00, 0x01];

/// `MGF1` over SHA-256 (RFC 8017 §B.2.1), writing exactly the buffer length.
fn mgf1(seed: &[u8], out: &mut [u8]) {
    let mut counter: u32 = 0;
    for block in out.chunks_mut(HLEN) {
        let mut hasher = Sha256::new();
        hasher.update(seed);
        hasher.update(counter.to_be_bytes());
        let digest = hasher.finalize();
        for (dst, src) in block.iter_mut().zip(digest.iter()) {
            *dst = *src;
        }
        // Переполнение недостижимо: блоков здесь семь. `wrapping_add` стоит
        // потому, что арифметика с побочными эффектами в этом workspace
        // запрещена, а «недостижимо» надо выражать кодом, а не комментарием.
        counter = counter.wrapping_add(1);
    }
}

/// Verify RSA-PSS-SHA256 with a 32-byte salt and exponent 65537.
///
/// # Errors
///
/// [`CryptoError::BadSignature`]: signature mismatch or encoding does not
/// conform to PSS. [`CryptoError::BadLength`]: incorrect signature length or
/// a modulus that cannot be parsed as a number of the required bit width.
///
/// Two distinct codes deliberately: "wrong length" means caller error or a
/// truncated file; "mismatch" means forgery or the wrong key. Merging them
/// would report "forgery" for our own error.
pub fn verify_pss_sha256(
    modulus: &[u8; MODULUS_LEN],
    message: &[u8],
    signature: &[u8],
) -> Result<(), CryptoError> {
    if signature.len() != MODULUS_LEN {
        return Err(CryptoError::BadLength);
    }

    let n = BoxedUint::from_be_slice(modulus.as_slice(), BITS).map_err(|_| CryptoError::BadLength)?;
    let s = BoxedUint::from_be_slice(signature, BITS).map_err(|_| CryptoError::BadLength)?;
    // `s < n` требует RFC, и без проверки представление подписи неоднозначно:
    // `s` и `s + n` дали бы одну кодировку.
    if s >= n {
        return Err(CryptoError::BadSignature);
    }
    // Модуль RSA нечётен по построению; чётный означает не «слабый ключ», а
    // подсунутый вместо ключа мусор.
    let odd = Odd::new(n).into_option().ok_or(CryptoError::BadLength)?;
    let params = BoxedMontyParams::new(odd);
    let exponent =
        BoxedUint::from_be_slice(&EXPONENT, BITS).map_err(|_| CryptoError::BadLength)?;

    let em = BoxedMontyForm::new(s, &params).pow(&exponent).retrieve().to_be_bytes();
    if em.len() != MODULUS_LEN {
        return Err(CryptoError::BadLength);
    }

    // Хвостовой байт кодировки.
    if em.last() != Some(&0xbc) {
        return Err(CryptoError::BadSignature);
    }

    let (masked_db, rest) = em.split_at_checked(DB_LEN).ok_or(CryptoError::BadLength)?;
    let h = rest.get(..HLEN).ok_or(CryptoError::BadLength)?;

    // Старший бит кодировки обязан быть нулевым: `emBits = 2047`, то есть на
    // один бит меньше, чем байтов. Без проверки кодировка перестаёт быть
    // однозначной.
    let first = masked_db.first().copied().ok_or(CryptoError::BadLength)?;
    if first & 0x80 != 0 {
        return Err(CryptoError::BadSignature);
    }

    let mut db = vec![0u8; DB_LEN];
    mgf1(h, &mut db);
    for (dst, src) in db.iter_mut().zip(masked_db.iter()) {
        *dst ^= *src;
    }
    if let Some(head) = db.first_mut() {
        *head &= 0x7f;
    }

    // Структура `DB`: 190 нулей, байт 0x01, 32 байта соли. Длина соли
    // ТРЕБУЕТСЯ, а не восстанавливается — см. шапку модуля.
    let (padding, tail) = db.split_at_checked(PS_LEN).ok_or(CryptoError::BadLength)?;
    if padding.iter().any(|byte| *byte != 0) {
        return Err(CryptoError::BadSignature);
    }
    let (separator, salt) = tail.split_first().ok_or(CryptoError::BadLength)?;
    if *separator != 0x01 || salt.len() != SLEN {
        return Err(CryptoError::BadSignature);
    }

    let mut hasher = Sha256::new();
    hasher.update([0u8; 8]);
    hasher.update(Sha256::digest(message));
    hasher.update(salt);
    let computed = hasher.finalize();

    let expected = <[u8; HLEN]>::try_from(computed.as_slice()).map_err(|_| CryptoError::BadLength)?;
    let found = <[u8; HLEN]>::try_from(h).map_err(|_| CryptoError::BadLength)?;

    // Сравнение постоянного времени. Здесь оно не обязательно — обе величины
    // публичны, — но доктрина сравнения дайджестов в репозитории одна на всё
    // (И-13), чтобы не приходилось каждый раз доказывать безопасность раннего
    // выхода заново.
    if crate::digest_eq(&expected, &found) {
        Ok(())
    } else {
        Err(CryptoError::BadSignature)
    }
}

// ---------------------------------------------------------------------------
// ОТКРЫТЫЕ ОПЕРАЦИИ ДЛЯ ПРОВЕРКИ АТТЕСТАЦИИ (B6a, `docs/protocol.md` §9.11).
//
// Подпись редактировавшего устройства выше прибита к RSA-2048 и PSS с солью 32:
// её производит наш подписывающий, и больше проверяющему уметь незачем. У
// аттестации подписывающие чужие — вендоры TPM и сам TPM, — и им нужно
// другое: PKCS#1 v1.5 (`sha256WithRSAEncryption` в сертификатах, RSASSA у
// ключа удостоверителя) и модули 3072 и 4096 бит у корней вендоров. Плюс одна
// операция в обратную сторону — OAEP-шифрование семени учётных данных на ключ
// подтверждения (TPM2_MakeCredential).
//
// Секрета у проверяющего здесь по-прежнему нет, кроме семени OAEP, а оно —
// ОСНОВАНИЕ, возводимое в открытую степень: время операции зависит от модуля и
// показателя, не от него. Засев OAEP приходит параметром, как всякая
// случайность в этом крейте.
// ---------------------------------------------------------------------------

/// Modulus lengths accepted by attestation verification: 2048, 3072, and 4096 bits.
///
/// Smaller lengths do not occur in TPM 2.0 and are rejected; larger ones do not occur with
/// vendors, and unbounded input would force the verifier to perform
/// arbitrary-length exponentiation.
pub const ATTESTATION_MODULUS_LENS: [usize; 3] = [256, 384, 512];

/// SHA-256 DigestInfo (RFC 8017 §9.2, note 1): the prefix before the hash.
const SHA256_DIGEST_INFO: [u8; 19] = [
    0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01,
    0x05, 0x00, 0x04, 0x20,
];

/// `x^65537 mod n` for an allowed modulus length; output is exactly modulus length.
///
/// The modulus must have FULL bit width (the first byte's highest bit set):
/// a "2048-bit" modulus with a zero high byte is a 2040-bit key under
/// the wrong length. Input must be less than the modulus, or representation
/// would be ambiguous (`x` and `x + n` would give the same result).
fn public_op(modulus: &[u8], input: &[u8]) -> Result<zeroize::Zeroizing<Vec<u8>>, CryptoError> {
    let k = modulus.len();
    if !ATTESTATION_MODULUS_LENS.contains(&k) || input.len() != k {
        return Err(CryptoError::BadLength);
    }
    if modulus.first().is_none_or(|byte| byte & 0x80 == 0) {
        return Err(CryptoError::BadLength);
    }
    let bits = u32::try_from(k.saturating_mul(8)).map_err(|_| CryptoError::BadLength)?;
    let n = BoxedUint::from_be_slice(modulus, bits).map_err(|_| CryptoError::BadLength)?;
    let x = BoxedUint::from_be_slice(input, bits).map_err(|_| CryptoError::BadLength)?;
    if x >= n {
        return Err(CryptoError::BadSignature);
    }
    let odd = Odd::new(n).into_option().ok_or(CryptoError::BadLength)?;
    let params = BoxedMontyParams::new(odd);
    let exponent = BoxedUint::from_be_slice(&EXPONENT, bits).map_err(|_| CryptoError::BadLength)?;
    let out = BoxedMontyForm::new(x, &params).pow(&exponent).retrieve().to_be_bytes();
    if out.len() != k {
        return Err(CryptoError::BadLength);
    }
    Ok(zeroize::Zeroizing::new(out.to_vec()))
}

/// Verify RSASSA-PKCS1-v1_5 with SHA-256 and exponent 65537.
///
/// The encoding is CONSTRUCTED from the message and compared with the recovered value in full,
/// not parsed. PKCS#1 v1.5 padding parsing is a classic forgery site
/// (Bleichenbacher 2006: a verifier reading `00 01 FF… 00 DigestInfo` without
/// checking the tail accepted a signature crafted for `e = 3`); comparing with
/// the sole correct encoding leaves no such opening.
///
/// # Errors
/// [`CryptoError::BadLength`]: signature length differs from modulus length or modulus
/// length is disallowed; [`CryptoError::BadSignature`]: mismatch.
pub fn verify_pkcs1v15_sha256(
    modulus: &[u8],
    message: &[u8],
    signature: &[u8],
) -> Result<(), CryptoError> {
    use subtle::ConstantTimeEq as _;
    let k = modulus.len();
    if signature.len() != k {
        return Err(CryptoError::BadLength);
    }
    let recovered = public_op(modulus, signature)?;

    let t_len = SHA256_DIGEST_INFO.len().saturating_add(HLEN);
    let pad_end = k.checked_sub(t_len).ok_or(CryptoError::BadLength)?;
    let mut expected = vec![0xffu8; k];
    let (head, tail) = expected.split_at_mut(pad_end);
    // `00 01 FF…FF 00`: первый байт ноль, второй единица, разделитель — последний
    // байт набивки.
    if let Some(first) = head.first_mut() {
        *first = 0x00;
    }
    if let Some(second) = head.get_mut(1) {
        *second = 0x01;
    }
    if let Some(separator) = head.last_mut() {
        *separator = 0x00;
    }
    let (info, digest) = tail.split_at_mut(SHA256_DIGEST_INFO.len());
    info.copy_from_slice(&SHA256_DIGEST_INFO);
    digest.copy_from_slice(&Sha256::digest(message));
    // Набивки не меньше восьми байт FF (RFC 8017 §9.2): при допустимых длинах
    // модуля это верно всегда, но пусть будет сказано кодом.
    if pad_end < 11 {
        return Err(CryptoError::BadLength);
    }

    if bool::from(recovered.as_slice().ct_eq(expected.as_slice())) {
        Ok(())
    } else {
        Err(CryptoError::BadSignature)
    }
}

/// Encrypt `message` using RSAES-OAEP with SHA-256 and MGF1-SHA-256 (RFC 8017 §7.1.1).
///
/// `seed` is 32 random bytes from the caller. The TPM credential label is
/// `b"IDENTITY\0"`, including its terminating zero (TPM 2.0, part 1, "Credential
/// Protection"); it must be supplied in full; this function does not append to it.
///
/// # Errors
/// [`CryptoError::BadLength`]: disallowed modulus length or a message exceeding
/// `k − 2·32 − 2`.
pub fn encrypt_oaep_sha256(
    modulus: &[u8],
    label: &[u8],
    message: &[u8],
    seed: &[u8; HLEN],
) -> Result<Vec<u8>, CryptoError> {
    let k = modulus.len();
    if !ATTESTATION_MODULUS_LENS.contains(&k) {
        return Err(CryptoError::BadLength);
    }
    let room = k.checked_sub(HLEN.saturating_mul(2).saturating_add(2)).ok_or(CryptoError::BadLength)?;
    if message.len() > room {
        return Err(CryptoError::BadLength);
    }
    let db_len = k.saturating_sub(HLEN).saturating_sub(1);

    // DB = lHash ‖ PS (нули) ‖ 0x01 ‖ M. Буферы фиксированной длины и
    // затирающиеся: в них сообщение и семя (И-11).
    let mut db = zeroize::Zeroizing::new(vec![0u8; db_len]);
    let (l_hash, rest) = db.split_at_mut(HLEN);
    l_hash.copy_from_slice(&Sha256::digest(label));
    let marker_at = rest.len().checked_sub(message.len().saturating_add(1)).ok_or(CryptoError::BadLength)?;
    let (_, tail) = rest.split_at_mut(marker_at);
    let (marker, body) = tail.split_first_mut().ok_or(CryptoError::BadLength)?;
    *marker = 0x01;
    body.copy_from_slice(message);

    let mut db_mask = zeroize::Zeroizing::new(vec![0u8; db_len]);
    mgf1(seed, &mut db_mask);
    for (byte, mask) in db.iter_mut().zip(db_mask.iter()) {
        *byte ^= *mask;
    }
    let mut seed_mask = zeroize::Zeroizing::new([0u8; HLEN]);
    mgf1(&db, seed_mask.as_mut_slice());
    let mut masked_seed = zeroize::Zeroizing::new(*seed);
    for (byte, mask) in masked_seed.iter_mut().zip(seed_mask.iter()) {
        *byte ^= *mask;
    }

    let mut em = zeroize::Zeroizing::new(vec![0u8; k]);
    let (zero_and_seed, masked_db) = em.split_at_mut(HLEN.saturating_add(1));
    let (_, seed_slot) = zero_and_seed.split_at_mut(1);
    seed_slot.copy_from_slice(masked_seed.as_slice());
    masked_db.copy_from_slice(&db);

    Ok(public_op(modulus, &em)?.to_vec())
}

#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::indexing_slicing)]
mod tests {
    use super::*;

    /// Derived lengths must match the PSS layout rather than being
    /// manually copied numbers.
    #[test]
    fn the_derived_lengths_match_the_pss_layout() {
        assert_eq!(DB_LEN, MODULUS_LEN - HLEN - 1);
        assert_eq!(PS_LEN, MODULUS_LEN - SLEN - HLEN - 2);
        assert_eq!(PS_LEN + 1 + SLEN, DB_LEN);
    }

    #[test]
    fn a_signature_of_the_wrong_length_is_refused_by_length_not_by_verdict() {
        let modulus = [0xffu8; MODULUS_LEN];
        assert_eq!(
            verify_pss_sha256(&modulus, b"x", &[0u8; MODULUS_LEN - 1]),
            Err(CryptoError::BadLength)
        );
        assert_eq!(
            verify_pss_sha256(&modulus, b"x", &[0u8; MODULUS_LEN + 1]),
            Err(CryptoError::BadLength)
        );
    }

    /// An even modulus is garbage in place of a key, not a weak key.
    #[test]
    fn an_even_modulus_is_refused() {
        let mut modulus = [0xffu8; MODULUS_LEN];
        modulus[MODULUS_LEN - 1] = 0xfe;
        let signature = [0u8; MODULUS_LEN];
        assert_eq!(verify_pss_sha256(&modulus, b"x", &signature), Err(CryptoError::BadLength));
    }

    /// Incorrect modulus length or incomplete bit width causes a length rejection.
    #[test]
    fn attestation_moduli_are_limited_to_full_width_known_lengths() {
        let short = [0xffu8; 128];
        assert_eq!(verify_pkcs1v15_sha256(&short, b"x", &[0u8; 128]), Err(CryptoError::BadLength));
        let mut hollow = [0xffu8; 256];
        hollow[0] = 0x7f;
        assert_eq!(verify_pkcs1v15_sha256(&hollow, b"x", &[0u8; 256]), Err(CryptoError::BadLength));
        assert_eq!(encrypt_oaep_sha256(&short, b"", b"x", &[0u8; 32]), Err(CryptoError::BadLength));
        // Сообщение длиннее места OAEP — отказ, а не усечение.
        let modulus = [0xffu8; 256];
        assert_eq!(
            encrypt_oaep_sha256(&modulus, b"", &[0u8; 256 - 64 - 1], &[0u8; 32]),
            Err(CryptoError::BadLength)
        );
    }

    /// A signature not smaller than the modulus is rejected: representation would be ambiguous.
    #[test]
    fn a_pkcs1_signature_not_below_the_modulus_is_refused() {
        let modulus = [0xffu8; 256];
        assert_eq!(
            verify_pkcs1v15_sha256(&modulus, b"x", &[0xffu8; 256]),
            Err(CryptoError::BadSignature)
        );
    }

    /// A zero signature must fail for every modulus: `0^e = 0`, and
    /// the encoding's trailing byte will not match.
    #[test]
    fn a_zero_signature_never_verifies() {
        let modulus = [0xffu8; MODULUS_LEN];
        assert_eq!(
            verify_pss_sha256(&modulus, b"x", &[0u8; MODULUS_LEN]),
            Err(CryptoError::BadSignature)
        );
    }
}