Add existing HPKE project files

This commit is contained in:
2026-03-06 23:52:07 +00:00
commit 534572b883
18 changed files with 4246 additions and 0 deletions

27
LICENSE.txt Normal file
View File

@@ -0,0 +1,27 @@
Copyright 2009 The Go Authors.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google LLC nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

130
aead.go Normal file
View File

@@ -0,0 +1,130 @@
// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package hpke
import (
"crypto/aes"
"crypto/cipher"
"errors"
"fmt"
"golang.org/x/crypto/chacha20poly1305"
)
// The AEAD is one of the three components of an HPKE ciphersuite, implementing
// symmetric encryption.
type AEAD interface {
ID() uint16
keySize() int
nonceSize() int
aead(key []byte) (cipher.AEAD, error)
}
// NewAEAD returns the AEAD implementation for the given AEAD ID.
//
// Applications are encouraged to use specific implementations like [AES128GCM]
// or [ChaCha20Poly1305] instead, unless runtime agility is required.
func NewAEAD(id uint16) (AEAD, error) {
switch id {
case 0x0001: // AES-128-GCM
return AES128GCM(), nil
case 0x0002: // AES-256-GCM
return AES256GCM(), nil
case 0x0003: // ChaCha20Poly1305
return ChaCha20Poly1305(), nil
case 0xFFFF: // Export-only
return ExportOnly(), nil
default:
return nil, fmt.Errorf("unsupported AEAD %04x", id)
}
}
// AES128GCM returns an AES-128-GCM AEAD implementation.
func AES128GCM() AEAD { return aes128GCM }
// AES256GCM returns an AES-256-GCM AEAD implementation.
func AES256GCM() AEAD { return aes256GCM }
// ChaCha20Poly1305 returns a ChaCha20Poly1305 AEAD implementation.
func ChaCha20Poly1305() AEAD { return chacha20poly1305AEAD }
// ExportOnly returns a placeholder AEAD implementation that cannot encrypt or
// decrypt, but only export secrets with [Sender.Export] or [Recipient.Export].
//
// When this is used, [Sender.Seal] and [Recipient.Open] return errors.
func ExportOnly() AEAD { return exportOnlyAEAD{} }
type aead struct {
nK int
nN int
new func([]byte) (cipher.AEAD, error)
id uint16
}
var aes128GCM = &aead{
nK: 128 / 8,
nN: 96 / 8,
new: newAESGCM,
id: 0x0001,
}
var aes256GCM = &aead{
nK: 256 / 8,
nN: 96 / 8,
new: newAESGCM,
id: 0x0002,
}
var chacha20poly1305AEAD = &aead{
nK: chacha20poly1305.KeySize,
nN: chacha20poly1305.NonceSize,
new: chacha20poly1305.New,
id: 0x0003,
}
func newAESGCM(key []byte) (cipher.AEAD, error) {
b, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
return cipher.NewGCM(b)
}
func (a *aead) ID() uint16 {
return a.id
}
func (a *aead) aead(key []byte) (cipher.AEAD, error) {
if len(key) != a.nK {
return nil, errors.New("invalid key size")
}
return a.new(key)
}
func (a *aead) keySize() int {
return a.nK
}
func (a *aead) nonceSize() int {
return a.nN
}
type exportOnlyAEAD struct{}
func (exportOnlyAEAD) ID() uint16 {
return 0xFFFF
}
func (exportOnlyAEAD) aead(key []byte) (cipher.AEAD, error) {
return nil, nil
}
func (exportOnlyAEAD) keySize() int {
return 0
}
func (exportOnlyAEAD) nonceSize() int {
return 0
}

15
crypto/ecdh/interfaces.go Normal file
View File

@@ -0,0 +1,15 @@
// Package ecdh defines an additional interface that will be added to the
// crypto/ecdh package in Go 1.26+.
package ecdh
import "crypto/ecdh"
// KeyExchanger is an interface for an opaque private key that can be used for
// key exchange operations. For example, an ECDH key kept in a hardware module.
//
// It is implemented by [ecdh.PrivateKey].
type KeyExchanger interface {
PublicKey() *ecdh.PublicKey
Curve() ecdh.Curve
ECDH(*ecdh.PublicKey) ([]byte, error)
}

15
crypto/ecdh/stubs.go Normal file
View File

@@ -0,0 +1,15 @@
package ecdh
import "crypto/ecdh"
// This file contains stubs to allow importing only this package instead of
// crypto/ecdh, to minimize the diff.
type Curve = ecdh.Curve
type PrivateKey = ecdh.PrivateKey
type PublicKey = ecdh.PublicKey
func X25519() Curve { return ecdh.X25519() }
func P256() Curve { return ecdh.P256() }
func P384() Curve { return ecdh.P384() }
func P521() Curve { return ecdh.P521() }

68
crypto/interfaces.go Normal file
View File

@@ -0,0 +1,68 @@
// Package crypto defines additional interfaces that will be added to the
// crypto package in Go 1.26+.
package crypto
import (
"crypto/ecdh"
"crypto/mlkem"
)
// KeyExchanger is an interface for an opaque private key that can be used for
// key exchange operations. For example, an ECDH key kept in a hardware module.
//
// It is implemented by [ecdh.PrivateKey].
type KeyExchanger interface {
PublicKey() *ecdh.PublicKey
Curve() ecdh.Curve
ECDH(*ecdh.PublicKey) ([]byte, error)
}
// Encapsulator is an interface for a public KEM key that can be used for
// encapsulation operations.
//
// It is implemented, for example, by [crypto/mlkem.EncapsulationKey768].
type Encapsulator interface {
Bytes() []byte
Encapsulate() (sharedKey, ciphertext []byte)
}
// Decapsulator is an interface for an opaque private KEM key that can be used for
// decapsulation operations. For example, an ML-KEM key kept in a hardware module.
//
// It will be implemented by [crypto/mlkem.DecapsulationKey768] in Go 1.26+.
// In the meantime, use [DecapsulatorFromDecapsulationKey768] and
// [DecapsulatorFromDecapsulationKey1024].
type Decapsulator interface {
Encapsulator() Encapsulator
Decapsulate(ciphertext []byte) (sharedKey []byte, err error)
}
// DecapsulatorFromDecapsulationKey768 wraps an ML-KEM-768 decapsulation key
// into a [Decapsulator], until Go 1.26+ where [crypto/mlkem.DecapsulationKey768]
// implements it natively.
func DecapsulatorFromDecapsulationKey768(dk *mlkem.DecapsulationKey768) Decapsulator {
return &mlkem768Decapsulator{dk}
}
type mlkem768Decapsulator struct {
*mlkem.DecapsulationKey768
}
func (d *mlkem768Decapsulator) Encapsulator() Encapsulator {
return d.EncapsulationKey()
}
// DecapsulatorFromDecapsulationKey1024 wraps an ML-KEM-1024 decapsulation key
// into a [Decapsulator], until Go 1.26+ where [crypto/mlkem.DecapsulationKey1024]
// implements it natively.
func DecapsulatorFromDecapsulationKey1024(dk *mlkem.DecapsulationKey1024) Decapsulator {
return &mlkem1024Decapsulator{dk}
}
type mlkem1024Decapsulator struct {
*mlkem.DecapsulationKey1024
}
func (d *mlkem1024Decapsulator) Encapsulator() Encapsulator {
return d.EncapsulationKey()
}

7
go.mod Normal file
View File

@@ -0,0 +1,7 @@
module sources.truenas.cloud/code/hpke
go 1.24.0
require golang.org/x/crypto v0.41.0
require golang.org/x/sys v0.35.0 // indirect

4
go.sum Normal file
View File

@@ -0,0 +1,4 @@
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=

262
hpke-pq.md Normal file
View 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

270
hpke.go Normal file
View File

@@ -0,0 +1,270 @@
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package hpke implements Hybrid Public Key Encryption (HPKE) as defined in
// [RFC 9180].
//
// [RFC 9180]: https://www.rfc-editor.org/rfc/rfc9180.html
package hpke
import (
"crypto/cipher"
"errors"
"sources.truenas.cloud/code/hpke/internal/byteorder"
)
type context struct {
suiteID []byte
export func(string, uint16) ([]byte, error)
aead cipher.AEAD
baseNonce []byte
// seqNum starts at zero and is incremented for each Seal/Open call.
// 64 bits are enough not to overflow for 500 years at 1ns per operation.
seqNum uint64
}
// Sender is a sending HPKE context. It is instantiated with a specific KEM
// encapsulation key (i.e. the public key), and it is stateful, incrementing the
// nonce counter for each [Sender.Seal] call.
type Sender struct {
*context
}
// Recipient is a receiving HPKE context. It is instantiated with a specific KEM
// decapsulation key (i.e. the secret key), and it is stateful, incrementing the
// nonce counter for each successful [Recipient.Open] call.
type Recipient struct {
*context
}
func newContext(sharedSecret []byte, kemID uint16, kdf KDF, aead AEAD, info []byte) (*context, error) {
sid := suiteID(kemID, kdf.ID(), aead.ID())
if kdf.oneStage() {
secrets := make([]byte, 0, 2+2+len(sharedSecret))
secrets = byteorder.BEAppendUint16(secrets, 0) // empty psk
secrets = byteorder.BEAppendUint16(secrets, uint16(len(sharedSecret)))
secrets = append(secrets, sharedSecret...)
ksContext := make([]byte, 0, 1+2+2+len(info))
ksContext = append(ksContext, 0) // mode 0
ksContext = byteorder.BEAppendUint16(ksContext, 0) // empty psk_id
ksContext = byteorder.BEAppendUint16(ksContext, uint16(len(info)))
ksContext = append(ksContext, info...)
secret, err := kdf.labeledDerive(sid, secrets, "secret", ksContext,
uint16(aead.keySize()+aead.nonceSize()+kdf.size()))
if err != nil {
return nil, err
}
key := secret[:aead.keySize()]
baseNonce := secret[aead.keySize() : aead.keySize()+aead.nonceSize()]
expSecret := secret[aead.keySize()+aead.nonceSize():]
a, err := aead.aead(key)
if err != nil {
return nil, err
}
export := func(exporterContext string, length uint16) ([]byte, error) {
return kdf.labeledDerive(sid, expSecret, "sec", []byte(exporterContext), length)
}
return &context{
aead: a,
suiteID: sid,
export: export,
baseNonce: baseNonce,
}, nil
}
pskIDHash, err := kdf.labeledExtract(sid, nil, "psk_id_hash", nil)
if err != nil {
return nil, err
}
infoHash, err := kdf.labeledExtract(sid, nil, "info_hash", info)
if err != nil {
return nil, err
}
ksContext := append([]byte{0}, pskIDHash...)
ksContext = append(ksContext, infoHash...)
secret, err := kdf.labeledExtract(sid, sharedSecret, "secret", nil)
if err != nil {
return nil, err
}
key, err := kdf.labeledExpand(sid, secret, "key", ksContext, uint16(aead.keySize()))
if err != nil {
return nil, err
}
a, err := aead.aead(key)
if err != nil {
return nil, err
}
baseNonce, err := kdf.labeledExpand(sid, secret, "base_nonce", ksContext, uint16(aead.nonceSize()))
if err != nil {
return nil, err
}
expSecret, err := kdf.labeledExpand(sid, secret, "exp", ksContext, uint16(kdf.size()))
if err != nil {
return nil, err
}
export := func(exporterContext string, length uint16) ([]byte, error) {
return kdf.labeledExpand(sid, expSecret, "sec", []byte(exporterContext), length)
}
return &context{
aead: a,
suiteID: sid,
export: export,
baseNonce: baseNonce,
}, nil
}
// NewSender returns a sending HPKE context for the provided KEM encapsulation
// key (i.e. the public key), and using the ciphersuite defined by the
// combination of KEM, KDF, and AEAD.
//
// The info parameter is additional public information that must match between
// sender and recipient.
//
// The returned enc ciphertext can be used to instantiate a matching receiving
// HPKE context with the corresponding KEM decapsulation key.
func NewSender(pk PublicKey, kdf KDF, aead AEAD, info []byte) (enc []byte, s *Sender, err error) {
return NewSenderWithTestingRandomness(pk, nil, kdf, aead, info)
}
// NewSenderWithTestingRandomness is like NewSender, but uses the provided
// testingRandomness for deterministic KEM encapsulation. This is only intended
// for use in tests with known-answer test vectors.
func NewSenderWithTestingRandomness(pk PublicKey, testingRandomness []byte, kdf KDF, aead AEAD, info []byte) (enc []byte, s *Sender, err error) {
sharedSecret, encapsulatedKey, err := pk.encap(testingRandomness)
if err != nil {
return nil, nil, err
}
context, err := newContext(sharedSecret, pk.KEM().ID(), kdf, aead, info)
if err != nil {
return nil, nil, err
}
return encapsulatedKey, &Sender{context}, nil
}
// NewRecipient returns a receiving HPKE context for the provided KEM
// decapsulation key (i.e. the secret key), and using the ciphersuite defined by
// the combination of KEM, KDF, and AEAD.
//
// The enc parameter must have been produced by a matching sending HPKE context
// with the corresponding KEM encapsulation key. The info parameter is
// additional public information that must match between sender and recipient.
func NewRecipient(enc []byte, k PrivateKey, kdf KDF, aead AEAD, info []byte) (*Recipient, error) {
sharedSecret, err := k.decap(enc)
if err != nil {
return nil, err
}
context, err := newContext(sharedSecret, k.KEM().ID(), kdf, aead, info)
if err != nil {
return nil, err
}
return &Recipient{context}, nil
}
// Seal encrypts the provided plaintext, optionally binding to the additional
// public data aad.
//
// Seal uses incrementing counters for each call, and Open on the receiving side
// must be called in the same order as Seal.
func (s *Sender) Seal(aad, plaintext []byte) ([]byte, error) {
if s.aead == nil {
return nil, errors.New("export-only instantiation")
}
ciphertext := s.aead.Seal(nil, s.nextNonce(), plaintext, aad)
s.seqNum++
return ciphertext, nil
}
// Seal instantiates a single-use HPKE sending HPKE context like [NewSender],
// and then encrypts the provided plaintext like [Sender.Seal] (with no aad).
// Seal returns the concatenation of the encapsulated key and the ciphertext.
func Seal(pk PublicKey, kdf KDF, aead AEAD, info, plaintext []byte) ([]byte, error) {
enc, s, err := NewSender(pk, kdf, aead, info)
if err != nil {
return nil, err
}
ct, err := s.Seal(nil, plaintext)
if err != nil {
return nil, err
}
return append(enc, ct...), nil
}
// Export produces a secret value derived from the shared key between sender and
// recipient. length must be at most 65,535.
func (s *Sender) Export(exporterContext string, length int) ([]byte, error) {
if length < 0 || length > 0xFFFF {
return nil, errors.New("invalid length")
}
return s.export(exporterContext, uint16(length))
}
// Open decrypts the provided ciphertext, optionally binding to the additional
// public data aad, or returns an error if decryption fails.
//
// Open uses incrementing counters for each successful call, and must be called
// in the same order as Seal on the sending side.
func (r *Recipient) Open(aad, ciphertext []byte) ([]byte, error) {
if r.aead == nil {
return nil, errors.New("export-only instantiation")
}
plaintext, err := r.aead.Open(nil, r.nextNonce(), ciphertext, aad)
if err != nil {
return nil, err
}
r.seqNum++
return plaintext, nil
}
// Open instantiates a single-use HPKE receiving HPKE context like [NewRecipient],
// and then decrypts the provided ciphertext like [Recipient.Open] (with no aad).
// ciphertext must be the concatenation of the encapsulated key and the actual ciphertext.
func Open(k PrivateKey, kdf KDF, aead AEAD, info, ciphertext []byte) ([]byte, error) {
encSize := k.KEM().encSize()
if len(ciphertext) < encSize {
return nil, errors.New("ciphertext too short")
}
enc, ciphertext := ciphertext[:encSize], ciphertext[encSize:]
r, err := NewRecipient(enc, k, kdf, aead, info)
if err != nil {
return nil, err
}
return r.Open(nil, ciphertext)
}
// Export produces a secret value derived from the shared key between sender and
// recipient. length must be at most 65,535.
func (r *Recipient) Export(exporterContext string, length int) ([]byte, error) {
if length < 0 || length > 0xFFFF {
return nil, errors.New("invalid length")
}
return r.export(exporterContext, uint16(length))
}
func (ctx *context) nextNonce() []byte {
nonce := make([]byte, ctx.aead.NonceSize())
byteorder.BEPutUint64(nonce[len(nonce)-8:], ctx.seqNum)
for i := range ctx.baseNonce {
nonce[i] ^= ctx.baseNonce[i]
}
return nonce
}
func suiteID(kemID, kdfID, aeadID uint16) []byte {
suiteID := make([]byte, 0, 4+2+2+2)
suiteID = append(suiteID, []byte("HPKE")...)
suiteID = byteorder.BEAppendUint16(suiteID, kemID)
suiteID = byteorder.BEAppendUint16(suiteID, kdfID)
suiteID = byteorder.BEAppendUint16(suiteID, aeadID)
return suiteID
}

416
hpke_test.go Normal file
View File

@@ -0,0 +1,416 @@
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package hpke
import (
"bytes"
"crypto/ecdh"
"crypto/sha3"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"os"
"testing"
)
func Example() {
// In this example, we use MLKEM768-X25519 as the KEM, HKDF-SHA256 as the
// KDF, and AES-256-GCM as the AEAD to encrypt a single message from a
// sender to a recipient using the one-shot API.
kem, kdf, aead := MLKEM768X25519(), HKDFSHA256(), AES256GCM()
// Recipient side
var (
recipientPrivateKey PrivateKey
publicKeyBytes []byte
)
{
k, err := kem.GenerateKey()
if err != nil {
panic(err)
}
recipientPrivateKey = k
publicKeyBytes = k.PublicKey().Bytes()
}
// Sender side
var ciphertext []byte
{
publicKey, err := kem.NewPublicKey(publicKeyBytes)
if err != nil {
panic(err)
}
message := []byte("|-()-|")
ct, err := Seal(publicKey, kdf, aead, []byte("example"), message)
if err != nil {
panic(err)
}
ciphertext = ct
}
// Recipient side
{
plaintext, err := Open(recipientPrivateKey, kdf, aead, []byte("example"), ciphertext)
if err != nil {
panic(err)
}
fmt.Printf("Decrypted message: %s\n", plaintext)
}
// Output:
// Decrypted message: |-()-|
}
func mustDecodeHex(t *testing.T, in string) []byte {
t.Helper()
b, err := hex.DecodeString(in)
if err != nil {
t.Fatal(err)
}
return b
}
func TestVectors(t *testing.T) {
t.Run("rfc9180", func(t *testing.T) {
testVectors(t, "rfc9180")
})
t.Run("hpke-pq", func(t *testing.T) {
testVectors(t, "hpke-pq")
})
}
func testVectors(t *testing.T, name string) {
vectorsJSON, err := os.ReadFile("testdata/" + name + ".json")
if err != nil {
t.Fatal(err)
}
var vectors []struct {
Mode uint16 `json:"mode"`
KEM uint16 `json:"kem_id"`
KDF uint16 `json:"kdf_id"`
AEAD uint16 `json:"aead_id"`
Info string `json:"info"`
IkmE string `json:"ikmE"`
IkmR string `json:"ikmR"`
SkRm string `json:"skRm"`
PkRm string `json:"pkRm"`
Enc string `json:"enc"`
Encryptions []struct {
Aad string `json:"aad"`
Ct string `json:"ct"`
Nonce string `json:"nonce"`
Pt string `json:"pt"`
} `json:"encryptions"`
Exports []struct {
Context string `json:"exporter_context"`
L int `json:"L"`
Value string `json:"exported_value"`
} `json:"exports"`
// Instead of checking in a very large rfc9180.json, we computed
// alternative accumulated values.
AccEncryptions string `json:"encryptions_accumulated"`
AccExports string `json:"exports_accumulated"`
}
if err := json.Unmarshal(vectorsJSON, &vectors); err != nil {
t.Fatal(err)
}
for _, vector := range vectors {
name := fmt.Sprintf("mode %04x kem %04x kdf %04x aead %04x",
vector.Mode, vector.KEM, vector.KDF, vector.AEAD)
t.Run(name, func(t *testing.T) {
if vector.Mode != 0 {
t.Skip("only mode 0 (base) is supported")
}
if vector.KEM == 0x0021 {
t.Skip("KEM 0x0021 (DHKEM(X448)) not supported")
}
if vector.KEM == 0x0040 {
t.Skip("KEM 0x0040 (ML-KEM-512) not supported")
}
if vector.KDF == 0x0012 || vector.KDF == 0x0013 {
t.Skipf("TurboSHAKE KDF not supported")
}
kdf, err := NewKDF(vector.KDF)
if err != nil {
t.Fatal(err)
}
if kdf.ID() != vector.KDF {
t.Errorf("unexpected KDF ID: got %04x, want %04x", kdf.ID(), vector.KDF)
}
aead, err := NewAEAD(vector.AEAD)
if err != nil {
t.Fatal(err)
}
if aead.ID() != vector.AEAD {
t.Errorf("unexpected AEAD ID: got %04x, want %04x", aead.ID(), vector.AEAD)
}
kem, err := NewKEM(vector.KEM)
if err != nil {
t.Fatal(err)
}
if kem.ID() != vector.KEM {
t.Errorf("unexpected KEM ID: got %04x, want %04x", kem.ID(), vector.KEM)
}
pubKeyBytes := mustDecodeHex(t, vector.PkRm)
kemSender, err := kem.NewPublicKey(pubKeyBytes)
if err != nil {
t.Fatal(err)
}
if kemSender.KEM() != kem {
t.Errorf("unexpected KEM from sender: got %04x, want %04x", kemSender.KEM().ID(), kem.ID())
}
if !bytes.Equal(kemSender.Bytes(), pubKeyBytes) {
t.Errorf("unexpected KEM bytes: got %x, want %x", kemSender.Bytes(), pubKeyBytes)
}
ikmE := mustDecodeHex(t, vector.IkmE)
info := mustDecodeHex(t, vector.Info)
encap, sender, err := NewSenderWithTestingRandomness(kemSender, ikmE, kdf, aead, info)
if err != nil {
t.Fatal(err)
}
if len(encap) != kem.encSize() {
t.Errorf("unexpected encapsulated key size: got %d, want %d", len(encap), kem.encSize())
}
expectedEncap := mustDecodeHex(t, vector.Enc)
if !bytes.Equal(encap, expectedEncap) {
t.Errorf("unexpected encapsulated key, got: %x, want %x", encap, expectedEncap)
}
privKeyBytes := mustDecodeHex(t, vector.SkRm)
kemRecipient, err := kem.NewPrivateKey(privKeyBytes)
if err != nil {
t.Fatal(err)
}
if kemRecipient.KEM() != kem {
t.Errorf("unexpected KEM from recipient: got %04x, want %04x", kemRecipient.KEM().ID(), kem.ID())
}
kemRecipientBytes, err := kemRecipient.Bytes()
if err != nil {
t.Fatal(err)
}
// X25519 serialized keys must be clamped, so the bytes might not match.
if !bytes.Equal(kemRecipientBytes, privKeyBytes) && vector.KEM != DHKEM(ecdh.X25519()).ID() {
t.Errorf("unexpected KEM bytes: got %x, want %x", kemRecipientBytes, privKeyBytes)
}
if vector.KEM == DHKEM(ecdh.X25519()).ID() {
kem2, err := kem.NewPrivateKey(kemRecipientBytes)
if err != nil {
t.Fatal(err)
}
kemRecipientBytes2, err := kem2.Bytes()
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(kemRecipientBytes2, kemRecipientBytes) {
t.Errorf("X25519 re-serialized key differs: got %x, want %x", kemRecipientBytes2, kemRecipientBytes)
}
if !bytes.Equal(kem2.PublicKey().Bytes(), pubKeyBytes) {
t.Errorf("X25519 re-derived public key differs: got %x, want %x", kem2.PublicKey().Bytes(), pubKeyBytes)
}
}
if !bytes.Equal(kemRecipient.PublicKey().Bytes(), pubKeyBytes) {
t.Errorf("unexpected KEM sender bytes: got %x, want %x", kemRecipient.PublicKey().Bytes(), pubKeyBytes)
}
ikm := mustDecodeHex(t, vector.IkmR)
derivRecipient, err := kem.DeriveKeyPair(ikm)
if err != nil {
t.Fatal(err)
}
derivRecipientBytes, err := derivRecipient.Bytes()
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(derivRecipientBytes, privKeyBytes) && vector.KEM != DHKEM(ecdh.X25519()).ID() {
t.Errorf("unexpected KEM bytes from seed: got %x, want %x", derivRecipientBytes, privKeyBytes)
}
if !bytes.Equal(derivRecipient.PublicKey().Bytes(), pubKeyBytes) {
t.Errorf("unexpected KEM sender bytes from seed: got %x, want %x", derivRecipient.PublicKey().Bytes(), pubKeyBytes)
}
recipient, err := NewRecipient(encap, kemRecipient, kdf, aead, info)
if err != nil {
t.Fatal(err)
}
if aead != ExportOnly() && len(vector.AccEncryptions) != 0 {
source, sink := sha3.NewSHAKE128(), sha3.NewSHAKE128()
for range 1000 {
aad, plaintext := drawRandomInput(t, source), drawRandomInput(t, source)
ciphertext, err := sender.Seal(aad, plaintext)
if err != nil {
t.Fatal(err)
}
sink.Write(ciphertext)
got, err := recipient.Open(aad, ciphertext)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, plaintext) {
t.Errorf("unexpected plaintext: got %x want %x", got, plaintext)
}
}
encryptions := make([]byte, 16)
sink.Read(encryptions)
expectedEncryptions := mustDecodeHex(t, vector.AccEncryptions)
if !bytes.Equal(encryptions, expectedEncryptions) {
t.Errorf("unexpected accumulated encryptions, got: %x, want %x", encryptions, expectedEncryptions)
}
} else if aead != ExportOnly() {
for _, enc := range vector.Encryptions {
aad := mustDecodeHex(t, enc.Aad)
plaintext := mustDecodeHex(t, enc.Pt)
expectedCiphertext := mustDecodeHex(t, enc.Ct)
ciphertext, err := sender.Seal(aad, plaintext)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(ciphertext, expectedCiphertext) {
t.Errorf("unexpected ciphertext, got: %x, want %x", ciphertext, expectedCiphertext)
}
got, err := recipient.Open(aad, ciphertext)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, plaintext) {
t.Errorf("unexpected plaintext: got %x want %x", got, plaintext)
}
}
} else {
if _, err := sender.Seal(nil, nil); err == nil {
t.Error("expected error from Seal with export-only AEAD")
}
if _, err := recipient.Open(nil, nil); err == nil {
t.Error("expected error from Open with export-only AEAD")
}
}
if len(vector.AccExports) != 0 {
source, sink := sha3.NewSHAKE128(), sha3.NewSHAKE128()
for l := range 1000 {
context := string(drawRandomInput(t, source))
value, err := sender.Export(context, l)
if err != nil {
t.Fatal(err)
}
sink.Write(value)
got, err := recipient.Export(context, l)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, value) {
t.Errorf("recipient: unexpected exported secret: got %x want %x", got, value)
}
}
exports := make([]byte, 16)
sink.Read(exports)
expectedExports := mustDecodeHex(t, vector.AccExports)
if !bytes.Equal(exports, expectedExports) {
t.Errorf("unexpected accumulated exports, got: %x, want %x", exports, expectedExports)
}
} else {
for _, exp := range vector.Exports {
context := string(mustDecodeHex(t, exp.Context))
expectedValue := mustDecodeHex(t, exp.Value)
value, err := sender.Export(context, exp.L)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(value, expectedValue) {
t.Errorf("unexpected exported value, got: %x, want %x", value, expectedValue)
}
got, err := recipient.Export(context, exp.L)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, value) {
t.Errorf("recipient: unexpected exported secret: got %x want %x", got, value)
}
}
}
})
}
}
func drawRandomInput(t *testing.T, r io.Reader) []byte {
t.Helper()
l := make([]byte, 1)
if _, err := r.Read(l); err != nil {
t.Fatal(err)
}
n := int(l[0])
b := make([]byte, n)
if _, err := r.Read(b); err != nil {
t.Fatal(err)
}
return b
}
func TestSingletons(t *testing.T) {
if HKDFSHA256() != HKDFSHA256() {
t.Error("HKDFSHA256() != HKDFSHA256()")
}
if HKDFSHA384() != HKDFSHA384() {
t.Error("HKDFSHA384() != HKDFSHA384()")
}
if HKDFSHA512() != HKDFSHA512() {
t.Error("HKDFSHA512() != HKDFSHA512()")
}
if AES128GCM() != AES128GCM() {
t.Error("AES128GCM() != AES128GCM()")
}
if AES256GCM() != AES256GCM() {
t.Error("AES256GCM() != AES256GCM()")
}
if ChaCha20Poly1305() != ChaCha20Poly1305() {
t.Error("ChaCha20Poly1305() != ChaCha20Poly1305()")
}
if ExportOnly() != ExportOnly() {
t.Error("ExportOnly() != ExportOnly()")
}
if DHKEM(ecdh.P256()) != DHKEM(ecdh.P256()) {
t.Error("DHKEM(P-256) != DHKEM(P-256)")
}
if DHKEM(ecdh.P384()) != DHKEM(ecdh.P384()) {
t.Error("DHKEM(P-384) != DHKEM(P-384)")
}
if DHKEM(ecdh.P521()) != DHKEM(ecdh.P521()) {
t.Error("DHKEM(P-521) != DHKEM(P-521)")
}
if DHKEM(ecdh.X25519()) != DHKEM(ecdh.X25519()) {
t.Error("DHKEM(X25519) != DHKEM(X25519)")
}
if MLKEM768() != MLKEM768() {
t.Error("MLKEM768() != MLKEM768()")
}
if MLKEM1024() != MLKEM1024() {
t.Error("MLKEM1024() != MLKEM1024()")
}
if MLKEM768X25519() != MLKEM768X25519() {
t.Error("MLKEM768X25519() != MLKEM768X25519()")
}
if MLKEM768P256() != MLKEM768P256() {
t.Error("MLKEM768P256() != MLKEM768P256()")
}
if MLKEM1024P384() != MLKEM1024P384() {
t.Error("MLKEM1024P384() != MLKEM1024P384()")
}
}

View File

@@ -0,0 +1,149 @@
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package byteorder provides functions for decoding and encoding
// little and big endian integer types from/to byte slices.
package byteorder
func LEUint16(b []byte) uint16 {
_ = b[1] // bounds check hint to compiler; see golang.org/issue/14808
return uint16(b[0]) | uint16(b[1])<<8
}
func LEPutUint16(b []byte, v uint16) {
_ = b[1] // early bounds check to guarantee safety of writes below
b[0] = byte(v)
b[1] = byte(v >> 8)
}
func LEAppendUint16(b []byte, v uint16) []byte {
return append(b,
byte(v),
byte(v>>8),
)
}
func LEUint32(b []byte) uint32 {
_ = b[3] // bounds check hint to compiler; see golang.org/issue/14808
return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24
}
func LEPutUint32(b []byte, v uint32) {
_ = b[3] // early bounds check to guarantee safety of writes below
b[0] = byte(v)
b[1] = byte(v >> 8)
b[2] = byte(v >> 16)
b[3] = byte(v >> 24)
}
func LEAppendUint32(b []byte, v uint32) []byte {
return append(b,
byte(v),
byte(v>>8),
byte(v>>16),
byte(v>>24),
)
}
func LEUint64(b []byte) uint64 {
_ = b[7] // bounds check hint to compiler; see golang.org/issue/14808
return uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24 |
uint64(b[4])<<32 | uint64(b[5])<<40 | uint64(b[6])<<48 | uint64(b[7])<<56
}
func LEPutUint64(b []byte, v uint64) {
_ = b[7] // early bounds check to guarantee safety of writes below
b[0] = byte(v)
b[1] = byte(v >> 8)
b[2] = byte(v >> 16)
b[3] = byte(v >> 24)
b[4] = byte(v >> 32)
b[5] = byte(v >> 40)
b[6] = byte(v >> 48)
b[7] = byte(v >> 56)
}
func LEAppendUint64(b []byte, v uint64) []byte {
return append(b,
byte(v),
byte(v>>8),
byte(v>>16),
byte(v>>24),
byte(v>>32),
byte(v>>40),
byte(v>>48),
byte(v>>56),
)
}
func BEUint16(b []byte) uint16 {
_ = b[1] // bounds check hint to compiler; see golang.org/issue/14808
return uint16(b[1]) | uint16(b[0])<<8
}
func BEPutUint16(b []byte, v uint16) {
_ = b[1] // early bounds check to guarantee safety of writes below
b[0] = byte(v >> 8)
b[1] = byte(v)
}
func BEAppendUint16(b []byte, v uint16) []byte {
return append(b,
byte(v>>8),
byte(v),
)
}
func BEUint32(b []byte) uint32 {
_ = b[3] // bounds check hint to compiler; see golang.org/issue/14808
return uint32(b[3]) | uint32(b[2])<<8 | uint32(b[1])<<16 | uint32(b[0])<<24
}
func BEPutUint32(b []byte, v uint32) {
_ = b[3] // early bounds check to guarantee safety of writes below
b[0] = byte(v >> 24)
b[1] = byte(v >> 16)
b[2] = byte(v >> 8)
b[3] = byte(v)
}
func BEAppendUint32(b []byte, v uint32) []byte {
return append(b,
byte(v>>24),
byte(v>>16),
byte(v>>8),
byte(v),
)
}
func BEUint64(b []byte) uint64 {
_ = b[7] // bounds check hint to compiler; see golang.org/issue/14808
return uint64(b[7]) | uint64(b[6])<<8 | uint64(b[5])<<16 | uint64(b[4])<<24 |
uint64(b[3])<<32 | uint64(b[2])<<40 | uint64(b[1])<<48 | uint64(b[0])<<56
}
func BEPutUint64(b []byte, v uint64) {
_ = b[7] // early bounds check to guarantee safety of writes below
b[0] = byte(v >> 56)
b[1] = byte(v >> 48)
b[2] = byte(v >> 40)
b[3] = byte(v >> 32)
b[4] = byte(v >> 24)
b[5] = byte(v >> 16)
b[6] = byte(v >> 8)
b[7] = byte(v)
}
func BEAppendUint64(b []byte, v uint64) []byte {
return append(b,
byte(v>>56),
byte(v>>48),
byte(v>>40),
byte(v>>32),
byte(v>>24),
byte(v>>16),
byte(v>>8),
byte(v),
)
}

View File

@@ -0,0 +1,44 @@
//go:build !go1.26 && fips140v1.0
package mlkemtest
import (
"crypto/mlkem"
"errors"
"unsafe"
)
// Reach ungracefully into the internals of crypto/internal/fips140/mlkem to
// perform derandomized encapsulation, which will be exposed in Go 1.26.
func Encapsulate768(ek *mlkem.EncapsulationKey768, rand []byte) (sharedKey, ciphertext []byte, err error) {
if len(rand) != 32 {
return nil, nil, errors.New("invalid ML-KEM-768 randomness size")
}
key := (*mlkem768EncapsulationKey)(unsafe.Pointer(ek))
sharedKey, ciphertext = mlkem768EncapsulateInternal(key.key, (*[32]byte)(rand))
return sharedKey, ciphertext, nil
}
type mlkem768EncapsulationKey struct {
key unsafe.Pointer // *crypto/internal/fips140/v1.0.0-c2097c7c/mlkem.EncapsulationKey768
}
//go:linkname mlkem768EncapsulateInternal crypto/internal/fips140/v1.0.0-c2097c7c/mlkem.(*EncapsulationKey768).EncapsulateInternal
func mlkem768EncapsulateInternal(ek unsafe.Pointer, m *[32]byte) (sharedKey, ciphertext []byte)
func Encapsulate1024(ek *mlkem.EncapsulationKey1024, rand []byte) (sharedKey, ciphertext []byte, err error) {
if len(rand) != 32 {
return nil, nil, errors.New("invalid ML-KEM-1024 randomness size")
}
key := (*mlkem1024EncapsulationKey)(unsafe.Pointer(ek))
sharedKey, ciphertext = mlkem1024EncapsulateInternal(key.key, (*[32]byte)(rand))
return sharedKey, ciphertext, nil
}
type mlkem1024EncapsulationKey struct {
key unsafe.Pointer // *crypto/internal/fips140/v1.0.0-c2097c7c/mlkem.EncapsulationKey1024
}
//go:linkname mlkem1024EncapsulateInternal crypto/internal/fips140/v1.0.0-c2097c7c/mlkem.(*EncapsulationKey1024).EncapsulateInternal
func mlkem1024EncapsulateInternal(ek unsafe.Pointer, m *[32]byte) (sharedKey, ciphertext []byte)

View File

@@ -0,0 +1,16 @@
//go:build go1.26
package mlkemtest
import (
"crypto/mlkem"
"crypto/mlkem/mlkemtest"
)
func Encapsulate768(ek *mlkem.EncapsulationKey768, rand []byte) (sharedKey, ciphertext []byte, err error) {
return mlkemtest.Encapsulate768(ek, rand)
}
func Encapsulate1024(ek *mlkem.EncapsulationKey1024, rand []byte) (sharedKey, ciphertext []byte, err error) {
return mlkemtest.Encapsulate1024(ek, rand)
}

156
kdf.go Normal file
View File

@@ -0,0 +1,156 @@
// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package hpke
import (
"crypto/hkdf"
"crypto/sha256"
"crypto/sha3"
"crypto/sha512"
"errors"
"fmt"
"hash"
"sources.truenas.cloud/code/hpke/internal/byteorder"
)
// The KDF is one of the three components of an HPKE ciphersuite, implementing
// key derivation.
type KDF interface {
ID() uint16
oneStage() bool
size() int // Nh
labeledDerive(suiteID, inputKey []byte, label string, context []byte, length uint16) ([]byte, error)
labeledExtract(suiteID, salt []byte, label string, inputKey []byte) ([]byte, error)
labeledExpand(suiteID, randomKey []byte, label string, info []byte, length uint16) ([]byte, error)
}
// NewKDF returns the KDF implementation for the given KDF ID.
//
// Applications are encouraged to use specific implementations like [HKDFSHA256]
// instead, unless runtime agility is required.
func NewKDF(id uint16) (KDF, error) {
switch id {
case 0x0001: // HKDF-SHA256
return HKDFSHA256(), nil
case 0x0002: // HKDF-SHA384
return HKDFSHA384(), nil
case 0x0003: // HKDF-SHA512
return HKDFSHA512(), nil
case 0x0010: // SHAKE128
return SHAKE128(), nil
case 0x0011: // SHAKE256
return SHAKE256(), nil
default:
return nil, fmt.Errorf("unsupported KDF %04x", id)
}
}
// HKDFSHA256 returns an HKDF-SHA256 KDF implementation.
func HKDFSHA256() KDF { return hkdfSHA256 }
// HKDFSHA384 returns an HKDF-SHA384 KDF implementation.
func HKDFSHA384() KDF { return hkdfSHA384 }
// HKDFSHA512 returns an HKDF-SHA512 KDF implementation.
func HKDFSHA512() KDF { return hkdfSHA512 }
type hkdfKDF struct {
hash func() hash.Hash
id uint16
nH int
}
var hkdfSHA256 = &hkdfKDF{hash: sha256.New, id: 0x0001, nH: sha256.Size}
var hkdfSHA384 = &hkdfKDF{hash: sha512.New384, id: 0x0002, nH: sha512.Size384}
var hkdfSHA512 = &hkdfKDF{hash: sha512.New, id: 0x0003, nH: sha512.Size}
func (kdf *hkdfKDF) ID() uint16 {
return kdf.id
}
func (kdf *hkdfKDF) size() int {
return kdf.nH
}
func (kdf *hkdfKDF) oneStage() bool {
return false
}
func (kdf *hkdfKDF) labeledDerive(_, _ []byte, _ string, _ []byte, _ uint16) ([]byte, error) {
return nil, errors.New("hpke: internal error: labeledDerive called on two-stage KDF")
}
func (kdf *hkdfKDF) labeledExtract(suiteID []byte, salt []byte, label string, inputKey []byte) ([]byte, error) {
labeledIKM := make([]byte, 0, 7+len(suiteID)+len(label)+len(inputKey))
labeledIKM = append(labeledIKM, []byte("HPKE-v1")...)
labeledIKM = append(labeledIKM, suiteID...)
labeledIKM = append(labeledIKM, label...)
labeledIKM = append(labeledIKM, inputKey...)
return hkdf.Extract(kdf.hash, labeledIKM, salt)
}
func (kdf *hkdfKDF) labeledExpand(suiteID []byte, randomKey []byte, label string, info []byte, length uint16) ([]byte, error) {
labeledInfo := make([]byte, 0, 2+7+len(suiteID)+len(label)+len(info))
labeledInfo = byteorder.BEAppendUint16(labeledInfo, length)
labeledInfo = append(labeledInfo, []byte("HPKE-v1")...)
labeledInfo = append(labeledInfo, suiteID...)
labeledInfo = append(labeledInfo, label...)
labeledInfo = append(labeledInfo, info...)
return hkdf.Expand(kdf.hash, randomKey, string(labeledInfo), int(length))
}
// SHAKE128 returns a SHAKE128 KDF implementation.
func SHAKE128() KDF {
return shake128KDF
}
// SHAKE256 returns a SHAKE256 KDF implementation.
func SHAKE256() KDF {
return shake256KDF
}
type shakeKDF struct {
hash func() *sha3.SHAKE
id uint16
nH int
}
var shake128KDF = &shakeKDF{hash: sha3.NewSHAKE128, id: 0x0010, nH: 32}
var shake256KDF = &shakeKDF{hash: sha3.NewSHAKE256, id: 0x0011, nH: 64}
func (kdf *shakeKDF) ID() uint16 {
return kdf.id
}
func (kdf *shakeKDF) size() int {
return kdf.nH
}
func (kdf *shakeKDF) oneStage() bool {
return true
}
func (kdf *shakeKDF) labeledDerive(suiteID, inputKey []byte, label string, context []byte, length uint16) ([]byte, error) {
H := kdf.hash()
H.Write(inputKey)
H.Write([]byte("HPKE-v1"))
H.Write(suiteID)
H.Write([]byte{byte(len(label) >> 8), byte(len(label))})
H.Write([]byte(label))
H.Write([]byte{byte(length >> 8), byte(length)})
H.Write(context)
out := make([]byte, length)
H.Read(out)
return out, nil
}
func (kdf *shakeKDF) labeledExtract(_, _ []byte, _ string, _ []byte) ([]byte, error) {
return nil, errors.New("hpke: internal error: labeledExtract called on one-stage KDF")
}
func (kdf *shakeKDF) labeledExpand(_, _ []byte, _ string, _ []byte, _ uint16) ([]byte, error) {
return nil, errors.New("hpke: internal error: labeledExpand called on one-stage KDF")
}

388
kem.go Normal file
View File

@@ -0,0 +1,388 @@
// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package hpke
import (
"crypto/rand"
"errors"
"sources.truenas.cloud/code/hpke/crypto/ecdh"
"sources.truenas.cloud/code/hpke/internal/byteorder"
)
// A KEM is a Key Encapsulation Mechanism, one of the three components of an
// HPKE ciphersuite.
type KEM interface {
// ID returns the HPKE KEM identifier.
ID() uint16
// GenerateKey generates a new key pair.
GenerateKey() (PrivateKey, error)
// NewPublicKey deserializes a public key from bytes.
//
// It implements DeserializePublicKey, as defined in RFC 9180.
NewPublicKey([]byte) (PublicKey, error)
// NewPrivateKey deserializes a private key from bytes.
//
// It implements DeserializePrivateKey, as defined in RFC 9180.
NewPrivateKey([]byte) (PrivateKey, error)
// DeriveKeyPair derives a key pair from the given input keying material.
//
// It implements DeriveKeyPair, as defined in RFC 9180.
DeriveKeyPair(ikm []byte) (PrivateKey, error)
encSize() int
}
// NewKEM returns the KEM implementation for the given KEM ID.
//
// Applications are encouraged to use specific implementations like [DHKEM] or
// [MLKEM768X25519] instead, unless runtime agility is required.
func NewKEM(id uint16) (KEM, error) {
switch id {
case 0x0010: // DHKEM(P-256, HKDF-SHA256)
return DHKEM(ecdh.P256()), nil
case 0x0011: // DHKEM(P-384, HKDF-SHA384)
return DHKEM(ecdh.P384()), nil
case 0x0012: // DHKEM(P-521, HKDF-SHA512)
return DHKEM(ecdh.P521()), nil
case 0x0020: // DHKEM(X25519, HKDF-SHA256)
return DHKEM(ecdh.X25519()), nil
case 0x0041: // ML-KEM-768
return MLKEM768(), nil
case 0x0042: // ML-KEM-1024
return MLKEM1024(), nil
case 0x647a: // MLKEM768-X25519
return MLKEM768X25519(), nil
case 0x0050: // MLKEM768-P256
return MLKEM768P256(), nil
case 0x0051: // MLKEM1024-P384
return MLKEM1024P384(), nil
default:
return nil, errors.New("unsupported KEM")
}
}
// A PublicKey is an instantiation of a KEM (one of the three components of an
// HPKE ciphersuite) with an encapsulation key (i.e. the public key).
//
// A PublicKey is usually obtained from a method of the corresponding [KEM] or
// [PrivateKey], such as [KEM.NewPublicKey] or [PrivateKey.PublicKey].
type PublicKey interface {
// KEM returns the instantiated KEM.
KEM() KEM
// Bytes returns the public key as the output of SerializePublicKey.
Bytes() []byte
// encap performs KEM encapsulation, producing a shared secret and
// ciphertext (i.e. the encapsulated key). The testingRandomness parameter,
// if not nil, is used to perform deterministic encapsulation for testing.
encap(testingRandomness []byte) (sharedSecret, enc []byte, err error)
}
// A PrivateKey is an instantiation of a KEM (one of the three components of
// an HPKE ciphersuite) with a decapsulation key (i.e. the secret key).
//
// A PrivateKey is usually obtained from a method of the corresponding [KEM],
// such as [KEM.GenerateKey] or [KEM.NewPrivateKey].
type PrivateKey interface {
// KEM returns the instantiated KEM.
KEM() KEM
// Bytes returns the private key as the output of SerializePrivateKey, as
// defined in RFC 9180.
//
// Note that for X25519 this might not match the input to NewPrivateKey.
// This is a requirement of RFC 9180, Section 7.1.2.
Bytes() ([]byte, error)
// PublicKey returns the corresponding PublicKey.
PublicKey() PublicKey
decap(enc []byte) (sharedSecret []byte, err error)
}
type dhKEM struct {
kdf KDF
id uint16
curve ecdh.Curve
Nsecret uint16
Nsk uint16
Nenc int
}
func (kem *dhKEM) extractAndExpand(dhKey, kemContext []byte) ([]byte, error) {
suiteID := byteorder.BEAppendUint16([]byte("KEM"), kem.id)
eaePRK, err := kem.kdf.labeledExtract(suiteID, nil, "eae_prk", dhKey)
if err != nil {
return nil, err
}
return kem.kdf.labeledExpand(suiteID, eaePRK, "shared_secret", kemContext, kem.Nsecret)
}
func (kem *dhKEM) ID() uint16 {
return kem.id
}
func (kem *dhKEM) encSize() int {
return kem.Nenc
}
var dhKEMP256 = &dhKEM{HKDFSHA256(), 0x0010, ecdh.P256(), 32, 32, 65}
var dhKEMP384 = &dhKEM{HKDFSHA384(), 0x0011, ecdh.P384(), 48, 48, 97}
var dhKEMP521 = &dhKEM{HKDFSHA512(), 0x0012, ecdh.P521(), 64, 66, 133}
var dhKEMX25519 = &dhKEM{HKDFSHA256(), 0x0020, ecdh.X25519(), 32, 32, 32}
// DHKEM returns a KEM implementing one of
//
// - DHKEM(P-256, HKDF-SHA256)
// - DHKEM(P-384, HKDF-SHA384)
// - DHKEM(P-521, HKDF-SHA512)
// - DHKEM(X25519, HKDF-SHA256)
//
// depending on curve.
func DHKEM(curve ecdh.Curve) KEM {
switch curve {
case ecdh.P256():
return dhKEMP256
case ecdh.P384():
return dhKEMP384
case ecdh.P521():
return dhKEMP521
case ecdh.X25519():
return dhKEMX25519
default:
// The set of ecdh.Curve implementations is closed, because the
// interface has unexported methods. Therefore, this default case is
// only hit if a new curve is added that DHKEM doesn't support.
return unsupportedCurveKEM{}
}
}
type unsupportedCurveKEM struct{}
func (unsupportedCurveKEM) ID() uint16 {
return 0
}
func (unsupportedCurveKEM) GenerateKey() (PrivateKey, error) {
return nil, errors.New("unsupported curve")
}
func (unsupportedCurveKEM) NewPublicKey([]byte) (PublicKey, error) {
return nil, errors.New("unsupported curve")
}
func (unsupportedCurveKEM) NewPrivateKey([]byte) (PrivateKey, error) {
return nil, errors.New("unsupported curve")
}
func (unsupportedCurveKEM) DeriveKeyPair([]byte) (PrivateKey, error) {
return nil, errors.New("unsupported curve")
}
func (unsupportedCurveKEM) encSize() int {
return 0
}
type dhKEMPublicKey struct {
kem *dhKEM
pub *ecdh.PublicKey
}
// NewDHKEMPublicKey returns a PublicKey implementing
//
// - DHKEM(P-256, HKDF-SHA256)
// - DHKEM(P-384, HKDF-SHA384)
// - DHKEM(P-521, HKDF-SHA512)
// - DHKEM(X25519, HKDF-SHA256)
//
// depending on the underlying curve of pub ([ecdh.X25519], [ecdh.P256],
// [ecdh.P384], or [ecdh.P521]).
//
// This function is meant for applications that already have an instantiated
// crypto/ecdh public key. Otherwise, applications should use the
// [KEM.NewPublicKey] method of [DHKEM].
func NewDHKEMPublicKey(pub *ecdh.PublicKey) (PublicKey, error) {
kem, ok := DHKEM(pub.Curve()).(*dhKEM)
if !ok {
return nil, errors.New("unsupported curve")
}
return &dhKEMPublicKey{
kem: kem,
pub: pub,
}, nil
}
func (kem *dhKEM) NewPublicKey(data []byte) (PublicKey, error) {
pub, err := kem.curve.NewPublicKey(data)
if err != nil {
return nil, err
}
return NewDHKEMPublicKey(pub)
}
func (pk *dhKEMPublicKey) KEM() KEM {
return pk.kem
}
func (pk *dhKEMPublicKey) Bytes() []byte {
return pk.pub.Bytes()
}
func (pk *dhKEMPublicKey) encap(testingRandomness []byte) (ss []byte, enc []byte, err error) {
var privEph ecdh.KeyExchanger
if testingRandomness == nil {
privEph, err = pk.pub.Curve().GenerateKey(rand.Reader)
if err != nil {
return nil, nil, err
}
} else {
r, err := pk.KEM().DeriveKeyPair(testingRandomness)
if err != nil {
return nil, nil, err
}
privEph = r.(*dhKEMPrivateKey).priv
}
dhVal, err := privEph.ECDH(pk.pub)
if err != nil {
return nil, nil, err
}
encPubEph := privEph.PublicKey().Bytes()
encPubRecip := pk.pub.Bytes()
kemContext := append(encPubEph, encPubRecip...)
ss, err = pk.kem.extractAndExpand(dhVal, kemContext)
if err != nil {
return nil, nil, err
}
return ss, encPubEph, nil
}
type dhKEMPrivateKey struct {
kem *dhKEM
priv ecdh.KeyExchanger
}
// NewDHKEMPrivateKey returns a PrivateKey implementing
//
// - DHKEM(P-256, HKDF-SHA256)
// - DHKEM(P-384, HKDF-SHA384)
// - DHKEM(P-521, HKDF-SHA512)
// - DHKEM(X25519, HKDF-SHA256)
//
// depending on the underlying curve of priv ([ecdh.X25519], [ecdh.P256],
// [ecdh.P384], or [ecdh.P521]).
//
// This function is meant for applications that already have an instantiated
// crypto/ecdh private key, or another implementation of a [ecdh.KeyExchanger]
// (e.g. a hardware key). Otherwise, applications should use the
// [KEM.NewPrivateKey] method of [DHKEM].
func NewDHKEMPrivateKey(priv ecdh.KeyExchanger) (PrivateKey, error) {
kem, ok := DHKEM(priv.Curve()).(*dhKEM)
if !ok {
return nil, errors.New("unsupported curve")
}
return &dhKEMPrivateKey{
kem: kem,
priv: priv,
}, nil
}
func (kem *dhKEM) GenerateKey() (PrivateKey, error) {
priv, err := kem.curve.GenerateKey(rand.Reader)
if err != nil {
return nil, err
}
return NewDHKEMPrivateKey(priv)
}
func (kem *dhKEM) NewPrivateKey(ikm []byte) (PrivateKey, error) {
priv, err := kem.curve.NewPrivateKey(ikm)
if err != nil {
return nil, err
}
return NewDHKEMPrivateKey(priv)
}
func (kem *dhKEM) DeriveKeyPair(ikm []byte) (PrivateKey, error) {
// DeriveKeyPair from RFC 9180 Section 7.1.3.
suiteID := byteorder.BEAppendUint16([]byte("KEM"), kem.id)
prk, err := kem.kdf.labeledExtract(suiteID, nil, "dkp_prk", ikm)
if err != nil {
return nil, err
}
if kem == dhKEMX25519 {
s, err := kem.kdf.labeledExpand(suiteID, prk, "sk", nil, kem.Nsk)
if err != nil {
return nil, err
}
return kem.NewPrivateKey(s)
}
var counter uint8
for counter < 4 {
s, err := kem.kdf.labeledExpand(suiteID, prk, "candidate", []byte{counter}, kem.Nsk)
if err != nil {
return nil, err
}
if kem == dhKEMP521 {
s[0] &= 0x01
}
r, err := kem.NewPrivateKey(s)
if err != nil {
counter++
continue
}
return r, nil
}
panic("chance of four rejections is < 2^-128")
}
func (k *dhKEMPrivateKey) KEM() KEM {
return k.kem
}
func (k *dhKEMPrivateKey) Bytes() ([]byte, error) {
// Bizarrely, RFC 9180, Section 7.1.2 says SerializePrivateKey MUST clamp
// the output, which I thought we all agreed to instead do as part of the DH
// function, letting private keys be random bytes.
//
// At the same time, it says DeserializePrivateKey MUST also clamp, implying
// that the input doesn't have to be clamped, so Bytes by spec doesn't
// necessarily match the NewPrivateKey input.
//
// I'm sure this will not lead to any unexpected behavior or interop issue.
priv, ok := k.priv.(*ecdh.PrivateKey)
if !ok {
return nil, errors.New("ecdh: private key does not support Bytes")
}
if k.kem == dhKEMX25519 {
b := priv.Bytes()
b[0] &= 248
b[31] &= 127
b[31] |= 64
return b, nil
}
return priv.Bytes(), nil
}
func (k *dhKEMPrivateKey) PublicKey() PublicKey {
return &dhKEMPublicKey{
kem: k.kem,
pub: k.priv.PublicKey(),
}
}
func (k *dhKEMPrivateKey) decap(encPubEph []byte) ([]byte, error) {
pubEph, err := k.priv.Curve().NewPublicKey(encPubEph)
if err != nil {
return nil, err
}
dhVal, err := k.priv.ECDH(pubEph)
if err != nil {
return nil, err
}
kemContext := append(encPubEph, k.priv.PublicKey().Bytes()...)
return k.kem.extractAndExpand(dhVal, kemContext)
}

567
pq.go Normal file
View File

@@ -0,0 +1,567 @@
// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package hpke
import (
"bytes"
"crypto/mlkem"
"crypto/rand"
"crypto/sha3"
"errors"
"sources.truenas.cloud/code/hpke/crypto"
"sources.truenas.cloud/code/hpke/crypto/ecdh"
"sources.truenas.cloud/code/hpke/internal/byteorder"
"sources.truenas.cloud/code/hpke/internal/mlkemtest"
)
var mlkem768X25519 = &hybridKEM{
id: 0x647a,
label: /**/ `\./` +
/* */ `/^\`,
curve: ecdh.X25519(),
curveSeedSize: 32,
curvePointSize: 32,
pqEncapsKeySize: mlkem.EncapsulationKeySize768,
pqCiphertextSize: mlkem.CiphertextSize768,
pqNewPublicKey: func(data []byte) (crypto.Encapsulator, error) {
return mlkem.NewEncapsulationKey768(data)
},
pqNewPrivateKey: func(data []byte) (crypto.Decapsulator, error) {
return wrapDecapsulator(mlkem.NewDecapsulationKey768(data))
},
}
// MLKEM768X25519 returns a KEM implementing MLKEM768-X25519 (a.k.a. X-Wing)
// from draft-ietf-hpke-pq.
func MLKEM768X25519() KEM {
return mlkem768X25519
}
var mlkem768P256 = &hybridKEM{
id: 0x0050,
label: "MLKEM768-P256",
curve: ecdh.P256(),
curveSeedSize: 32,
curvePointSize: 65,
pqEncapsKeySize: mlkem.EncapsulationKeySize768,
pqCiphertextSize: mlkem.CiphertextSize768,
pqNewPublicKey: func(data []byte) (crypto.Encapsulator, error) {
return mlkem.NewEncapsulationKey768(data)
},
pqNewPrivateKey: func(data []byte) (crypto.Decapsulator, error) {
return wrapDecapsulator(mlkem.NewDecapsulationKey768(data))
},
}
// MLKEM768P256 returns a KEM implementing MLKEM768-P256 from draft-ietf-hpke-pq.
func MLKEM768P256() KEM {
return mlkem768P256
}
var mlkem1024P384 = &hybridKEM{
id: 0x0051,
label: "MLKEM1024-P384",
curve: ecdh.P384(),
curveSeedSize: 48,
curvePointSize: 97,
pqEncapsKeySize: mlkem.EncapsulationKeySize1024,
pqCiphertextSize: mlkem.CiphertextSize1024,
pqNewPublicKey: func(data []byte) (crypto.Encapsulator, error) {
return mlkem.NewEncapsulationKey1024(data)
},
pqNewPrivateKey: func(data []byte) (crypto.Decapsulator, error) {
return wrapDecapsulator(mlkem.NewDecapsulationKey1024(data))
},
}
// MLKEM1024P384 returns a KEM implementing MLKEM1024-P384 from draft-ietf-hpke-pq.
func MLKEM1024P384() KEM {
return mlkem1024P384
}
type hybridKEM struct {
id uint16
label string
curve ecdh.Curve
curveSeedSize int
curvePointSize int
pqEncapsKeySize int
pqCiphertextSize int
pqNewPublicKey func(data []byte) (crypto.Encapsulator, error)
pqNewPrivateKey func(data []byte) (crypto.Decapsulator, error)
}
func (kem *hybridKEM) ID() uint16 {
return kem.id
}
func (kem *hybridKEM) encSize() int {
return kem.pqCiphertextSize + kem.curvePointSize
}
func (kem *hybridKEM) sharedSecret(ssPQ, ssT, ctT, ekT []byte) []byte {
h := sha3.New256()
h.Write(ssPQ)
h.Write(ssT)
h.Write(ctT)
h.Write(ekT)
h.Write([]byte(kem.label))
return h.Sum(nil)
}
type hybridPublicKey struct {
kem *hybridKEM
t *ecdh.PublicKey
pq crypto.Encapsulator
}
// NewHybridPublicKey returns a PublicKey implementing one of
//
// - MLKEM768-X25519 (a.k.a. X-Wing)
// - MLKEM768-P256
// - MLKEM1024-P384
//
// from draft-ietf-hpke-pq, depending on the underlying curve of t
// ([ecdh.X25519], [ecdh.P256], or [ecdh.P384]) and the type of pq (either
// *[mlkem.EncapsulationKey768] or *[mlkem.EncapsulationKey1024]).
//
// This function is meant for applications that already have instantiated
// crypto/ecdh and crypto/mlkem public keys. Otherwise, applications should use
// the [KEM.NewPublicKey] method of e.g. [MLKEM768X25519].
func NewHybridPublicKey(pq crypto.Encapsulator, t *ecdh.PublicKey) (PublicKey, error) {
switch t.Curve() {
case ecdh.X25519():
if _, ok := pq.(*mlkem.EncapsulationKey768); !ok {
return nil, errors.New("invalid PQ KEM for X25519 hybrid")
}
return &hybridPublicKey{mlkem768X25519, t, pq}, nil
case ecdh.P256():
if _, ok := pq.(*mlkem.EncapsulationKey768); !ok {
return nil, errors.New("invalid PQ KEM for P-256 hybrid")
}
return &hybridPublicKey{mlkem768P256, t, pq}, nil
case ecdh.P384():
if _, ok := pq.(*mlkem.EncapsulationKey1024); !ok {
return nil, errors.New("invalid PQ KEM for P-384 hybrid")
}
return &hybridPublicKey{mlkem1024P384, t, pq}, nil
default:
return nil, errors.New("unsupported curve")
}
}
func (kem *hybridKEM) NewPublicKey(data []byte) (PublicKey, error) {
if len(data) != kem.pqEncapsKeySize+kem.curvePointSize {
return nil, errors.New("invalid public key size")
}
pq, err := kem.pqNewPublicKey(data[:kem.pqEncapsKeySize])
if err != nil {
return nil, err
}
k, err := kem.curve.NewPublicKey(data[kem.pqEncapsKeySize:])
if err != nil {
return nil, err
}
return NewHybridPublicKey(pq, k)
}
func (pk *hybridPublicKey) KEM() KEM {
return pk.kem
}
func (pk *hybridPublicKey) Bytes() []byte {
return append(pk.pq.Bytes(), pk.t.Bytes()...)
}
func (pk *hybridPublicKey) encap(testingRandomness []byte) (ss []byte, enc []byte, err error) {
var ssPQ, ctPQ []byte
if testingRandomness == nil {
ssPQ, ctPQ = pk.pq.Encapsulate()
} else {
if len(testingRandomness) < 32 {
return nil, nil, errors.New("insufficient testing randomness")
}
switch ek := pk.pq.(type) {
case *mlkem.EncapsulationKey768:
ssPQ, ctPQ, err = mlkemtest.Encapsulate768(ek, testingRandomness[:32])
case *mlkem.EncapsulationKey1024:
ssPQ, ctPQ, err = mlkemtest.Encapsulate1024(ek, testingRandomness[:32])
default:
return nil, nil, errors.New("internal error: unsupported public key type")
}
if err != nil {
return nil, nil, err
}
testingRandomness = testingRandomness[32:]
}
var skE ecdh.KeyExchanger
if testingRandomness == nil {
skE, err = pk.t.Curve().GenerateKey(rand.Reader)
if err != nil {
return nil, nil, err
}
} else {
for len(testingRandomness) >= pk.kem.curveSeedSize {
seedT := testingRandomness[:pk.kem.curveSeedSize]
testingRandomness = testingRandomness[pk.kem.curveSeedSize:]
skE, err = pk.t.Curve().NewPrivateKey(seedT)
if err != nil {
continue
}
break
}
}
ssT, err := skE.ECDH(pk.t)
if err != nil {
return nil, nil, err
}
ctT := skE.PublicKey().Bytes()
ss = pk.kem.sharedSecret(ssPQ, ssT, ctT, pk.t.Bytes())
enc = append(ctPQ, ctT...)
return ss, enc, nil
}
type hybridPrivateKey struct {
kem *hybridKEM
seed []byte // can be nil
t ecdh.KeyExchanger
pq crypto.Decapsulator
}
// NewHybridPrivateKey returns a PrivateKey implementing
//
// - MLKEM768-X25519 (a.k.a. X-Wing)
// - MLKEM768-P256
// - MLKEM1024-P384
//
// from draft-ietf-hpke-pq, depending on the underlying curve of t
// ([ecdh.X25519], [ecdh.P256], or [ecdh.P384]) and the type of pq.Encapsulator()
// (either *[mlkem.EncapsulationKey768] or *[mlkem.EncapsulationKey1024]).
//
// This function is meant for applications that already have instantiated
// crypto/ecdh and crypto/mlkem private keys, or another implementation of a
// [ecdh.KeyExchanger] and [crypto.Decapsulator] (e.g. a hardware key).
// Otherwise, applications should use the [KEM.NewPrivateKey] method of e.g.
// [MLKEM768X25519].
func NewHybridPrivateKey(pq crypto.Decapsulator, t ecdh.KeyExchanger) (PrivateKey, error) {
return newHybridPrivateKey(pq, t, nil)
}
func (kem *hybridKEM) GenerateKey() (PrivateKey, error) {
seed := make([]byte, 32)
rand.Read(seed)
return kem.NewPrivateKey(seed)
}
func (kem *hybridKEM) NewPrivateKey(priv []byte) (PrivateKey, error) {
if len(priv) != 32 {
return nil, errors.New("hpke: invalid hybrid KEM secret length")
}
s := sha3.NewSHAKE256()
s.Write(priv)
seedPQ := make([]byte, mlkem.SeedSize)
s.Read(seedPQ)
pq, err := kem.pqNewPrivateKey(seedPQ)
if err != nil {
return nil, err
}
seedT := make([]byte, kem.curveSeedSize)
for {
s.Read(seedT)
k, err := kem.curve.NewPrivateKey(seedT)
if err != nil {
continue
}
return newHybridPrivateKey(pq, k, priv)
}
}
func newHybridPrivateKey(pq crypto.Decapsulator, t ecdh.KeyExchanger, seed []byte) (PrivateKey, error) {
switch t.Curve() {
case ecdh.X25519():
if _, ok := pq.Encapsulator().(*mlkem.EncapsulationKey768); !ok {
return nil, errors.New("invalid PQ KEM for X25519 hybrid")
}
return &hybridPrivateKey{mlkem768X25519, bytes.Clone(seed), t, pq}, nil
case ecdh.P256():
if _, ok := pq.Encapsulator().(*mlkem.EncapsulationKey768); !ok {
return nil, errors.New("invalid PQ KEM for P-256 hybrid")
}
return &hybridPrivateKey{mlkem768P256, bytes.Clone(seed), t, pq}, nil
case ecdh.P384():
if _, ok := pq.Encapsulator().(*mlkem.EncapsulationKey1024); !ok {
return nil, errors.New("invalid PQ KEM for P-384 hybrid")
}
return &hybridPrivateKey{mlkem1024P384, bytes.Clone(seed), t, pq}, nil
default:
return nil, errors.New("unsupported curve")
}
}
func (kem *hybridKEM) DeriveKeyPair(ikm []byte) (PrivateKey, error) {
suiteID := byteorder.BEAppendUint16([]byte("KEM"), kem.id)
dk, err := SHAKE256().labeledDerive(suiteID, ikm, "DeriveKeyPair", nil, 32)
if err != nil {
return nil, err
}
return kem.NewPrivateKey(dk)
}
func (k *hybridPrivateKey) KEM() KEM {
return k.kem
}
func (k *hybridPrivateKey) Bytes() ([]byte, error) {
if k.seed == nil {
return nil, errors.New("private key seed not available")
}
return k.seed, nil
}
func (k *hybridPrivateKey) PublicKey() PublicKey {
return &hybridPublicKey{
kem: k.kem,
t: k.t.PublicKey(),
pq: k.pq.Encapsulator(),
}
}
func (k *hybridPrivateKey) decap(enc []byte) ([]byte, error) {
if len(enc) != k.kem.pqCiphertextSize+k.kem.curvePointSize {
return nil, errors.New("invalid encapsulated key size")
}
ctPQ, ctT := enc[:k.kem.pqCiphertextSize], enc[k.kem.pqCiphertextSize:]
ssPQ, err := k.pq.Decapsulate(ctPQ)
if err != nil {
return nil, err
}
pub, err := k.t.Curve().NewPublicKey(ctT)
if err != nil {
return nil, err
}
ssT, err := k.t.ECDH(pub)
if err != nil {
return nil, err
}
ss := k.kem.sharedSecret(ssPQ, ssT, ctT, k.t.PublicKey().Bytes())
return ss, nil
}
var mlkem768 = &mlkemKEM{
id: 0x0041,
ciphertextSize: mlkem.CiphertextSize768,
newPublicKey: func(data []byte) (crypto.Encapsulator, error) {
return mlkem.NewEncapsulationKey768(data)
},
newPrivateKey: func(data []byte) (crypto.Decapsulator, error) {
return wrapDecapsulator(mlkem.NewDecapsulationKey768(data))
},
generateKey: func() (crypto.Decapsulator, error) {
return wrapDecapsulator(mlkem.GenerateKey768())
},
}
// MLKEM768 returns a KEM implementing ML-KEM-768 from draft-ietf-hpke-pq.
func MLKEM768() KEM {
return mlkem768
}
var mlkem1024 = &mlkemKEM{
id: 0x0042,
ciphertextSize: mlkem.CiphertextSize1024,
newPublicKey: func(data []byte) (crypto.Encapsulator, error) {
return mlkem.NewEncapsulationKey1024(data)
},
newPrivateKey: func(data []byte) (crypto.Decapsulator, error) {
return wrapDecapsulator(mlkem.NewDecapsulationKey1024(data))
},
generateKey: func() (crypto.Decapsulator, error) {
return wrapDecapsulator(mlkem.GenerateKey1024())
},
}
// MLKEM1024 returns a KEM implementing ML-KEM-1024 from draft-ietf-hpke-pq.
func MLKEM1024() KEM {
return mlkem1024
}
type mlkemKEM struct {
id uint16
ciphertextSize int
newPublicKey func(data []byte) (crypto.Encapsulator, error)
newPrivateKey func(data []byte) (crypto.Decapsulator, error)
generateKey func() (crypto.Decapsulator, error)
}
func (kem *mlkemKEM) ID() uint16 {
return kem.id
}
func (kem *mlkemKEM) encSize() int {
return kem.ciphertextSize
}
type mlkemPublicKey struct {
kem *mlkemKEM
pq crypto.Encapsulator
}
// NewMLKEMPublicKey returns a KEMPublicKey implementing
//
// - ML-KEM-768
// - ML-KEM-1024
//
// from draft-ietf-hpke-pq, depending on the type of pub
// (*[mlkem.EncapsulationKey768] or *[mlkem.EncapsulationKey1024]).
//
// This function is meant for applications that already have an instantiated
// crypto/mlkem public key. Otherwise, applications should use the
// [KEM.NewPublicKey] method of e.g. [MLKEM768].
func NewMLKEMPublicKey(pub crypto.Encapsulator) (PublicKey, error) {
switch pub.(type) {
case *mlkem.EncapsulationKey768:
return &mlkemPublicKey{mlkem768, pub}, nil
case *mlkem.EncapsulationKey1024:
return &mlkemPublicKey{mlkem1024, pub}, nil
default:
return nil, errors.New("unsupported public key type")
}
}
func (kem *mlkemKEM) NewPublicKey(data []byte) (PublicKey, error) {
pq, err := kem.newPublicKey(data)
if err != nil {
return nil, err
}
return NewMLKEMPublicKey(pq)
}
func (pk *mlkemPublicKey) KEM() KEM {
return pk.kem
}
func (pk *mlkemPublicKey) Bytes() []byte {
return pk.pq.Bytes()
}
func (pk *mlkemPublicKey) encap(testingRandomness []byte) (ss []byte, enc []byte, err error) {
if testingRandomness != nil {
switch ek := pk.pq.(type) {
case *mlkem.EncapsulationKey768:
return mlkemtest.Encapsulate768(ek, testingRandomness)
case *mlkem.EncapsulationKey1024:
return mlkemtest.Encapsulate1024(ek, testingRandomness)
default:
return nil, nil, errors.New("internal error: unsupported public key type")
}
}
ss, enc = pk.pq.Encapsulate()
return ss, enc, nil
}
type mlkemPrivateKey struct {
kem *mlkemKEM
pq crypto.Decapsulator
}
// NewMLKEMPrivateKey returns a KEMPrivateKey implementing
//
// - ML-KEM-768
// - ML-KEM-1024
//
// from draft-ietf-hpke-pq, depending on the type of priv.Encapsulator()
// (either *[mlkem.EncapsulationKey768] or *[mlkem.EncapsulationKey1024]).
//
// This function is meant for applications that already have an instantiated
// crypto/mlkem private key. Otherwise, applications should use the
// [KEM.NewPrivateKey] method of e.g. [MLKEM768].
func NewMLKEMPrivateKey(priv crypto.Decapsulator) (PrivateKey, error) {
switch priv.Encapsulator().(type) {
case *mlkem.EncapsulationKey768:
return &mlkemPrivateKey{mlkem768, priv}, nil
case *mlkem.EncapsulationKey1024:
return &mlkemPrivateKey{mlkem1024, priv}, nil
default:
return nil, errors.New("unsupported public key type")
}
}
func (kem *mlkemKEM) GenerateKey() (PrivateKey, error) {
pq, err := kem.generateKey()
if err != nil {
return nil, err
}
return NewMLKEMPrivateKey(pq)
}
func (kem *mlkemKEM) NewPrivateKey(priv []byte) (PrivateKey, error) {
pq, err := kem.newPrivateKey(priv)
if err != nil {
return nil, err
}
return NewMLKEMPrivateKey(pq)
}
func (kem *mlkemKEM) DeriveKeyPair(ikm []byte) (PrivateKey, error) {
suiteID := byteorder.BEAppendUint16([]byte("KEM"), kem.id)
dk, err := SHAKE256().labeledDerive(suiteID, ikm, "DeriveKeyPair", nil, 64)
if err != nil {
return nil, err
}
return kem.NewPrivateKey(dk)
}
func (k *mlkemPrivateKey) KEM() KEM {
return k.kem
}
func (k *mlkemPrivateKey) Bytes() ([]byte, error) {
pq, ok := k.pq.(interface {
Bytes() []byte
})
if !ok {
return nil, errors.New("private key seed not available")
}
return pq.Bytes(), nil
}
func (k *mlkemPrivateKey) PublicKey() PublicKey {
return &mlkemPublicKey{
kem: k.kem,
pq: k.pq.Encapsulator(),
}
}
func (k *mlkemPrivateKey) decap(enc []byte) ([]byte, error) {
return k.pq.Decapsulate(enc)
}
func wrapDecapsulator(dk any, err error) (crypto.Decapsulator, error) {
if err != nil {
return nil, err
}
switch key := dk.(type) {
case *mlkem.DecapsulationKey768:
return crypto.DecapsulatorFromDecapsulationKey768(key), nil
case *mlkem.DecapsulationKey1024:
return crypto.DecapsulatorFromDecapsulationKey1024(key), nil
default:
return nil, errors.New("hpke: internal error: unknown decapsulation key type")
}
}

1380
testdata/hpke-pq.json vendored Normal file

File diff suppressed because it is too large Load Diff

332
testdata/rfc9180.json vendored Normal file
View File

@@ -0,0 +1,332 @@
[
{
"mode": 0,
"kem_id": 32,
"kdf_id": 1,
"aead_id": 1,
"info": "4f6465206f6e2061204772656369616e2055726e",
"ikmE": "7268600d403fce431561aef583ee1613527cff655c1343f29812e66706df3234",
"ikmR": "6db9df30aa07dd42ee5e8181afdb977e538f5e1fec8a06223f33f7013e525037",
"skRm": "4612c550263fc8ad58375df3f557aac531d26850903e55a9f23f21d8534e8ac8",
"pkRm": "3948cfe0ad1ddb695d780e59077195da6c56506b027329794ab02bca80815c4d",
"enc": "37fda3567bdbd628e88668c3c8d7e97d1d1253b6d4ea6d44c150f741f1bf4431",
"encryptions_accumulated": "dcabb32ad8e8acea785275323395abd0",
"exports_accumulated": "45db490fc51c86ba46cca1217f66a75e"
},
{
"mode": 0,
"kem_id": 32,
"kdf_id": 1,
"aead_id": 2,
"info": "4f6465206f6e2061204772656369616e2055726e",
"ikmE": "2cd7c601cefb3d42a62b04b7a9041494c06c7843818e0ce28a8f704ae7ab20f9",
"ikmR": "dac33b0e9db1b59dbbea58d59a14e7b5896e9bdf98fad6891e99d1686492b9ee",
"skRm": "497b4502664cfea5d5af0b39934dac72242a74f8480451e1aee7d6a53320333d",
"pkRm": "430f4b9859665145a6b1ba274024487bd66f03a2dd577d7753c68d7d7d00c00c",
"enc": "6c93e09869df3402d7bf231bf540fadd35cd56be14f97178f0954db94b7fc256",
"encryptions_accumulated": "1702e73e1e71705faa8241022af1deea",
"exports_accumulated": "5cb678bf1c52afbd9afb58b8f7c1ced3"
},
{
"mode": 0,
"kem_id": 32,
"kdf_id": 1,
"aead_id": 3,
"info": "4f6465206f6e2061204772656369616e2055726e",
"ikmE": "909a9b35d3dc4713a5e72a4da274b55d3d3821a37e5d099e74a647db583a904b",
"ikmR": "1ac01f181fdf9f352797655161c58b75c656a6cc2716dcb66372da835542e1df",
"skRm": "8057991eef8f1f1af18f4a9491d16a1ce333f695d4db8e38da75975c4478e0fb",
"pkRm": "4310ee97d88cc1f088a5576c77ab0cf5c3ac797f3d95139c6c84b5429c59662a",
"enc": "1afa08d3dec047a643885163f1180476fa7ddb54c6a8029ea33f95796bf2ac4a",
"encryptions_accumulated": "225fb3d35da3bb25e4371bcee4273502",
"exports_accumulated": "54e2189c04100b583c84452f94eb9a4a"
},
{
"mode": 0,
"kem_id": 32,
"kdf_id": 1,
"aead_id": 65535,
"info": "4f6465206f6e2061204772656369616e2055726e",
"ikmE": "55bc245ee4efda25d38f2d54d5bb6665291b99f8108a8c4b686c2b14893ea5d9",
"ikmR": "683ae0da1d22181e74ed2e503ebf82840deb1d5e872cade20f4b458d99783e31",
"skRm": "33d196c830a12f9ac65d6e565a590d80f04ee9b19c83c87f2c170d972a812848",
"pkRm": "194141ca6c3c3beb4792cd97ba0ea1faff09d98435012345766ee33aae2d7664",
"enc": "e5e8f9bfff6c2f29791fc351d2c25ce1299aa5eaca78a757c0b4fb4bcd830918",
"exports_accumulated": "3fe376e3f9c349bc5eae67bbce867a16"
},
{
"mode": 0,
"kem_id": 32,
"kdf_id": 3,
"aead_id": 1,
"info": "4f6465206f6e2061204772656369616e2055726e",
"ikmE": "895221ae20f39cbf46871d6ea162d44b84dd7ba9cc7a3c80f16d6ea4242cd6d4",
"ikmR": "59a9b44375a297d452fc18e5bba1a64dec709f23109486fce2d3a5428ed2000a",
"skRm": "ddfbb71d7ea8ebd98fa9cc211aa7b535d258fe9ab4a08bc9896af270e35aad35",
"pkRm": "adf16c696b87995879b27d470d37212f38a58bfe7f84e6d50db638b8f2c22340",
"enc": "8998da4c3d6ade83c53e861a022c046db909f1c31107196ab4c2f4dd37e1a949",
"encryptions_accumulated": "19a0d0fb001f83e7606948507842f913",
"exports_accumulated": "e5d853af841b92602804e7a40c1f2487"
},
{
"mode": 0,
"kem_id": 32,
"kdf_id": 3,
"aead_id": 2,
"info": "4f6465206f6e2061204772656369616e2055726e",
"ikmE": "e72b39232ee9ef9f6537a72afe28f551dbe632006aa1b300a00518883a3f2dc1",
"ikmR": "a0484936abc95d587acf7034156229f9970e9dfa76773754e40fb30e53c9de16",
"skRm": "bdd8943c1e60191f3ea4e69fc4f322aa1086db9650f1f952fdce88395a4bd1af",
"pkRm": "aa7bddcf5ca0b2c0cf760b5dffc62740a8e761ec572032a809bebc87aaf7575e",
"enc": "c12ba9fb91d7ebb03057d8bea4398688dcc1d1d1ff3b97f09b96b9bf89bd1e4a",
"encryptions_accumulated": "20402e520fdbfee76b2b0af73d810deb",
"exports_accumulated": "80b7f603f0966ca059dd5e8a7cede735"
},
{
"mode": 0,
"kem_id": 32,
"kdf_id": 3,
"aead_id": 3,
"info": "4f6465206f6e2061204772656369616e2055726e",
"ikmE": "636d1237a5ae674c24caa0c32a980d3218d84f916ba31e16699892d27103a2a9",
"ikmR": "969bb169aa9c24a501ee9d962e96c310226d427fb6eb3fc579d9882dbc708315",
"skRm": "fad15f488c09c167bd18d8f48f282e30d944d624c5676742ad820119de44ea91",
"pkRm": "06aa193a5612d89a1935c33f1fda3109fcdf4b867da4c4507879f184340b0e0e",
"enc": "1d38fc578d4209ea0ef3ee5f1128ac4876a9549d74dc2d2f46e75942a6188244",
"encryptions_accumulated": "c03e64ef58b22065f04be776d77e160c",
"exports_accumulated": "fa84b4458d580b5069a1be60b4785eac"
},
{
"mode": 0,
"kem_id": 32,
"kdf_id": 3,
"aead_id": 65535,
"info": "4f6465206f6e2061204772656369616e2055726e",
"ikmE": "3cfbc97dece2c497126df8909efbdd3d56b3bbe97ddf6555c99a04ff4402474c",
"ikmR": "dff9a966e02b161472f167c0d4252d400069449e62384beb78111cb596220921",
"skRm": "7596739457c72bbd6758c7021cfcb4d2fcd677d1232896b8f00da223c5519c36",
"pkRm": "9a83674c1bc12909fd59635ba1445592b82a7c01d4dad3ffc8f3975e76c43732",
"enc": "444fbbf83d64fef654dfb2a17997d82ca37cd8aeb8094371da33afb95e0c5b0e",
"exports_accumulated": "7557bdf93eadf06e3682fce3d765277f"
},
{
"mode": 0,
"kem_id": 16,
"kdf_id": 1,
"aead_id": 1,
"info": "4f6465206f6e2061204772656369616e2055726e",
"ikmE": "4270e54ffd08d79d5928020af4686d8f6b7d35dbe470265f1f5aa22816ce860e",
"ikmR": "668b37171f1072f3cf12ea8a236a45df23fc13b82af3609ad1e354f6ef817550",
"skRm": "f3ce7fdae57e1a310d87f1ebbde6f328be0a99cdbcadf4d6589cf29de4b8ffd2",
"pkRm": "04fe8c19ce0905191ebc298a9245792531f26f0cece2460639e8bc39cb7f706a826a779b4cf969b8a0e539c7f62fb3d30ad6aa8f80e30f1d128aafd68a2ce72ea0",
"enc": "04a92719c6195d5085104f469a8b9814d5838ff72b60501e2c4466e5e67b325ac98536d7b61a1af4b78e5b7f951c0900be863c403ce65c9bfcb9382657222d18c4",
"encryptions_accumulated": "fcb852ae6a1e19e874fbd18a199df3e4",
"exports_accumulated": "655be1f8b189a6b103528ac6d28d3109"
},
{
"mode": 0,
"kem_id": 16,
"kdf_id": 1,
"aead_id": 2,
"info": "4f6465206f6e2061204772656369616e2055726e",
"ikmE": "a90d3417c3da9cb6c6ae19b4b5dd6cc9529a4cc24efb7ae0ace1f31887a8cd6c",
"ikmR": "a0ce15d49e28bd47a18a97e147582d814b08cbe00109fed5ec27d1b4e9f6f5e3",
"skRm": "317f915db7bc629c48fe765587897e01e282d3e8445f79f27f65d031a88082b2",
"pkRm": "04abc7e49a4c6b3566d77d0304addc6ed0e98512ffccf505e6a8e3eb25c685136f853148544876de76c0f2ef99cdc3a05ccf5ded7860c7c021238f9e2073d2356c",
"enc": "04c06b4f6bebc7bb495cb797ab753f911aff80aefb86fd8b6fcc35525f3ab5f03e0b21bd31a86c6048af3cb2d98e0d3bf01da5cc4c39ff5370d331a4f1f7d5a4e0",
"encryptions_accumulated": "8d3263541fc1695b6e88ff3a1208577c",
"exports_accumulated": "038af0baa5ce3c4c5f371c3823b15217"
},
{
"mode": 0,
"kem_id": 16,
"kdf_id": 1,
"aead_id": 3,
"info": "4f6465206f6e2061204772656369616e2055726e",
"ikmE": "f1f1a3bc95416871539ecb51c3a8f0cf608afb40fbbe305c0a72819d35c33f1f",
"ikmR": "61092f3f56994dd424405899154a9918353e3e008171517ad576b900ddb275e7",
"skRm": "a4d1c55836aa30f9b3fbb6ac98d338c877c2867dd3a77396d13f68d3ab150d3b",
"pkRm": "04a697bffde9405c992883c5c439d6cc358170b51af72812333b015621dc0f40bad9bb726f68a5c013806a790ec716ab8669f84f6b694596c2987cf35baba2a006",
"enc": "04c07836a0206e04e31d8ae99bfd549380b072a1b1b82e563c935c095827824fc1559eac6fb9e3c70cd3193968994e7fe9781aa103f5b50e934b5b2f387e381291",
"encryptions_accumulated": "702cdecae9ba5c571c8b00ad1f313dbf",
"exports_accumulated": "2e0951156f1e7718a81be3004d606800"
},
{
"mode": 0,
"kem_id": 16,
"kdf_id": 1,
"aead_id": 65535,
"info": "4f6465206f6e2061204772656369616e2055726e",
"ikmE": "3800bb050bb4882791fc6b2361d7adc2543e4e0abbac367cf00a0c4251844350",
"ikmR": "c6638d8079a235ea4054885355a7caefee67151c6ff2a04f4ba26d099c3a8b02",
"skRm": "62c3868357a464f8461d03aa0182c7cebcde841036aea7230ddc7339f1088346",
"pkRm": "046c6bb9e1976402c692fef72552f4aaeedd83a5e5079de3d7ae732da0f397b15921fb9c52c9866affc8e29c0271a35937023a9245982ec18bab1eb157cf16fc33",
"enc": "04d804370b7e24b94749eb1dc8df6d4d4a5d75f9effad01739ebcad5c54a40d57aaa8b4190fc124dbde2e4f1e1d1b012a3bc4038157dc29b55533a932306d8d38d",
"exports_accumulated": "a6d39296bc2704db6194b7d6180ede8a"
},
{
"mode": 0,
"kem_id": 16,
"kdf_id": 3,
"aead_id": 1,
"info": "4f6465206f6e2061204772656369616e2055726e",
"ikmE": "4ab11a9dd78c39668f7038f921ffc0993b368171d3ddde8031501ee1e08c4c9a",
"ikmR": "ea9ff7cc5b2705b188841c7ace169290ff312a9cb31467784ca92d7a2e6e1be8",
"skRm": "3ac8530ad1b01885960fab38cf3cdc4f7aef121eaa239f222623614b4079fb38",
"pkRm": "04085aa5b665dc3826f9650ccbcc471be268c8ada866422f739e2d531d4a8818a9466bc6b449357096232919ec4fe9070ccbac4aac30f4a1a53efcf7af90610edd",
"enc": "0493ed86735bdfb978cc055c98b45695ad7ce61ce748f4dd63c525a3b8d53a15565c6897888070070c1579db1f86aaa56deb8297e64db7e8924e72866f9a472580",
"encryptions_accumulated": "3d670fc7760ce5b208454bb678fbc1dd",
"exports_accumulated": "0a3e30b572dafc58b998cd51959924be"
},
{
"mode": 0,
"kem_id": 16,
"kdf_id": 3,
"aead_id": 2,
"info": "4f6465206f6e2061204772656369616e2055726e",
"ikmE": "0c4b7c8090d9995e298d6fd61c7a0a66bb765a12219af1aacfaac99b4deaf8ad",
"ikmR": "a2f6e7c4d9e108e03be268a64fe73e11a320963c85375a30bfc9ec4a214c6a55",
"skRm": "9648e8711e9b6cb12dc19abf9da350cf61c3669c017b1db17bb36913b54a051d",
"pkRm": "0400f209b1bf3b35b405d750ef577d0b2dc81784005d1c67ff4f6d2860d7640ca379e22ac7fa105d94bc195758f4dfc0b82252098a8350c1bfeda8275ce4dd4262",
"enc": "0404dc39344526dbfa728afba96986d575811b5af199c11f821a0e603a4d191b25544a402f25364964b2c129cb417b3c1dab4dfc0854f3084e843f731654392726",
"encryptions_accumulated": "9da1683aade69d882aa094aa57201481",
"exports_accumulated": "80ab8f941a71d59f566e5032c6e2c675"
},
{
"mode": 0,
"kem_id": 16,
"kdf_id": 3,
"aead_id": 3,
"info": "4f6465206f6e2061204772656369616e2055726e",
"ikmE": "02bd2bdbb430c0300cea89b37ada706206a9a74e488162671d1ff68b24deeb5f",
"ikmR": "8d283ea65b27585a331687855ab0836a01191d92ab689374f3f8d655e702d82f",
"skRm": "ebedc3ca088ad03dfbbfcd43f438c4bb5486376b8ccaea0dc25fc64b2f7fc0da",
"pkRm": "048fed808e948d46d95f778bd45236ce0c464567a1dc6f148ba71dc5aeff2ad52a43c71851b99a2cdbf1dad68d00baad45007e0af443ff80ad1b55322c658b7372",
"enc": "044415d6537c2e9dd4c8b73f2868b5b9e7e8e3d836990dc2fd5b466d1324c88f2df8436bac7aa2e6ebbfd13bd09eaaa7c57c7495643bacba2121dca2f2040e1c5f",
"encryptions_accumulated": "f025dca38d668cee68e7c434e1b98f9f",
"exports_accumulated": "2efbb7ade3f87133810f507fdd73f874"
},
{
"mode": 0,
"kem_id": 16,
"kdf_id": 3,
"aead_id": 65535,
"info": "4f6465206f6e2061204772656369616e2055726e",
"ikmE": "497efeca99592461588394f7e9496129ed89e62b58204e076d1b7141e999abda",
"ikmR": "49b7cbfc1756e8ae010dc80330108f5be91268b3636f3e547dbc714d6bcd3d16",
"skRm": "9d34abe85f6da91b286fbbcfbd12c64402de3d7f63819e6c613037746b4eae6b",
"pkRm": "0453a4d1a4333b291e32d50a77ac9157bbc946059941cf9ed5784c15adbc7ad8fe6bf34a504ed81fd9bc1b6bb066a037da30fccd6c0b42d72bf37b9fef43c8e498",
"enc": "04f910248e120076be2a4c93428ac0c8a6b89621cfef19f0f9e113d835cf39d5feabbf6d26444ebbb49c991ec22338ade3a5edff35a929be67c4e5f33dcff96706",
"exports_accumulated": "6df17307eeb20a9180cff75ea183dd60"
},
{
"mode": 0,
"kem_id": 18,
"kdf_id": 1,
"aead_id": 1,
"info": "4f6465206f6e2061204772656369616e2055726e",
"ikmE": "5040af7a10269b11f78bb884812ad20041866db8bbd749a6a69e3f33e54da7164598f005bce09a9fe190e29c2f42df9e9e3aad040fccc625ddbd7aa99063fc594f40",
"ikmR": "39a28dc317c3e48b908948f99d608059f882d3d09c0541824bc25f94e6dee7aa0df1c644296b06fbb76e84aef5008f8a908e08fbabadf70658538d74753a85f8856a",
"skRm": "009227b4b91cf1eb6eecb6c0c0bae93a272d24e11c63bd4c34a581c49f9c3ca01c16bbd32a0a1fac22784f2ae985c85f183baad103b2d02aee787179dfc1a94fea11",
"pkRm": "0400b81073b1612cf7fdb6db07b35cf4bc17bda5854f3d270ecd9ea99f6c07b46795b8014b66c523ceed6f4829c18bc3886c891b63fa902500ce3ddeb1fbec7e608ac70050b76a0a7fc081dbf1cb30b005981113e635eb501a973aba662d7f16fcc12897dd752d657d37774bb16197c0d9724eecc1ed65349fb6ac1f280749e7669766f8cd",
"enc": "0400bec215e31718cd2eff5ba61d55d062d723527ec2029d7679a9c867d5c68219c9b217a9d7f78562dc0af3242fef35d1d6f4a28ee75f0d4b31bc918937b559b70762004c4fd6ad7373db7e31da8735fbd6171bbdcfa770211420682c760a40a482cc24f4125edbea9cb31fe71d5d796cfe788dc408857697a52fef711fb921fa7c385218",
"encryptions_accumulated": "94209973d36203eef2e56d155ef241d5",
"exports_accumulated": "31f25ea5e192561bce5f2c2822a9432c"
},
{
"mode": 0,
"kem_id": 18,
"kdf_id": 1,
"aead_id": 2,
"info": "4f6465206f6e2061204772656369616e2055726e",
"ikmE": "9953fbd633be69d984fc4fffc4d7749f007dbf97102d36a647a8108b0bb7c609e826b026aec1cd47b93fc5acb7518fa455ed38d0c29e900c56990635612fd3d220d2",
"ikmR": "17320bc93d9bc1d422ba0c705bf693e9a51a855d6e09c11bddea5687adc1a1122ec81384dc7e47959cae01c420a69e8e39337d9ebf9a9b2f3905cb76a35b0693ac34",
"skRm": "01a27e65890d64a121cfe59b41484b63fd1213c989c00e05a049ac4ede1f5caeec52bf43a59bdc36731cb6f8a0b7d7724b047ff52803c421ee99d61d4ea2e569c825",
"pkRm": "0400eb4010ca82412c044b52bdc218625c4ea797e061236206843e318882b3c1642e7e14e7cc1b4b171a433075ac0c8563043829eee51059a8b68197c8a7f6922465650075f40b6f440fdf525e2512b0c2023709294d912d8c68f94140390bff228097ce2d5f89b2b21f50d4c0892cfb955c380293962d5fe72060913870b61adc8b111953",
"enc": "0401c1cf49cafa9e26e24a9e20d7fa44a50a4e88d27236ef17358e79f3615a97f825899a985b3edb5195cad24a4fb64828701e81fbfd9a7ef673efde508e789509bd7c00fd5bfe053377bbee22e40ae5d64aa6fb47b314b5ab7d71b652db9259962dce742317d54084f0cf62a4b7e3f3caa9e6afb8efd6bf1eb8a2e13a7e73ec9213070d68",
"encryptions_accumulated": "69d16fa7c814cd8be9aa2122fda8768f",
"exports_accumulated": "d295fad3aef8be1f89d785800f83a30b"
},
{
"mode": 0,
"kem_id": 18,
"kdf_id": 1,
"aead_id": 3,
"info": "4f6465206f6e2061204772656369616e2055726e",
"ikmE": "566568b6cbfd1c6c06d1b0a2dc22d4e4965858bf3d54bf6cba5c018be0fad7a5cd9237937800f3cb57f10fa5691faeecab1685aa6da9b667469224a0989ff82b822b",
"ikmR": "f9f594556282cfe3eb30958ca2ef90ecd2a6ffd2661d41eb39ba184f3dae9f914aad297dd80cc763cb6525437a61ceae448aeeb304de137dc0f28dd007f0d592e137",
"skRm": "0168c8bf969b30bd949e154bf2db1964535e3f230f6604545bc9a33e9cd80fb17f4002170a9c91d55d7dd21db48e687cea83083498768cc008c6adf1e0ca08a309bd",
"pkRm": "040086b1a785a52af34a9a830332999896e99c5df0007a2ec3243ee3676ba040e60fde21bacf8e5f8db26b5acd42a2c81160286d54a2f124ca8816ac697993727431e50002aa5f5ebe70d88ff56445ade400fb979b466c9046123bbf5be72db9d90d1cde0bb7c217cff8ea0484445150eaf60170b039f54a5f6baeb7288bc62b1dedb59a1b",
"enc": "0401f828650ec526a647386324a31dadf75b54550b06707ae3e1fb83874b2633c935bb862bc4f07791ccfafbb08a1f00e18c531a34fec76f2cf3d581e7915fa40bbc3b010ab7c3d9162ea69928e71640ecff08b97f4fa9e8c66dfe563a13bf561cee7635563f91d387e2a38ee674ea28b24c633a988d1a08968b455e96307c64bda3f094b7",
"encryptions_accumulated": "586d5a92612828afbd7fdcea96006892",
"exports_accumulated": "a70389af65de4452a3f3147b66bd5c73"
},
{
"mode": 0,
"kem_id": 18,
"kdf_id": 1,
"aead_id": 65535,
"info": "4f6465206f6e2061204772656369616e2055726e",
"ikmE": "5dfb76f8b4708970acb4a6efa35ec4f2cebd61a3276a711c2fa42ef0bc9c191ea9dac7c0ac907336d830cea4a8394ab69e9171f344c4817309f93170cb34914987a5",
"ikmR": "9fd2aad24a653787f53df4a0d514c6d19610ca803298d7812bc0460b76c21da99315ebfec2343b4848d34ce526f0d39ce5a8dfddd9544e1c4d4b9a62f4191d096b42",
"skRm": "01ca47cf2f6f36fef46a01a46b393c30672224dd566aa3dd07a229519c49632c83d800e66149c3a7a07b840060549accd0d480ec5c71d2a975f88f6aa2fc0810b393",
"pkRm": "040143b7db23907d3ae1c43ef4882a6cdb142ca05a21c2475985c199807dd143e898136c65faf1ca1b6c6c2e8a92d67a0ab9c24f8c5cff7610cb942a73eb2ec4217c26018d67621cc78a60ec4bd1e23f90eb772adba2cf5a566020ee651f017b280a155c016679bd7e7ebad49e28e7ab679f66765f4ef34eae6b38a99f31bc73ea0f0d694d",
"enc": "040073dda7343ce32926c028c3be28508cccb751e2d4c6187bcc4e9b1de82d3d70c5702c6c866a920d9d9a574f5a4d4a0102db76207d5b3b77da16bb57486c5cc2a95f006b5d2e15efb24e297bdf8f2b6d7b25bf226d1b6efca47627b484d2942c14df6fe018d82ab9fb7306370c248864ea48fe5ca94934993517aacaa3b6bca8f92efc84",
"exports_accumulated": "d8fa94ac5e6829caf5ab4cdd1e05f5e1"
},
{
"mode": 0,
"kem_id": 18,
"kdf_id": 3,
"aead_id": 1,
"info": "4f6465206f6e2061204772656369616e2055726e",
"ikmE": "018b6bb1b8bbcefbd91e66db4e1300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"ikmR": "7bf9fd92611f2ff4e6c2ab4dd636a320e0397d6a93d014277b025a7533684c3255a02aa1f2a142be5391eebfc60a6a9c729b79c2428b8d78fa36497b1e89e446d402",
"skRm": "019db24a3e8b1f383436cd06997dd864eb091418ff561e3876cee2e4762a0cc0b69688af9a7a4963c90d394b2be579144af97d4933c0e6c2c2d13e7505ea51a06b0d",
"pkRm": "0401e06b350786c48a60dfc50eed324b58ecafc4efba26242c46c14274bd97f0989487a6fae0626188fea971ae1cb53f5d0e87188c1c62af92254f17138bbcebf5acd0018e574ee1d695813ce9dc45b404d2cf9c04f27627c4c55da1f936d813fd39435d0713d4a3cdc5409954a1180eb2672bdfc4e0e79c04eda89f857f625e058742a1c8",
"enc": "0400ac8d1611948105f23cf5e6842b07bd39b352d9d1e7bff2c93ac063731d6372e2661eff2afce604d4a679b49195f15e4fa228432aed971f2d46c1beb51fb3e5812501fe199c3d94c1b199393642500443dd82ce1c01701a1279cc3d74e29773030e26a70d3512f761e1eb0d7882209599eb9acd295f5939311c55e737f11c19988878d6",
"encryptions_accumulated": "207972885962115e69daaa3bc5015151",
"exports_accumulated": "8e9c577501320d86ee84407840188f5f"
},
{
"mode": 0,
"kem_id": 18,
"kdf_id": 3,
"aead_id": 2,
"info": "4f6465206f6e2061204772656369616e2055726e",
"ikmE": "7f06ab8215105fc46aceeb2e3dc5028b44364f960426eb0d8e4026c2f8b5d7e7a986688f1591abf5ab753c357a5d6f0440414b4ed4ede71317772ac98d9239f70904",
"ikmR": "2ad954bbe39b7122529f7dde780bff626cd97f850d0784a432784e69d86eccaade43b6c10a8ffdb94bf943c6da479db137914ec835a7e715e36e45e29b587bab3bf1",
"skRm": "01462680369ae375e4b3791070a7458ed527842f6a98a79ff5e0d4cbde83c27196a3916956655523a6a2556a7af62c5cadabe2ef9da3760bb21e005202f7b2462847",
"pkRm": "0401b45498c1714e2dce167d3caf162e45e0642afc7ed435df7902ccae0e84ba0f7d373f646b7738bbbdca11ed91bdeae3cdcba3301f2457be452f271fa6837580e661012af49583a62e48d44bed350c7118c0d8dc861c238c72a2bda17f64704f464b57338e7f40b60959480c0e58e6559b190d81663ed816e523b6b6a418f66d2451ec64",
"enc": "040138b385ca16bb0d5fa0c0665fbbd7e69e3ee29f63991d3e9b5fa740aab8900aaeed46ed73a49055758425a0ce36507c54b29cc5b85a5cee6bae0cf1c21f2731ece2013dc3fb7c8d21654bb161b463962ca19e8c654ff24c94dd2898de12051f1ed0692237fb02b2f8d1dc1c73e9b366b529eb436e98a996ee522aef863dd5739d2f29b0",
"encryptions_accumulated": "31769e36bcca13288177eb1c92f616ae",
"exports_accumulated": "fbffd93db9f000f51cf8ab4c1127fbda"
},
{
"mode": 0,
"kem_id": 18,
"kdf_id": 3,
"aead_id": 3,
"info": "4f6465206f6e2061204772656369616e2055726e",
"ikmE": "f9d540fde009bb1e5e71617c122a079862306b97144c8c4dca45ef6605c2ec9c43527c150800f5608a7e4cff771226579e7c776fb3def4e22e68e9fdc92340e94b6e",
"ikmR": "5273f7762dea7a2408333dbf8db9f6ef2ac4c475ad9e81a3b0b8c8805304adf5c876105d8703b42117ad8ee350df881e3d52926aafcb5c90f649faf94be81952c78a",
"skRm": "015b59f17366a1d4442e5b92d883a8f35fe8d88fea0e5bac6dfac7153c78fd0c6248c618b083899a7d62ba6e00e8a22cdde628dd5399b9a3377bb898792ff6f54ab9",
"pkRm": "040084698a47358f06a92926ee826a6784341285ee45f4b8269de271a8c6f03d5e8e24f628de13f5c37377b7cabfbd67bc98f9e8e758dfbee128b2fe752cd32f0f3ccd0061baec1ed7c6b52b7558bc120f783e5999c8952242d9a20baf421ccfc2a2b87c42d7b5b806fea6d518d5e9cd7bfd6c85beb5adeb72da41ac3d4f27bba83cff24d7",
"enc": "0400edc201c9b32988897a7f7b19104ebb54fc749faa41a67e9931e87ec30677194898074afb9a5f40a97df2972368a0c594e5b60e90d1ff83e9e35f8ff3ad200fd6d70028b5645debe9f1f335dbc1225c066218e85cf82a05fbe361fa477740b906cb3083076e4d17232513d102627597d38e354762cf05b3bd0f33dc4d0fb78531afd3fd",
"encryptions_accumulated": "aa69356025f552372770ef126fa2e59a",
"exports_accumulated": "1fcffb5d8bc1d825daf904a0c6f4a4d3"
},
{
"mode": 0,
"kem_id": 18,
"kdf_id": 3,
"aead_id": 65535,
"info": "4f6465206f6e2061204772656369616e2055726e",
"ikmE": "3018d74c67d0c61b5e4075190621fc192996e928b8859f45b3ad2399af8599df69c34b7a3eefeda7ee49ae73d4579300b85dde1654c0dfc3a3f78143d239a628cf72",
"ikmR": "a243eff510b99140034c72587e9f131809b9bce03a9da3da458771297f535cede0f48167200bf49ac123b52adfd789cf0adfd5cded6be2f146aeb00c34d4e6d234fc",
"skRm": "0045fe00b1d55eb64182d334e301e9ac553d6dbafbf69935e65f5bf89c761b9188c0e4d50a0167de6b98af7bebd05b2627f45f5fca84690cd86a61ba5a612870cf53",
"pkRm": "0401635b3074ad37b752696d5ca311da9cc790a899116030e4c71b83edd06ced92fdd238f6c921132852f20e6a2cbcf2659739232f4a69390f2b14d80667bcf9b71983000a919d29366554f53107a6c4cc7f8b24fa2de97b42433610cbd236d5a2c668e991ff4c4383e9fe0a9e7858fc39064e31fca1964e809a2f898c32fba46ce33575b8",
"enc": "0400932d9ff83ca4b799968bda0dd9dac4d02c9232cdcf133db7c53cfbf3d80a299fd99bc42da38bb78f57976bdb69988819b6e2924fadacdad8c05052997cf50b29110139f000af5b2c599b05fc63537d60a8384ca984821f8cd12621577a974ebadaf98bfdad6d1643dd4316062d7c0bda5ba0f0a2719992e993af615568abf19a256993",
"exports_accumulated": "29c0f6150908f6e0d979172f23f1d57b"
}
]