Add existing HPKE project files
This commit is contained in:
262
hpke-pq.md
Normal file
262
hpke-pq.md
Normal file
@@ -0,0 +1,262 @@
|
||||
# HPKE Hybrid KEMs
|
||||
|
||||
[sources.truenas.cloud/code/hpke-pq](https://sources.truenas.cloud/code/hpke-pq)
|
||||
|
||||
This document is a simplified and self-contained implementation reference for
|
||||
the MLKEM768-X25519, MLKEM768-P256, and MLKEM1024-P384 hybrid HPKE KEMs,
|
||||
specified in [draft-ietf-hpke-pq-03][], [draft-irtf-cfrg-hybrid-kems-07][],
|
||||
[draft-irtf-cfrg-concrete-hybrid-kems-02][], and [draft-ietf-hpke-hpke-02].
|
||||
|
||||
It compensates for the need to cross-reference four documents, with different
|
||||
nomenclature (including functions and components with the same name but
|
||||
different behavior), alternative irrelevant definitions (the UG, UK, and CK
|
||||
frameworks), and multiple KEM abstraction layers.
|
||||
|
||||
## Conventions used in this document
|
||||
|
||||
`||` denotes concatenation. `[N:M]` denotes the byte slice from index N (inclusive)
|
||||
to index M (exclusive). Strings quoted with `""` are encoded as ASCII. Values in
|
||||
code blocks are hex encoded byte strings. `random(N)` denotes N bytes of CSPRNG
|
||||
output. All lengths are in bytes.
|
||||
|
||||
ML-KEM.KeyGen_internal, ML-KEM.Encaps, ML-KEM.Encaps_internal, and ML-KEM.Decaps
|
||||
are defined in [FIPS 203][]. `SHAKE256(s, L)` is an invocation of `SHAKE256(s, 8*L)`
|
||||
defined in [FIPS 202][]. SHA3-256 is defined in [FIPS 202][].
|
||||
|
||||
## KEM definitions
|
||||
|
||||
| Parameter | MLKEM768-X25519 | MLKEM768-P256 | MLKEM1024-P384 |
|
||||
| ----------------- | ------------------ | ------------------ | ------------------ |
|
||||
| ML-KEM parameters | ML-KEM-768 | ML-KEM-768 | ML-KEM-1024 |
|
||||
| Group | Curve25519 | P-256 | P-384 |
|
||||
| KEM identifier | 0x647a | 0x0050 | 0x0051 |
|
||||
| Nsecret | 32 | 32 | 32 |
|
||||
| Nenc | 1120 | 1153 | 1665 |
|
||||
| Npk | 1216 | 1249 | 1665 |
|
||||
| Nsk | 32 | 32 | 32 |
|
||||
| Nrandom | 64 | 160 | 80 |
|
||||
| Label | `"\.//^\"` | `"MLKEM768-P256"` | `"MLKEM1024-P384"` |
|
||||
| KEM.Nct | 1088 | 1088 | 1568 |
|
||||
| KEM.Nek | 1184 | 1184 | 1568 |
|
||||
| KEM.Nseed | 64 | 64 | 64 |
|
||||
| KEM.Nrandom | 32 | 32 | 32 |
|
||||
| Group.Nelem | 32 | 65 | 97 |
|
||||
| Group.Nseed | 32 | 128 | 48 |
|
||||
| Group.Nscalar | N/A | 32 | 48 |
|
||||
|
||||
The MLKEM768-X25519 Label is alternatively encoded as
|
||||
|
||||
5c2e2f2f5e5c
|
||||
|
||||
## KEM functions
|
||||
|
||||
```
|
||||
def GenerateKeyPair():
|
||||
seed = random(Nsk)
|
||||
|
||||
ek_PQ, ek_T, _, _ = expandKey(seed)
|
||||
ek = ek_PQ || ek_T
|
||||
|
||||
return (seed, ek)
|
||||
|
||||
def DeriveKeyPair(ikm):
|
||||
# SHAKE256.LabeledDerive is part of the single-stage KDF described in
|
||||
# draft-ietf-hpke-hpke-02 and defined in draft-ietf-hpke-pq-03, but is
|
||||
# reproduced below for convenience.
|
||||
seed = SHAKE256.LabeledDerive(ikm, "DeriveKeyPair", "", Nsk)
|
||||
|
||||
ek_PQ, ek_T, _, _ = expandKey(seed)
|
||||
ek = ek_PQ || ek_T
|
||||
|
||||
return (seed, ek)
|
||||
|
||||
def Encaps(ek):
|
||||
ek_PQ = ek[0 : KEM.Nek]
|
||||
ek_T = ek[KEM.Nek : KEM.Nek + Group.Nelem]
|
||||
|
||||
ss_PQ, ct_PQ = ML-KEM.Encaps(ek_PQ)
|
||||
|
||||
sk_E = Group.RandomScalar(random(Group.Nseed))
|
||||
ct_T = Group.Exp(Group.G, sk_E)
|
||||
ss_T = Group.ElementToSharedSecret(Group.Exp(ek_T, sk_E))
|
||||
|
||||
ss = SHA3-256(ss_PQ || ss_T || ct_T || ek_T || Label)
|
||||
ct = ct_PQ || ct_T
|
||||
|
||||
return (ss, ct)
|
||||
|
||||
def Decaps(seed, ct):
|
||||
ct_PQ = ct[0 : KEM.Nct]
|
||||
ct_T = ct[KEM.Nct : KEM.Nct + Group.Nelem]
|
||||
|
||||
ek_PQ, ek_T, dk_PQ, dk_T = expandKey(seed)
|
||||
|
||||
ss_PQ = ML-KEM.Decaps(dk_PQ, ct_PQ)
|
||||
ss_T = Group.ElementToSharedSecret(Group.Exp(ct_T, dk_T))
|
||||
|
||||
ss = SHA3-256(ss_PQ || ss_T || ct_T || ek_T || Label)
|
||||
|
||||
return ss
|
||||
|
||||
def expandKey(seed):
|
||||
seed_full = SHAKE256(seed, KEM.Nseed + Group.Nseed)
|
||||
seed_PQ = seed_full[0 : KEM.Nseed]
|
||||
seed_T = seed_full[KEM.Nseed : KEM.Nseed + Group.Nseed]
|
||||
|
||||
# Note that even if expandKey returns the semi-expanded ML-KEM decapsulation
|
||||
# key dk_PQ to use FIPS 203 definitions, that format should be avoided and
|
||||
# instead seed_PQ should be expanded directly into the implementation's
|
||||
# internal ML-KEM private representation.
|
||||
(ek_PQ, dk_PQ) = ML-KEM.KeyGen_internal(seed_PQ)
|
||||
dk_T = Group.RandomScalar(seed_T)
|
||||
ek_T = Group.Exp(Group.G, dk_T)
|
||||
|
||||
return (ek_PQ, ek_T, dk_PQ, dk_T)
|
||||
```
|
||||
|
||||
There is no distinction between a private/public key and its serialization:
|
||||
there is no abstract key format, only byte strings. In practice, implementations
|
||||
will probably want to load keys into pairs of internal representations, and
|
||||
serialize them back to their byte string format when needed.
|
||||
|
||||
The IETF/IRTF documents lack a specified way to turn a private key into public
|
||||
key, although it can be inferred from the key generation process. We define such
|
||||
a process here as `PrivateKeyToPublicKey`.
|
||||
|
||||
```
|
||||
def PrivateKeyToPublicKey(seed):
|
||||
ek_PQ, ek_T, _, _ = expandKey(seed)
|
||||
ek = ek_PQ || ek_T
|
||||
|
||||
return ek
|
||||
```
|
||||
|
||||
### Deterministic Encapsulation
|
||||
|
||||
For testing purposes, implementations can provide Nrandom bytes of encapsulation
|
||||
randomness to the deterministic internal function `EncapsDerand`.
|
||||
|
||||
```
|
||||
def EncapsDerand(ek, randomness):
|
||||
ek_PQ = ek[0 : KEM.Nek]
|
||||
ek_T = ek[KEM.Nek : KEM.Nek + Group.Nelem]
|
||||
|
||||
randomness_PQ = randomness[0 : KEM.Nrandom]
|
||||
randomness_T = randomness[KEM.Nrandom : KEM.Nrandom + Group.Nseed]
|
||||
|
||||
ss_PQ, ct_PQ = ML-KEM.Encaps_internal(ek_PQ, randomness_PQ)
|
||||
|
||||
sk_E = Group.RandomScalar(randomness_T)
|
||||
ct_T = Group.Exp(Group.G, sk_E)
|
||||
ss_T = Group.ElementToSharedSecret(Group.Exp(ek_T, sk_E))
|
||||
|
||||
ss = SHA3-256(ss_PQ || ss_T || ct_T || ek_T || Label)
|
||||
ct = ct_PQ || ct_T
|
||||
|
||||
return (ss, ct)
|
||||
```
|
||||
|
||||
## Group definitions
|
||||
|
||||
### Curve25519
|
||||
|
||||
Group.Exp is the X25519 function defined in [RFC 7748][].
|
||||
|
||||
Group.G is the canonical generator, which encodes to
|
||||
|
||||
0900000000000000000000000000000000000000000000000000000000000000
|
||||
|
||||
consistently with [RFC 7748, Section 4.1][] and [RFC 7748, Section 6.1][].
|
||||
|
||||
Group.RandomScalar and Group.ElementToSharedSecret are the identity.
|
||||
|
||||
### P-256 and P-384
|
||||
|
||||
The NIST P-256 and P-384 elliptic curves are defined in [SP800-186][].
|
||||
|
||||
`Group.Exp(p, x)` computes scalar multiplication between the input element p and
|
||||
the scalar x. The input element p and the output element have length Group.Nelem
|
||||
and are encoded in uncompressed representation using the
|
||||
Elliptic-Curve-Point-to-Octet-String and Octet-String-to-Elliptic-Curve-Point
|
||||
functions defined in [SEC 1, Version 2.0][]. The input scalar x has length
|
||||
Group.Nscalar and is encoded in big-endian representation using the I2OSP and
|
||||
OS2IP functions defined in [RFC 8017][].
|
||||
|
||||
Group.G is the canonical generator, which encodes to
|
||||
|
||||
046b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c29
|
||||
64fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5
|
||||
|
||||
for P-256, and to
|
||||
|
||||
04aa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a385502f25dbf55296c3a545e3872760ab
|
||||
73617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f
|
||||
|
||||
for P-384, consistently with [SP800-186][], Section 3.2.1.
|
||||
|
||||
```
|
||||
def Group.RandomScalar(seed):
|
||||
start = 0
|
||||
end = Nscalar
|
||||
sk = seed[start : end]
|
||||
while OS2IP(sk) == 0 || OS2IP(sk) >= OS2IP(Group.N):
|
||||
start = end
|
||||
end = end + Nscalar
|
||||
if end > len(seed):
|
||||
# This happens with cryptographically negligible probability.
|
||||
# The chance of a single rejection is < 2^-32 for P-256 and
|
||||
# < 2^-192 for P-384. The chance of reaching this is thus
|
||||
# < 2^-128 for P-256 and < 2^-192 for P-384.
|
||||
raise Exception("Rejection sampling failed")
|
||||
sk = seed[start : end]
|
||||
return sk
|
||||
```
|
||||
|
||||
Group.N is the order of the curve's group, which encodes to
|
||||
|
||||
ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551
|
||||
|
||||
for P-256, and to
|
||||
|
||||
ffffffffffffffffffffffffffffffffffffffffffffffffc7634d81f4372ddf581a0db248b0a77aecec196accc52973
|
||||
|
||||
for P-384, consistently with [SP800-186][], Section 3.2.1.
|
||||
|
||||
Group.ElementToSharedSecret encodes the input element as an X coordinate using
|
||||
the Field-Element-to-Octet-String function in [SEC 1, Version 2.0][].
|
||||
|
||||
> Note that since the scalar x is always derived uniformly at random, the chance
|
||||
> of it being zero are cryptographically negligible. Moreover,
|
||||
> Octet-String-to-Elliptic-Curve-Point never decodes the point at infinity from
|
||||
> a string of Group.Nelem bytes. Since NIST P curves have prime order, this
|
||||
> means that the output of Group.Exp and input to Group.ElementToSharedSecret is
|
||||
> also never the point at infinity.
|
||||
|
||||
## SHAKE256.LabeledDerive
|
||||
|
||||
SHAKE256.LabeledDerive is used by DeriveKeyPair, and is part of the single-stage
|
||||
KDF specified across [draft-ietf-hpke-hpke-02][] and [draft-ietf-hpke-pq-03][],
|
||||
but is reproduced below for convenience.
|
||||
|
||||
```
|
||||
def SHAKE256.LabeledDerive(ikm, label, context, L):
|
||||
suite_id = concat("KEM", I2OSP(kem_id, 2))
|
||||
prefixed_label = I2OSP(len(label), 2) || label
|
||||
labeled_ikm = ikm || "HPKE-v1" || suite_id || prefixed_label || I2OSP(L, 2) || context
|
||||
return SHAKE256(labeled_ikm, L)
|
||||
```
|
||||
|
||||
I2OSP is defined in [RFC 8017][], and `kem_id` is the KEM identifier.
|
||||
|
||||
[draft-ietf-hpke-hpke-02]: https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-02
|
||||
[draft-ietf-hpke-pq-03]: https://datatracker.ietf.org/doc/html/draft-ietf-hpke-pq-03
|
||||
[draft-irtf-cfrg-hybrid-kems-07]: https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hybrid-kems-07
|
||||
[draft-irtf-cfrg-concrete-hybrid-kems-02]: https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-concrete-hybrid-kems-02
|
||||
[RFC 7748]: https://rfc-editor.org/rfc/rfc7748.html
|
||||
[RFC 7748, Section 4.1]: https://rfc-editor.org/rfc/rfc7748.html#section-4.1
|
||||
[RFC 7748, Section 6.1]: https://rfc-editor.org/rfc/rfc7748.html#section-6.1
|
||||
[RFC 8017]: https://datatracker.ietf.org/doc/html/rfc8017
|
||||
[FIPS 202]: https://doi.org/10.6028/NIST.FIPS.202
|
||||
[FIPS 203]: https://doi.org/10.6028/NIST.FIPS.203
|
||||
[SP800-186]: https://doi.org/10.6028/NIST.SP.800-186
|
||||
[SEC 1, Version 2.0]: https://www.secg.org/sec1-v2.pdf
|
||||
Reference in New Issue
Block a user