Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Addition of generic NIST-DSA PKEY and ASN1 to support ML-DSA (#1963)
#### 1. PKEY structure changes This PR adds the support to include additional signature algorithms (in this case ML-DSA-44 and ML-DSA-87) to AWS-LC. Before this PR AWS-LC only supported ML-DSA-65, as such, we were utilizing the `void *ptr` of `evp_pkey_st`, rather than a distinct structure for ML-DSA. This PR introduces the new structure `PQDSA_KEY * pqdsa_key;` inside `evp_pkey_st` to support ML-DSA and any additional FIPS digital signature algorithms provided by NIST, should AWS-LC wish to include them in the library. ``` union { void *ptr; RSA *rsa; DSA *dsa; DH *dh; EC_KEY *ec; KEM_KEY *kem_key; PQDSA_KEY * pqdsa_key; } pkey; ``` While the structure `DSA` [already exists](https://github.com/jakemas/aws-lc/blob/5d49dfe3c651925da7fd97ec6413f9e3bbd28508/crypto/dsa/internal.h) it does not provide support for FIPS-like signature APIs (see more at [NIST api conventions](https://csrc.nist.gov/CSRC/media/Projects/Post-Quantum-Cryptography/documents/example-files/api-notes.pdf). Rather than create a PKEY struct of the form `MLDSA_KEY` that can only support ML-DSA, this is an opportunity to build support for future signature algorithms that have similar. calling structures, de-randomized testing modes, API converntions, etc. by making a more generic structure type -- much like we did for `KEM_KEY`. Under the design in this PR, adding SLH-DSA to the PKEY struct would be very simple, as it conforms to a `PQDSA_KEY` in design. So too will be true of any additional signature algorithms produced by NISTs call for [additional signature algorithms](https://csrc.nist.gov/projects/pqc-dig-sig). As, such, `PQDSA_KEY` utilizes a new structure to hold public/secret keys, as well as a `PQDSA` struct which defines signature algorithm specific information: ``` struct pqdsa_key_st { const PQDSA *pqdsa; uint8_t *public_key; uint8_t *secret_key; }; ``` ``` typedef struct { int nid; const uint8_t *oid; uint8_t oid_len; const char *comment; size_t public_key_len; size_t secret_key_len; size_t signature_len; size_t keygen_seed_len; size_t sign_seed_len; const PQDSA_METHOD *method; } PQDSA; ``` This allows us to use a single PKEY structure for all NIST FIPS signature algorithms, much like the existing PKEY struct `KEM_KEY *kem_key` for KEMS. As a consequence, we are now able to define signature methods such as: ``` DEFINE_LOCAL_DATA(PQDSA_METHOD, sig_ml_dsa_65_method) { out->keygen = ml_dsa_65_keypair; out->sign = ml_dsa_65_sign; out->verify = ml_dsa_65_verify; } DEFINE_LOCAL_DATA(PQDSA, sig_ml_dsa_65) { out->nid = NID_MLDSA65; out->oid = kOIDMLDSA65; out->oid_len = sizeof(kOIDMLDSA65); out->comment = "MLDSA65 "; out->public_key_len = MLDSA65_PUBLIC_KEY_BYTES; out->secret_key_len = MLDSA65_PRIVATE_KEY_BYTES; out->signature_len = MLDSA65_SIGNATURE_BYTES; out->keygen_seed_len = MLDSA65_KEYGEN_SEED_BYTES; out->sign_seed_len = MLDSA65_SIGNATURE_SEED_BYTES; out->method = sig_ml_dsa_65_method(); } ``` much like the existing [kem.c](https://github.com/jakemas/aws-lc/blob/e4092fb224a2336b8e0d908f311924794d739042/crypto/fipsmodule/kem/kem.c) file. These structures will be incredibly useful for subsequent PRs, in which we will be adding de-randomized APIs to support FIPS validation and KATs from seeds. In effect, we will be extending the method to include: ``` DEFINE_LOCAL_DATA(PQDSA_METHOD, sig_ml_dsa_65_method) { out->keygen = ml_dsa_65_keypair; out->keygen_internal = ml_dsa_65_keypair_internal; out->sign = ml_dsa_65_sign; out->sign_internal = ml_dsa_65_sign_internal; out->verify = ml_dsa_65_verify; } ``` The directory `dilithium/p_dilithium3.c` is used to house generic `PKEY` operations, as well as PQDSA specific EVP functions such as `EVP_PKEY_pqdsa_set_params`, `EVP_PKEY_pqdsa_set_params`, which work in a similar way to [p_kem.c](https://github.com/jakemas/aws-lc/blob/3f03f7d7b2eccca69f278feaeb073ab9a5540c61/crypto/fipsmodule/evp/p_kem.c) functions `EVP_PKEY_CTX_kem_set_params` and `EVP_PKEY_kem_set_params`. The directory `dilithium/p_dilithium3_asn1.c` is used to house generic `asn.1` operations for NIST FIPS DSA algorithms. Rather than rename/relocate these files in this PR (which will convolute the CR), I will move these files in a subsequent PR. #### 2. Internal Design ![pqdsa1](https://github.com/user-attachments/assets/1c771c69-f96a-49fb-98d1-e8d51d5eff36) We introduce new structures: `PQDSA_METHOD`, `PQDSA`, `PQDSA_KEY` which are very similar to how both EC Crypto, and the KEM API is implemented. The figure above shows the newly proposed structures and their integration into the existing EVP structures. - `PQDSA_METHOD` is a table of function pointers with 5 functions defined for a `PQDSA`: key generation, key generation internal (for testing/FIPS), sign, sign internal (for testing/FIPS), and verify. Every `PQDSA` implementation has to implement this API, for example `sig_ml_dsa_44_method` implements the three functions for ML-DSA-44 (analogous to `EC_METHOD` and for example, `EC_GFp_nistz256_method`). - `PQDSA` is a structure that holds basic information about the DSA: the id, size of parameters, and the pointer to the implementation of the corresponding `PQDSA_METHOD`. - `PQDSA_KEY` structure is a helper structure that holds pointers to public and secret key and the pointer to the corresponding `PQDSA`. - `PQDSA_PKEY_CTX` is a helper structure used to store DSA parameters in an `EVP_PKEY_CTX` object (the same as `EC_PKEY_CTX`). Since `PQDSA` has everything we need, that’s what we store in `PQDSA_PKEY_CTX`. #### 3. OID changes We now have real OIDs available from https://csrc.nist.gov/projects/computer-security-objects-register/algorithm-registration. These have been added to the `obj` files and automatically populated by `go run objects.go`. We include the following NIDs for ML-DSA: ``` //2.16.840.1.101.3.4.3.17 static const uint8_t kOIDMLDSA44[] = {0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x11}; //2.16.840.1.101.3.4.3.18 static const uint8_t kOIDMLDSA65[] = {0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x12}; //2.16.840.1.101.3.4.3.19 static const uint8_t kOIDMLDSA87[] = {0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x13}; ``` and a "top-level" NID `EVP_PKEY_NISTDSA` (similar to [EVP_PKEY_KEM](https://github.com/jakemas/aws-lc/blob/c82a6f9b1bdc7ec0abf7571c92c64db91959428d/include/openssl/evp.h#L210) to subset all NIST FIPS DSA algorithms. Much like for the KEM API, we include functions `SIG_find_dsa_by_nid(int nid)` to return actual algorithm specific methods. #### 4. Testing structure changes Prior to this PR all testing for ML-DSA-65 was done as a series of g-tests. As we now have the ability to get algorithm/security level specific parameters from the `NISTDSA` struct, I have overhauled the testing suite to be parameterized over the currently supported signature algorithms: ``` static const struct ML_DSA parameterSet[] = { {"MLDSA65", NID_MLDSA65, 1952, 4032, 3309, "dilithium/kat/mldsa65.txt", mldsa65kPublicKey, mldsa65kPublicKeySPKI, 1973}, }; ``` For each of the above parameter sets we run: ``` TEST_P(MLDSAParameterTest, KAT) TEST_P(MLDSAParameterTest, KeyGen) TEST_P(MLDSAParameterTest, KeyCmp) TEST_P(MLDSAParameterTest, KeySize) TEST_P(MLDSAParameterTest, NewKeyFromBytes) TEST_P(MLDSAParameterTest, RawFunctions) TEST_P(MLDSAParameterTest, SIGOperations) TEST_P(MLDSAParameterTest, MarshalParse) ``` #### 5. X.509 changes Changes to X.509 code have been minimized to only the required changes to support this PR. This is already a big PR and I want to avoid expansion wherever possible. For X.509 the changes predominantly are regarding the change of NIDs from the old Dilithium NIDs to the new NIST DSA NIDs/OIDs. As the OIDs changed, so too much the test certificates `kDilithium3Cert`, `kDilithium3CertNull`, and `kDilithium3CertParam`. These have been regenerated using the following: ``` Tool used https://github.com/google/der-ascii 1. First generate a valid test certificate in PEM encoding, say cert.pem 2. Convert PEM to DER: openssl x509 -in cert.pem -out cert.der -outform DER 3. Convert DER to ASCII: der2ascii -i cert.der >> cert.txt 4. Make edits to cert.txt as desired, say; certnew.txt 5. Convert ASCII to DER: ascii2der -i certnew.txt >> certnew.der 6. Convert DER to PEM: openssl x509 -in certnew.der -inform DER -out certnew.pem -outform PEM ``` For step (1) "generate a valid test certificate" you can use the X.509 test `Dilithium3SignVerifyCert` with added code to print the certificate (or store to file): ``` TEST(X509Test, Dilithium3SignVerifyCert) { // Generate mldsa key bssl::UniquePtr<EVP_PKEY_CTX> ctx(EVP_PKEY_CTX_new_id(EVP_PKEY_NISTDSA, nullptr)); ASSERT_TRUE(ctx); ASSERT_TRUE(EVP_PKEY_CTX_nistdsa_set_params(ctx.get(), NID_MLDSA65)); ASSERT_TRUE(EVP_PKEY_keygen_init(ctx.get())); EVP_PKEY *raw = nullptr; ASSERT_TRUE(EVP_PKEY_keygen(ctx.get(), &raw)); bssl::UniquePtr<EVP_PKEY> pkey(raw); ctx.reset(EVP_PKEY_CTX_new(pkey.get(), nullptr)); bssl::UniquePtr<X509> leaf = MakeTestCert("Intermediate", "Leaf", pkey.get(), false); ASSERT_TRUE(leaf); bssl::ScopedEVP_MD_CTX md_ctx; EVP_DigestSignInit(md_ctx.get(), nullptr, nullptr, nullptr, pkey.get()); ASSERT_TRUE(X509_sign_ctx(leaf.get(), md_ctx.get())); // print certificate and PEM encoding: bssl::UniquePtr<BIO> bio(BIO_new(BIO_s_mem())); X509_print(bio.get(), leaf.get()); PEM_write_bio_X509(bio.get(),leaf.get()); const uint8_t *out; size_t outlen; ASSERT_TRUE(BIO_mem_contents(bio.get(), &out, &outlen)); printf("%s",out); } ``` #### 6. Speed Tool `ML-DSA-65` has been added to the speed tool. To facilitate the new APIs used in this PR, the API version has been incremented by 1. The speed tool now produces benchmarks for all three parameter levels, example output: ``` Did 2871 MLDSA65 keygen operations in 1051869us (2729.4 ops/sec) Did 774 MLDSA65 signing operations in 1039468us (744.6 ops/sec) Did 2893 MLDSA65 verify operations in 1089059us (2656.4 ops/sec) ``` ### Call-outs: - `algorithm.c` : modified OIDs from DILITHIUM to PQDSA - `base.h` : added PQDSA_KEY struct - `evp.h` : added EVP_PKEY_pqdsa_set_params and EVP_PKEY_CTX_pqdsa_set_params functions to setting the specific OID (NID_MLDSAXX) for a PKEY of type EVP_PKEY_NISTDSA - `evp_extra_test.cc` : updated example MLDSA65 private key with new OID and encoding - `dilithium/internal.h` : new file to hold ML-DSA structure - `evp_extra/internal.h` : modified ML-DSA-65 specific ASN.1 and PKEY method to a generic one for all NIST PQC signature algorithms - `fipsmodule/evp/internal.h` : added PQDSA_KEY to PKEY structures to hold PKEY types for NIST PQC signature algorithms - `nid.h`, `obj_dat.h`, `obj_mac.num`, `obj_xref.c`, `objects.txt`: all modified to include designated OIDs for ML-DSA. - `p_dilithium3.c`: temporarily holds PQDSA new/clear/init functions. All existing PKEY functions (keygen/sign/verify) have been modified to use generic PQDSA and extract algorithm specific parameters/function methods. - `p_dilithium3_asn1.c`: All existing ASN1 functions (get/set/encode/decode/size/bits/free) have been modified to use generic PQDSA and extract algorithm specific parameters/function methods. - `p_dilithium_test.cc` : has been modified to parameterize all tests into a single test suite that use algorithm specific parameterization - `evp_extra/p_methods`: dilithium specific methods renamed - `print.c`, `speed.cc`, `x509_test.cc` : updated to retain functionality ### Testing: See above for description of changes to testing framework. By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license and the ISC license.
- Loading branch information