Conversation
src/rust/src/backend/mldsa.rs
Outdated
| // Note: private_key_to_pkcs8() (i2d_PKCS8PrivateKey_bio) must be used | ||
| // instead of private_key_to_der() (i2d_PrivateKey), because AWS-LC's | ||
| // i2d_PrivateKey doesn't support PQDSA keys. | ||
| let pkcs8_der = self.pkey.private_key_to_pkcs8()?; |
There was a problem hiding this comment.
FLAG: This is not elegant here - but moving this to another layer (like the cryptography-openssl crate would induce a new dependency on asn1)
|
Can you take a look at the missing coverage before I review? (If there's questions about idioms for covering certain things, let me know) |
49220b3 to
4f4bc0b
Compare
4f4bc0b to
65ab941
Compare
|
@DarkaMaul any reason you were not basing on my branches? |
|
@abbra -- please do not nag contributors about this. As I last checked, your branch is far too large to be reviewable, is targeting OpenSSL rather than the backends we intend to start with, and you have not submitted any of it for review as a PR. In contrast, this pull request is moving towards a mergable state, so attempting to rebase it on top of your work would be a hinderance, rather than a help. |
|
@alex I have splitted the code into three different branches: for ML-DSA-44, ML-DSA-65, ML-DSA-87, as you and Paul did request. I haven't submitted any of them individually because you asked not to submit them without non-OpenSSL backend. I'd like to at least have higher level API agreed. Do you want me to submit those branches now? |
|
@abbra It’s great that you split them, but you didn’t submit anything to us for review. Our development happens in the open on this issue tracker and at this time other contributors have stepped forward and have submitted things we’re actively reviewing. Their work is going to take priority, but hopefully you’ll be able to rebase on top of them to submit going forward. |
|
@reaperhulk looks like we misunderstood each other then. |
alex
left a comment
There was a problem hiding this comment.
if we're adding new vectors they need to be documented in test-vectors.rst
| def private_bytes_raw(self) -> bytes: | ||
| """ | ||
| The raw bytes of the private key (32-byte seed). | ||
| Equivalent to private_bytes(Raw, Raw, NoEncryption()). |
There was a problem hiding this comment.
(replied about this one in public_bytes_raw comment)
There was a problem hiding this comment.
I wonder if we should be using the noun seed here, rather than raw -- wdyt @reaperhulk ?
There was a problem hiding this comment.
Hmm, well a seed only key can have two forms. The raw/bare seed or one wrapped in a PKCS8 structure. This parses the raw seed so I think I'm fine with it being called raw?
| fn test_serialize_public_key_unsupported_mldsa_variant() { | ||
| // Load an ML-DSA-44 public key from a Wycheproof test vector DER. | ||
| let der = include_bytes!( | ||
| "../../../../vectors/cryptography_vectors/asymmetric/MLDSA/mldsa44_pub.der" |
There was a problem hiding this comment.
Tests like this should go through the Python interface -- we generally only do rust tests if the code is actually unreachable from python
There was a problem hiding this comment.
That's actually unreachable from Python at the moment because we can't construct an ML-DSA-44 key object in the Python layer. The serialize_public_key path is only called from public_bytes() on an MlDsa65PublicKey, and all constructors that produce that type validate the key size upfront.
| def private_bytes_raw(self) -> bytes: | ||
| """ | ||
| The raw bytes of the private key (32-byte seed). | ||
| Equivalent to private_bytes(Raw, Raw, NoEncryption()). |
There was a problem hiding this comment.
I wonder if we should be using the noun seed here, rather than raw -- wdyt @reaperhulk ?
| } | ||
|
|
||
| #[cfg(CRYPTOGRAPHY_IS_AWSLC)] | ||
| AlgorithmParameters::MlDsa65 => parse_mldsa_private_key(k.private_key), |
There was a problem hiding this comment.
I think we need a length check here? Or is something lower in the stack verifying that the length matches the mldsa type?
There was a problem hiding this comment.
What about open-coding the parsing? Check if the length is correct and that the prefix is the expected value. Then return the suffix.
There was a problem hiding this comment.
There is a length check deeper in the chain:
// Get PQDSA instance and validate lengths
const PQDSA *pqdsa = PQDSA_KEY_get0_dsa(ret->pkey.pqdsa_key);
if (len != pqdsa->private_key_len && len != pqdsa->keygen_seed_len) {
OPENSSL_PUT_ERROR(EVP, EVP_R_INVALID_BUFFER_SIZE);
goto err;
}I have written the following test to confirm it.
#[cfg(CRYPTOGRAPHY_IS_AWSLC)]
#[test]
fn test_parse_mldsa_private_key_wrong_size() {
let bad_seed = asn1::write_single(&super::MlDsaPrivateKey::Seed(&[0u8; 33])).unwrap();
assert!(super::parse_mldsa_private_key(&bad_seed).is_err());
}
src/rust/src/backend/keys.rs
Outdated
| .into_any()), | ||
|
|
||
| #[cfg(CRYPTOGRAPHY_IS_AWSLC)] | ||
| id if id == openssl::pkey::Id::from_raw(cryptography_openssl::mldsa::NID_PQDSA) => { |
tests/wycheproof/test_mldsa.py
Outdated
| ) | ||
| @wycheproof_tests("mldsa_65_sign_seed_test.json") | ||
| def test_mldsa65_sign_seed(backend, wycheproof): | ||
| # Skip "Internal" tests |
There was a problem hiding this comment.
can you expand this comment very slightly.
|
This is making good progress, I think we're close. |
# Conflicts: # vectors/cryptography_vectors/asymmetric/MLDSA/mldsa65_noseed_priv.der
0bf9bf9 to
c56210c
Compare
| self, | ||
| encoding: _serialization.Encoding, | ||
| format: _serialization.PrivateFormat, | ||
| encryption_algorithm: (_serialization.KeySerializationEncryption), |
There was a problem hiding this comment.
| encryption_algorithm: (_serialization.KeySerializationEncryption), | |
| encryption_algorithm: _serialization.KeySerializationEncryption, |
| def private_bytes_raw(self) -> bytes: | ||
| """ | ||
| The raw bytes of the private key (32-byte seed). | ||
| Equivalent to private_bytes(Raw, Raw, NoEncryption()). |
| #[cfg(CRYPTOGRAPHY_IS_AWSLC)] | ||
| // NO-COVERAGE-START | ||
| #[derive(asn1::Asn1Read, asn1::Asn1Write)] | ||
| // NO-COVERAGE-END |
There was a problem hiding this comment.
We've never needed these markers on an asn1derive, strongly suggests there's some relevant path that we're never using -- do we not use both read/write on this type?
There was a problem hiding this comment.
We do both use the read and write operation here, but the macro generates more code there.
I used cargo expand on the macro to generate a new coverage run on the expanded version:
The CI fails with the following missing lines:
src/rust/cryptography-key-parsing/src/pkcs8.rs 383 14 0 0 96% 39-40, 48-53, 79-84
- L39-40: this could be exercised by having a test that calls
asn1::write_single(&MlDsaPrivateKey::Seed(wrong_seed))but in practice, that would mean theraw_private_keyreturned an invalid seed, and that should not be doable.
.map_err(|e| {
e.add_location(asn1::ParseLocation::Field("MlDsaPrivateKey::Seed"))
})?- L48-53: not sure how to test this
fn can_parse(tag: asn1::Tag) -> bool {
if asn1::Implicit::<&'a [u8], 0>::can_parse(tag) {
return true;
}
false
}- L79-84: same here, we are using the impl for
MlDsaPrivateKeynot&MlDsaPrivateKey
impl<'a> asn1::Asn1Writable for &MlDsaPrivateKey<'a> {
type Error = asn1::WriteError;
fn write(&self, w: &mut asn1::Writer<'_>) -> asn1::WriteResult {
(*self).write(w)
}
fn encoded_length(&self) -> Option<usize> {
(*self).encoded_length()
}
}(source)
What's the best/recommended practice here?
There was a problem hiding this comment.
Huh... I am not sure. I'm baffled how we've never run into these with any other enums before...
There was a problem hiding this comment.
Not required anymore thanks to our new runner!
tests/wycheproof/test_mldsa.py
Outdated
| ) | ||
| @wycheproof_tests("mldsa_65_sign_seed_test.json") | ||
| def test_mldsa65_sign_seed(backend, wycheproof): | ||
| # Skip "Internal" tests |
There was a problem hiding this comment.
can you expand this comment very slightly.
| #[cfg(CRYPTOGRAPHY_IS_AWSLC)] | ||
| // NO-COVERAGE-START | ||
| #[derive(asn1::Asn1Read, asn1::Asn1Write)] | ||
| // NO-COVERAGE-END |
There was a problem hiding this comment.
Huh... I am not sure. I'm baffled how we've never run into these with any other enums before...
| // NO-COVERAGE-END | ||
| pub enum MlDsaPrivateKey<'a> { | ||
| #[implicit(0)] | ||
| Seed(&'a [u8]), |
There was a problem hiding this comment.
Should this be a [u8; 32]? the RFC defines it as SIZE 32.
There was a problem hiding this comment.
Yes! And narrowing it surfaced a bug because this was happily serializing the expanded private key instead of the seed.
I've also modified the test_round_trip_private_serialization to add the missing equality...
src/rust/src/backend/mldsa.rs
Outdated
| &self, | ||
| py: pyo3::Python<'p>, | ||
| ) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> { | ||
| // AWS-LC's raw_private_key() returns the expanded key, not the seed. |
There was a problem hiding this comment.
Is this still true? IIUC this was changed.
| encryption_algorithm: &pyo3::Bound<'p, pyo3::PyAny>, | ||
| ) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> { | ||
| // Intercept Raw/Raw/NoEncryption so we return the seed. | ||
| // The generic pkey_private_bytes raw path calls raw_private_key() |
7a1087e to
1f1303a
Compare
This PR adds ML-DSA-65 support when using the AWS-LC backend.
I know this PR is quite massive (+1000 lines), but I've already excluded the documentation from it. I can split it even further by:
mldsa_supported() -> bool = Falseso the test suite continue to pass (would be about 500 lines)I could also try to remove the x509/pkcs8 support, but in my tests, that led to a weird API state where some functions would not work.
I'm also happy to also split it in any other potential better ways if you see any.