Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pkcs12: KDF support #1154

Merged
merged 22 commits into from
Jul 23, 2023
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions pkcs12/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@ readme = "README.md"
edition = "2021"
rust-version = "1.65"

[dependencies]
cfg-if = "1.0.0"
xemwebe marked this conversation as resolved.
Show resolved Hide resolved
digest = { version = "0.10.7", features=["alloc"] }
zeroize = "1.6.0"

[dev-dependencies]
hex-literal = "0.4"
sha2 = "0.10.7"
whirlpool = "0.10.4"

[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]

108 changes: 108 additions & 0 deletions pkcs12/src/kdf.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
//! Implementation of the key derivation function
//! [RFC 7292 Appendix B](https://datatracker.ietf.org/doc/html/rfc7292#appendix-B)

use alloc::{vec, vec::Vec};
use digest::{core_api::BlockSizeUser, Digest, FixedOutputReset, OutputSizeUser, Update};
use zeroize::Zeroize;

/// Transform a utf-8 string in a unicode (utf16) string as binary array.
/// The Utf16 code points are stored in big endian format with two trailing zero bytes.
fn str_to_unicode(utf8_str: &str) -> Vec<u8> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The RFC talks about BMPString which happens to be UTF16, but renaming that str_to_bmpstring or something would be great I think.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I guess we should really add BMPString support to der

Copy link
Member

@baloo baloo Jul 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit uncomfortable with the use of str for inputs here. This will backfire with any 4-Byte UTF8 character. https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=c4edb02a6d7bf8fb45147475e43f1930

I don't know what the best course of action is here though, the best I can think of would be to add a der::asn1::BmpString but it is meant to only be subtyped from what I can read (https://www.oss.com/asn1/resources/asn1-made-simple/asn1-quick-reference/bmpstring.html)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(sorry last comment was sent before I refreshed the page, well, go for a BmpString then)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The format of an ASN1 BMPString differs slightly from what is here required: It has a leading id byte 0x1e, one or bytes indicating the length in bytes, and no terminating zeros. I.e. using BMPString would also requires some pre-processing. Multi-Byte UTF8 characters should work fine here, at least the example you provided gives the exact same result als openssl's function PKCS12_gen_key_utf8 (see the added test case and the respective test program).

Openssl provides similar functions for PKCS12_gen_key_uni and PCKS12_gen_asc for unicode and ASCII string passwords (see here: https://www.openssl.org/docs/man3.0/man3/PKCS12_key_gen_utf8.html) Maybe a similar approach could be taken here.

However, I am not sure how to interpret the remark "There are no Unicode byte order marks." in the RFC.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

last commit does not really fix the issue with unicode characters that do not encode on two bytes.
RFC 7292 does not really specify the behavior with those.
And I would be in favor of outright rejecting them (or maybe hide a From<Vec<u8>> under hazmat feature flag for a der::asn1::BmpString).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, I am not sure how to interpret the remark "There are no Unicode byte order marks." in the RFC.

https://en.wikipedia.org/wiki/Byte_order_mark

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A BOM would precede the string, and if it had one in this case would be FEFF to indicate big endian.

BmpString (if it existed) would still be a nice place to handle all of the string conversions. The header doesn't matter: we can still use EncodeValue to avoid it, and two trailing digest bytes are easily added to the Digest prior to finalization.

We can worry about that all later though. It isn't needed for an initial PR.

let mut utf16_bytes = Vec::new();
// reserve max number of required bytes to avoid re-allocation
utf16_bytes.reserve(utf8_str.len() * 2 + 2);
xemwebe marked this conversation as resolved.
Show resolved Hide resolved
for code_point in utf8_str.encode_utf16().chain(Some(0)) {
utf16_bytes.extend(code_point.to_be_bytes());
}
utf16_bytes
}

/// Specify the usage type of the generated key
/// This allows to derive distinct encryption keys, IVs and MAC from the same password or text
/// string.
pub enum Pkcs12KeyType {
/// Use key for encryption
EncryptionKey = 1,
/// Use key as initial vector
Iv = 2,
/// Use key as MAC
Mac = 3,
}

/// Derives `key` of type `id` from `pass` and `salt` with length `key_len` using `rounds`
/// iterations of the algorithm
/// ```rust
/// let key = pkcs12::kdf::derive_key::<sha2::Sha256>("top-secret", &[0x1, 0x2, 0x3, 0x4],
/// pkcs12::kdf::Pkcs12KeyType::EncryptionKey, 1000, 32);
/// ```
pub fn derive_key<D>(
pass: &str,
salt: &[u8],
id: Pkcs12KeyType,
rounds: i32,
key_len: usize,
) -> Vec<u8>
where
D: Digest + FixedOutputReset + BlockSizeUser,
{
let mut digest = D::new();
let mut pass_utf16 = str_to_unicode(pass);
let output_size = <D as OutputSizeUser>::output_size();
let block_size = D::block_size();
let slen = block_size * ((salt.len() + block_size - 1) / block_size);
let plen = block_size * ((pass_utf16.len() + block_size - 1) / block_size);
let ilen = slen + plen;
let mut init_key = vec![0u8; ilen];
for i in 0..slen {
init_key[i] = salt[i % salt.len()];
}
for i in 0..plen {
init_key[slen + i] = pass_utf16[i % pass_utf16.len()];
}
pass_utf16.zeroize();
Copy link
Member

@baloo baloo Jul 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like step2,3 & 4

   2.  Concatenate copies of the salt together to create a string S of
       length v(ceiling(s/v)) bits (the final copy of the salt may be
       truncated to create S).  Note that if the salt is the empty
       string, then so is S.

   3.  Concatenate copies of the password together to create a string P
       of length v(ceiling(p/v)) bits (the final copy of the password
       may be truncated to create P).  Note that if the password is the
       empty string, then so is P.
   
   4.  Set I=S||P to be the concatenation of S and P.

Could you move code around (to preserve ordering) and copy the algorithm from the RFC as comments?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added comments to the code describing the single steps of the implementation, as far as possible since I derive at some point slightly from the algorithm.


let id_block = match id {
Pkcs12KeyType::EncryptionKey => vec![1u8; block_size],
Pkcs12KeyType::Iv => vec![2u8; block_size],
Pkcs12KeyType::Mac => vec![3u8; block_size],
baloo marked this conversation as resolved.
Show resolved Hide resolved
};

let mut m = key_len;
let mut n = 0;
let mut out = vec![0u8; key_len];
loop {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RFC calls for a c = ceiling(n/u) and for the loop to be:

for _i in 1..=c {
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a loop instead has the advantage that the last step 6. could be skipped.

<D as Update>::update(&mut digest, &id_block);
<D as Update>::update(&mut digest, &init_key);
let mut result = digest.finalize_fixed_reset();
for _ in 1..rounds {
<D as Update>::update(&mut digest, &result[0..output_size]);
result = digest.finalize_fixed_reset();
}
let new_bytes_num = m.min(output_size);
out[n..n + new_bytes_num].copy_from_slice(&result[0..new_bytes_num]);
n += new_bytes_num;
if m <= new_bytes_num {
break;
}

// prepare `init_key` for next block if `ouput_size` is smaller than `key_len`
m -= new_bytes_num;
let mut j = 0;
while j < ilen {
let mut c = 1_u16;
let mut k = block_size - 1;
loop {
c += init_key[k + j] as u16 + result[k % output_size] as u16;
init_key[j + k] = (c & 0x00ff) as u8;
c >>= 8;
if k == 0 {
break;
}
k -= 1;
}
j += block_size;
}
}
init_key.zeroize();
out
}
6 changes: 5 additions & 1 deletion pkcs12/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,8 @@
unused_qualifications
)]

//! TODO: PKCS#12 crate
//! TODO: complete PKCS#12 crate

extern crate alloc;

pub mod kdf;
141 changes: 141 additions & 0 deletions pkcs12/tests/kdf.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/// Test cases for the key derivation functions.
/// All test cases have been verified against openssl's method `PKCS12_key_gen_utf8`.
/// See https://github.com/xemwebe/test_pkcs12_kdf for a sample program.
///

use hex_literal::hex;
use pkcs12::kdf::{derive_key, Pkcs12KeyType};

#[test]
fn pkcs12_key_derive_sha256() {
const PASS_SHORT: &str = "ge@äheim";
const SALT_INC: [u8; 8] = [0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8];

assert_eq!(
derive_key::<sha2::Sha256>(PASS_SHORT, &SALT_INC, Pkcs12KeyType::EncryptionKey, 100, 32),
hex!("fae4d4957a3cc781e1180b9d4fb79c1e0c8579b746a3177e5b0768a3118bf863")
);

assert_eq!(
derive_key::<sha2::Sha256>(PASS_SHORT, &SALT_INC, Pkcs12KeyType::Iv, 100, 32),
hex!("e5ff813bc6547de5155b14d2fada85b3201a977349db6e26ccc998d9e8f83d6c")
);

assert_eq!(
derive_key::<sha2::Sha256>(PASS_SHORT, &SALT_INC, Pkcs12KeyType::Mac, 100, 32),
hex!("136355ed9434516682534f46d63956db5ff06b844702c2c1f3b46321e2524a4d")
);

assert_eq!(
derive_key::<sha2::Sha256>(PASS_SHORT, &SALT_INC, Pkcs12KeyType::EncryptionKey, 100, 20),
hex!("fae4d4957a3cc781e1180b9d4fb79c1e0c8579b7")
);

assert_eq!(
derive_key::<sha2::Sha256>(PASS_SHORT, &SALT_INC, Pkcs12KeyType::Iv, 100, 20),
hex!("e5ff813bc6547de5155b14d2fada85b3201a9773")
);

assert_eq!(
derive_key::<sha2::Sha256>(PASS_SHORT, &SALT_INC, Pkcs12KeyType::Mac, 100, 20),
hex!("136355ed9434516682534f46d63956db5ff06b84")
);

assert_eq!(
derive_key::<sha2::Sha256>(PASS_SHORT, &SALT_INC, Pkcs12KeyType::EncryptionKey, 100, 12),
hex!("fae4d4957a3cc781e1180b9d")
);

assert_eq!(
derive_key::<sha2::Sha256>(PASS_SHORT, &SALT_INC, Pkcs12KeyType::Iv, 100, 12),
hex!("e5ff813bc6547de5155b14d2")
);

assert_eq!(
derive_key::<sha2::Sha256>(PASS_SHORT, &SALT_INC, Pkcs12KeyType::Mac, 100, 12),
hex!("136355ed9434516682534f46")
);

assert_eq!(
derive_key::<sha2::Sha256>(
PASS_SHORT,
&SALT_INC,
Pkcs12KeyType::EncryptionKey,
1000,
32
),
hex!("2b95a0569b63f641fae1efca32e84db3699ab74540628ba66283b58cf5400527")
);

assert_eq!(
derive_key::<sha2::Sha256>(PASS_SHORT, &SALT_INC, Pkcs12KeyType::Iv, 1000, 32),
hex!("6472c0ebad3fab4123e8b5ed7834de21eeb20187b3eff78a7d1cdffa4034851d")
);

assert_eq!(
derive_key::<sha2::Sha256>(PASS_SHORT, &SALT_INC, Pkcs12KeyType::Mac, 1000, 32),
hex!("3f9113f05c30a996c4a516409bdac9d065f44296ccd52bb75de3fcfdbe2bf130")
);

assert_eq!(
derive_key::<sha2::Sha256>(PASS_SHORT, &SALT_INC, Pkcs12KeyType::Mac, 1000, 32),
hex!("3f9113f05c30a996c4a516409bdac9d065f44296ccd52bb75de3fcfdbe2bf130")
);

assert_eq!(
derive_key::<sha2::Sha256>(PASS_SHORT, &SALT_INC, Pkcs12KeyType::EncryptionKey, 1000, 100),
hex!("2b95a0569b63f641fae1efca32e84db3699ab74540628ba66283b58cf5400527d8d0ebe2ccbf768c51c4d8fbd1bb156be06c1c59cbb69e44052ffc37376fdb47b2de7f9e543de9d096d8e5474b220410ff1c5d8bb7e5bc0f61baeaa12fd0da1d7a970172")
);

assert_eq!(
derive_key::<sha2::Sha256>(PASS_SHORT, &SALT_INC, Pkcs12KeyType::EncryptionKey, 1000, 200),
hex!("2b95a0569b63f641fae1efca32e84db3699ab74540628ba66283b58cf5400527d8d0ebe2ccbf768c51c4d8fbd1bb156be06c1c59cbb69e44052ffc37376fdb47b2de7f9e543de9d096d8e5474b220410ff1c5d8bb7e5bc0f61baeaa12fd0da1d7a9701729cea6014d7fe62a2ed926dc36b61307f119d64edbceb5a9c58133bbf75ba0bef000a1a5180e4b1de7d89c89528bcb7899a1e46fd4da0d9de8f8e65e8d0d775e33d1247e76d596a34303161b219f39afda448bf518a2835fc5e28f0b55a1b6137a2c70cf7")
);
}

#[test]
fn pkcs12_key_derive_sha512() {
use hex_literal::hex;
use pkcs12::kdf::{derive_key, Pkcs12KeyType};

const PASS_SHORT: &str = "ge@äheim";
const SALT_INC: [u8; 8] = [0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8];

assert_eq!(
derive_key::<sha2::Sha512>(PASS_SHORT, &SALT_INC, Pkcs12KeyType::EncryptionKey, 100, 32),
hex!("b14a9f01bfd9dce4c9d66d2fe9937e5fd9f1afa59e370a6fa4fc81c1cc8ec8ee")
);
}

#[test]
fn pkcs12_key_derive_whirlpool() {
use hex_literal::hex;
use pkcs12::kdf::{derive_key, Pkcs12KeyType};

const PASS_SHORT: &str = "ge@äheim";
const SALT_INC: [u8; 8] = [0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8];

assert_eq!(
derive_key::<whirlpool::Whirlpool>(
PASS_SHORT,
&SALT_INC,
Pkcs12KeyType::EncryptionKey,
100,
32
),
hex!("3324282adb468bff0734d3b7e399094ec8500cb5b0a3604055da107577aaf766")
);
}

#[test]
fn pkcs12_key_derive_special_chars() {
const PASS_SHORT: &str = "🔥";
const SALT_INC: [u8; 8] = [0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8];

assert_eq!(
derive_key::<sha2::Sha256>(PASS_SHORT, &SALT_INC, Pkcs12KeyType::EncryptionKey, 100, 32),
hex!("d01e72a940b4b1a7a5707fc8264a60cb7606ff9051dedff90930687d2513c006")
);
}