Skip to content

Commit

Permalink
Addition of generic NIST-DSA PKEY and ASN1 to support ML-DSA (#1963)
Browse files Browse the repository at this point in the history
#### 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
jakemas authored Nov 18, 2024
1 parent 0d4dab7 commit 404fe0f
Show file tree
Hide file tree
Showing 28 changed files with 2,389 additions and 1,962 deletions.
9 changes: 5 additions & 4 deletions crypto/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -323,9 +323,10 @@ if(ENABLE_DILITHIUM)
set(
DILITHIUM_SOURCES

dilithium/p_dilithium3.c
dilithium/p_dilithium3_asn1.c
dilithium/sig_dilithium3.c
dilithium/pqdsa.c
dilithium/p_pqdsa.c
dilithium/p_pqdsa_asn1.c
dilithium/ml_dsa.c
)
endif()

Expand Down Expand Up @@ -775,7 +776,7 @@ if(BUILD_TESTING)
ecdh_extra/ecdh_test.cc
dh_extra/dh_test.cc
digest_extra/digest_test.cc
dilithium/p_dilithium_test.cc
dilithium/p_pqdsa_test.cc
dsa/dsa_test.cc
des/des_test.cc
endian_test.cc
Expand Down
71 changes: 71 additions & 0 deletions crypto/dilithium/internal.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0 OR ISC

#ifndef AWSLC_HEADER_PQDSA_INTERNAL_H
#define AWSLC_HEADER_PQDSA_INTERNAL_H

#include <openssl/base.h>

#if defined(__cplusplus)
extern "C" {
#endif

// PQDSA_METHOD structure and helper functions.
typedef struct {
int (*keygen)(uint8_t *public_key,
uint8_t *private_key);

int (*sign)(const uint8_t *private_key,
uint8_t *sig,
size_t *sig_len,
const uint8_t *message,
size_t message_len,
const uint8_t *ctx_string,
size_t ctx_string_len);

int (*verify)(const uint8_t *public_key,
const uint8_t *sig,
size_t sig_len,
const uint8_t *message,
size_t message_len,
const uint8_t *ctx_string,
size_t ctx_string_len);

} PQDSA_METHOD;

// PQDSA structure and helper functions.
typedef struct {
int nid;
const uint8_t *oid;
uint8_t oid_len;
const char *comment;
size_t public_key_len;
size_t private_key_len;
size_t signature_len;
size_t keygen_seed_len;
size_t sign_seed_len;
const PQDSA_METHOD *method;
} PQDSA;

// PQDSA_KEY structure and helper functions.
struct pqdsa_key_st {
const PQDSA *pqdsa;
uint8_t *public_key;
uint8_t *private_key;
};

int PQDSA_KEY_init(PQDSA_KEY *key, const PQDSA *pqdsa);
const PQDSA * PQDSA_find_dsa_by_nid(int nid);
const EVP_PKEY_ASN1_METHOD *PQDSA_find_asn1_by_nid(int nid);
const PQDSA *PQDSA_KEY_get0_dsa(PQDSA_KEY* key);
PQDSA_KEY *PQDSA_KEY_new(void);
void PQDSA_KEY_free(PQDSA_KEY *key);
int EVP_PKEY_pqdsa_set_params(EVP_PKEY *pkey, int nid);

int PQDSA_KEY_set_raw_public_key(PQDSA_KEY *key, const uint8_t *in);
int PQDSA_KEY_set_raw_private_key(PQDSA_KEY *key, const uint8_t *in);
#if defined(__cplusplus)
} // extern C
#endif

#endif // AWSLC_HEADER_DSA_TEST_INTERNAL_H
43 changes: 22 additions & 21 deletions crypto/dilithium/sig_dilithium3.c → crypto/dilithium/ml_dsa.c
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0 OR ISC

#include "../evp_extra/internal.h"
#include "../fipsmodule/evp/internal.h"
#include "sig_dilithium.h"
#include "ml_dsa.h"
#include "pqcrystals_dilithium_ref_common/sign.h"
#include "pqcrystals_dilithium_ref_common/params.h"

Expand All @@ -20,39 +21,39 @@
#include "./pqcrystals_dilithium_ref_common/symmetric-shake.c"

// Note: These methods currently default to using the reference code for
// Dilithium. In a future where AWS-LC has optimized options available,
// ML-DSA. In a future where AWS-LC has optimized options available,
// those can be conditionally (or based on compile-time flags) called here,
// depending on platform support.

int ml_dsa_65_keypair(uint8_t *public_key /* OUT */,
uint8_t *secret_key /* OUT */) {
int ml_dsa_65_keypair(uint8_t *public_key /* OUT */,
uint8_t *private_key /* OUT */) {
ml_dsa_params params;
ml_dsa_65_params_init(&params);
return crypto_sign_keypair(&params, public_key, secret_key);
return (crypto_sign_keypair(&params, public_key, private_key) == 0);
}

int ml_dsa_65_sign(uint8_t *sig /* OUT */,
size_t *sig_len /* OUT */,
const uint8_t *message /* IN */,
size_t message_len /* IN */,
const uint8_t *ctx /* IN */,
size_t ctx_len /* IN */,
const uint8_t *secret_key /* IN */) {
int ml_dsa_65_sign(const uint8_t *private_key /* IN */,
uint8_t *sig /* OUT */,
size_t *sig_len /* OUT */,
const uint8_t *message /* IN */,
size_t message_len /* IN */,
const uint8_t *ctx_string /* IN */,
size_t ctx_string_len /* IN */) {
ml_dsa_params params;
ml_dsa_65_params_init(&params);
return crypto_sign_signature(&params, sig, sig_len, message, message_len,
ctx, ctx_len, secret_key);
ctx_string, ctx_string_len, private_key) == 0;
}

int ml_dsa_65_verify(const uint8_t *message /* IN */,
size_t message_len /* IN */,
const uint8_t *sig /* IN */,
size_t sig_len /* IN */,
const uint8_t *ctx /* IN */,
size_t ctx_len /* IN */,
const uint8_t *public_key /* IN */) {
int ml_dsa_65_verify(const uint8_t *public_key /* IN */,
const uint8_t *sig /* IN */,
size_t sig_len /* IN */,
const uint8_t *message /* IN */,
size_t message_len /* IN */,
const uint8_t *ctx_string /* IN */,
size_t ctx_string_len /* IN */) {
ml_dsa_params params;
ml_dsa_65_params_init(&params);
return crypto_sign_verify(&params, sig, sig_len, message, message_len,
ctx, ctx_len, public_key);
ctx_string, ctx_string_len, public_key) == 0;
}
36 changes: 36 additions & 0 deletions crypto/dilithium/ml_dsa.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0 OR ISC

#ifndef ML_DSA_H
#define ML_DSA_H

#include <stddef.h>
#include <stdint.h>
#include <openssl/base.h>
#include <openssl/evp.h>

#define MLDSA65_PUBLIC_KEY_BYTES 1952
#define MLDSA65_PRIVATE_KEY_BYTES 4032
#define MLDSA65_SIGNATURE_BYTES 3309
#define MLDSA65_KEYGEN_SEED_BYTES 32
#define MLDSA65_SIGNATURE_SEED_BYTES 32

int ml_dsa_65_keypair(uint8_t *public_key,
uint8_t *private_key);

int ml_dsa_65_sign(const uint8_t *private_key,
uint8_t *sig,
size_t *sig_len,
const uint8_t *message,
size_t message_len,
const uint8_t *ctx_string,
size_t ctx_string_len);

int ml_dsa_65_verify(const uint8_t *public_key,
const uint8_t *sig,
size_t sig_len,
const uint8_t *message,
size_t message_len,
const uint8_t *ctx_string,
size_t ctx_string_len);
#endif
125 changes: 0 additions & 125 deletions crypto/dilithium/p_dilithium3.c

This file was deleted.

Loading

0 comments on commit 404fe0f

Please sign in to comment.