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

Support for ML-DSA public key generation from private key #2142

Merged
merged 3 commits into from
Jan 28, 2025

Conversation

jakemas
Copy link
Contributor

@jakemas jakemas commented Jan 28, 2025

Issues:

Resolves #CryptoAlg-2868

Description of changes:

It is often useful when serializing asymmetric key pairs to populate both the public and private elements, given only the private element. For this to be possible, an algorithm utility function is often provided to derive key material. ML-DSA does not support this in the reference implementation.

Background ML-DSA keypairs

An ML-DSA private key is constructed of the following elements: (ref https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.204.pdf)

sk = (
      rho, // public random seed (32-bytes)
      tr,  // public key hash (64-bytes)
      key, // private random seed (32-bytes) (utilized during sign)
      t0,  // polynomial vector: encodes the least significant bits of public-key polynomial t, facilitating certain computational efficiencies.
      s1,  // secret polynomial vectors. These vectors contain polynomials with coefficients in a specified range, 
      s2.  // serving as the secret components in the lattice-based structure of ML-DSA.
)

An ML-DSA public key is constructed of the following elements:

pk = (
      rho, // public random seed (32-bytes)
      t1.  // compressed representation of the public key polynomial 
)
  • The vector t is decomposed into two parts:
  • t1: Represents the higher-order bits of t.
  • t0: Represents the lower-order bits of t.

One can see that to reconstruct the public key from the private key, one must:

  1. Extract all elements from sk, using the existing function in /ml_dsa_ref/packing.c: ml_dsa_unpack_sk
    1. This will provide sk = (rho, tr, key, t0, s1, s2).
  2. Reconstruct A using rho with the existing function in /ml_dsa_ref/polyvec.c: ml_dsa_polyvec_matrix_expand
  3. Reconstruct t from t = A*s1 + s2
  4. Drop d lower bits from t to get t1
  5. Pack rho, t1 into public key.
  6. Verify pk matches expected value, by comparing SHAKE256(pk) + tr (unpacked from secret key).

This has been implemented in ml_dsa_pack_pk_from_sk -- not tied to the name, just using what I've seen so far in common nomenclature.

As the values of d differ for each parameter set of ML-DSA, we must create packing functions for each parameter size. As such, ml_dsa_44_pack_pk_from_sk, ml_dsa_65_pack_pk_from_sk, and ml_dsa_87_pack_pk_from_sk have been added to ml_dsa.h to serve as utility functions in higher level EVP APIs.

Call-outs:

The scope of this PR is only the algorithm level, using these functions for useful tasks such as populating the public key automatically on private key import -- will be added in subsequent PRs.

Testing:

A new test has been added to PQDSAParameterTest, namely, KeyConsistencyTest that will assert that packing the key is successful, and that the key produced matches the original public key.

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.

@jakemas jakemas requested a review from a team as a code owner January 28, 2025 07:33
@codecov-commenter
Copy link

codecov-commenter commented Jan 28, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 78.97%. Comparing base (37c2b5e) to head (355d6d7).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2142      +/-   ##
==========================================
+ Coverage   78.96%   78.97%   +0.01%     
==========================================
  Files         610      610              
  Lines      105293   105323      +30     
  Branches    14919    14921       +2     
==========================================
+ Hits        83141    83178      +37     
+ Misses      21500    21493       -7     
  Partials      652      652              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

justsmth
justsmth previously approved these changes Jan 28, 2025
Copy link
Contributor

@justsmth justsmth left a comment

Choose a reason for hiding this comment

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

Thanks for getting this change up so quickly!

Copy link
Contributor

@nebeid nebeid left a comment

Choose a reason for hiding this comment

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

May I suggest renaming to pack_pk_from_sk? I understand they're internal functions but they're exported and so renaming them later may be more difficult.

Nit: comments in

* - const uint8_t rho[]: output byte array for rho
* - const uint8_t tr[]: output byte array for tr
* - const uint8_t key[]: output byte array for key
* - const polyveck *t0: pointer to output vector t0
* - const polyvecl *s1: pointer to output vector s1
* - const polyveck *s2: pointer to output vector s2
should not have const.

@jakemas
Copy link
Contributor Author

jakemas commented Jan 28, 2025

May I suggest renaming to pack_pk_from_sk? I understand they're internal functions but they're exported and so renaming them later may be more difficult.

Thank you, changed to ml_dsa_pack_pk_from_sk in bf01ead.

Nit: comments in

* - const uint8_t rho[]: output byte array for rho
* - const uint8_t tr[]: output byte array for tr
* - const uint8_t key[]: output byte array for key
* - const polyveck *t0: pointer to output vector t0
* - const polyvecl *s1: pointer to output vector s1
* - const polyveck *s2: pointer to output vector s2

should not have const.

Got them, also fixed in
bf01ead

justsmth
justsmth previously approved these changes Jan 28, 2025
@justsmth justsmth enabled auto-merge (squash) January 28, 2025 17:57
@justsmth justsmth merged commit 1f48000 into aws:main Jan 28, 2025
123 of 126 checks passed
manastasova pushed a commit to manastasova/aws-lc that referenced this pull request Jan 30, 2025
### Issues:
Resolves #CryptoAlg-2868

### Description of changes: 

It is often useful when serializing asymmetric key pairs to populate
both the public and private elements, given only the private element.
For this to be possible, an algorithm utility function is often provided
to derive key material. ML-DSA does not support this in the reference
implementation.

#### Background ML-DSA keypairs 

An ML-DSA private key is constructed of the following elements: (ref
https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.204.pdf)
```
sk = (
      rho, // public random seed (32-bytes)
      tr,  // public key hash (64-bytes)
      key, // private random seed (32-bytes) (utilized during sign)
      t0,  // polynomial vector: encodes the least significant bits of public-key polynomial t, facilitating certain computational efficiencies.
      s1,  // secret polynomial vectors. These vectors contain polynomials with coefficients in a specified range, 
      s2.  // serving as the secret components in the lattice-based structure of ML-DSA.
)
```

An ML-DSA public key is constructed of the following elements:
```
pk = (
      rho, // public random seed (32-bytes)
      t1.  // compressed representation of the public key polynomial 
)
```

- The vector t is decomposed into two parts:
- `t1`: Represents the higher-order bits of `t`.
- `t0`: Represents the lower-order bits of `t`.

One can see that to reconstruct the public key from the private key, one
must:
1. Extract all elements from `sk`, using the existing function in
`/ml_dsa_ref/packing.c`: `ml_dsa_unpack_sk`
    1. This will provide `sk = (rho, tr, key, t0, s1, s2)`.
2. Reconstruct `A` using `rho` with the existing function in
`/ml_dsa_ref/polyvec.c`: `ml_dsa_polyvec_matrix_expand`
3. Reconstruct `t` from `t = A*s1 + s2`
4. Drop `d` lower bits from `t` to get `t1`
5. Pack `rho`, `t1` into public key.
6. Verify `pk` matches expected value, by comparing SHAKE256(pk) + `tr`
(unpacked from secret key).

This has been implemented in `ml_dsa_pack_pk_from_sk` -- not tied to the
name, just using what I've seen so far in common nomenclature.

As the values of `d` differ for each parameter set of ML-DSA, we must
create packing functions for each parameter size. As such,
`ml_dsa_44_pack_pk_from_sk``, `ml_dsa_65_pack_pk_from_sk``, and
`ml_dsa_87_pack_pk_from_sk`` have been added to `ml_dsa.h` to serve as
utility functions in higher level EVP APIs.

### Call-outs:

The scope of this PR is only the algorithm level, using these functions
for useful tasks such as populating the public key automatically on
private key import -- will be added in subsequent PRs.

### Testing:
A new test has been added to `PQDSAParameterTest`, namely,
`KeyConsistencyTest` that will assert that packing the key is
successful, and that the key produced matches the original public key.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants