Subject: Re: [COSE] Update to the COSE-HPKE draft and new use case (?)

From: Ilari Liusvaara <[email-masked]>
Date: Thu, 3 Mar 2022 20:12:21 +0200

On Mon, Feb 28, 2022 at 03:04:30PM +0000, Hannes Tschofenig wrote:
> Hi all,
> 
> Following the virtual interim meeting we have updated the draft and
> here is a recent snapshot:
> https://github.com/cose-wg/HPKE/blob/main/draft-ietf-cose-hpke.txt
> 
> Here are the slides from that meeting:
> https://datatracker.ietf.org/meeting/interim-2022-cose-01/materials/slides-interim-2022-cose-01-sessa-cose-hpke-00
> 
> At that meeting we made progress on two open issues:
> 
> 
>   1.  Simplify the layering (to a 2-layer model), as proposed by
>       Ilari.
>   2.  Not to introduce new parameters for HPKE algorithms but instead
>       have the HPKE authors to add a note to their draft so that new 
>       value registrations are automatically populated into the IANA
>       COSE registry.
> 
> There are additional open issues described in the slide deck that
> require further discussion in the group. I will add those to the
> Github open issue list.

As note, I was not thinking about alignment to AES-KW when I came up
with that two-layer structure, but alignment to HPKE itself.

It is maybe helpful to consider inputs and outputs of base mode HPKE
encryption/decryption and types thereof (in successful case):

HPKE base mode encryption wants...

- AEAD ID [uint16]
- KDF ID [uint16]
- public key [(uint16,byte_string)]
- associated data [byte_string]
- plaintext [byte_string]

... And produces:

- encapsulated key [byte_string]
- ciphertext [byte_string]

HPKE base mode decryption wants...

- AEAD ID [uint16]
- KDF ID [uint16]
- private key [(uint16,byte_string)]
- associated data [byte_string]
- encapsulated key [byte_string]
- ciphertext [byte_string]

... And produces:

- plaintext [byte_string]

Note that everything is either uint16 (16-bit integer), a byte string or
conbination thereof. This holds for any current or future algorithms,
including any future PQC algorithms (this is hard constraint of HPKE).

Now, there are some non-optimal things here:

1) While using multiple AEAD IDs with the same key is cryptographically
kosher (there is internal key separation), using multiple KDF IDs might
not be a kosher thing to do (as keys are not separated).

2) If using NIST curves, the byte_string part of public key and
encapsulated key is uncompressed curve point, which is about 2x larger
than it it needs to be.

3) In practice KDF ID and public key algorithm are coupled, due to
pk algorithm internally using some KDF, which may or may not have KDF
ID. All the present ones have KDF IDs, but future ones might not.

Here is one mapping of all this into COSE layer, optimized for message
size:

- Allocate algorithm for HPKE.
- Allocate new header field for HPKE AEAD ID.
  + Registry not our problem (let CFRG deal with it).
- Allocate new key type for HPKE.
- Map HPKE KDF and KEM IDs to crv by kdf * 65536 + kem.
  + Registry not our problem (let CFRG deal with it).
- Use negative crv to denote special encodings.
  + hkdf-sha256-nist-p256 (33 byte pubkey).
  + hkdf-sha384-nist-p384 (49 byte pubkey).
  + hkdf-sha512-nist-p521 (66 byte pubkey).
  + hkdf-sha256-x25519 (32 byte pubkey).
  + hkdf-sha512-x448 (56 byte pubkey).
- If encapsulated key is raw octet string, omit crv.
  + COSE does not let one put bstr into ephemeral field.
- Encode encapsulated key as the ephemeral field.
- Encode the ciphertext as the ciphertext field.

a) NIST P-256, SHA-256 and AES-128-GCM:

protected: {
	alg: hpke,
	hpke-aead-alg: 1,
},
unprotected: {
	ephemeral: {
		kty: hpke,
		crv: hkdf-sha256-nist-p256,	//Negative
		x: h'<...>'			//33 bytes.
	},
	kid: '<...>',
},
ciphertext: h'<...>'				//16 byte overhead.

Public Key:

{
	kty: hpke,
	crv: hkdf-sha256-nist-p256,	//Negative
	x: h'<...>'			//33 bytes.
}

b) X448, SHA-512 and Chacha20-Poly1305:

protected: {
	alg: hpke,
	hpke-aead-alg: 3,
},
unprotected: {
	ephemeral: {
		kty: hpke,
		//no crv, this is raw byte string.
		x: h'<...>'			//56 bytes.
	},
	kid: '<...>',
},
ciphertext: h'<...>'				//16 byte overhead.

Public Key:

{
	kty: hpke,
	crv: hkdf-sha512-x448,		//Negative
	x: h'<...>'			//33 bytes.
}

c) KEM-0x41 (future post-quantum?), SHA-384 and AES-256-GCM:

protected: {
	alg: hpke,
	hpke-aead-alg: 2,
},
unprotected: {
	ephemeral: {
		kty: hpke,
		//no crv, this is raw byte string.
		x: h'<...>'			//~1.3kB.
	},
	kid: '<...>',
},
ciphertext: h'<...>'				//16 byte overhead.

Public Key:

{
	kty: hpke,
	crv: 0x02041,				//KDF 2, KEM 0x41.
	x: h'<...>'				//~1.3kB.
}

Alternatives considered:

- Instead adding a new header parameter, reseve a block of 65536 algs
  for HPKE from 5-byte range. E.g. 0x48450000-0x4845FFFF. Loses 2
  bytes (assuming 1 byte new allocations).
- Instead of adding new kty for generic case, reserve a block of 2^32
  crvs for HPKE from 9-byte range and use OKP. Loses 4 bytes.
- Instead of adding new kty for special case, reuse compatible EC2/
  OKP crv. Can lead to keys getting mixed up for HPKE/not, which is
  not good (unless there is single HPKE alg, which can be locked via
  the key alg field). Loses 1 byte for P256/P384, 2 bytes for P521.

> A new aspect raised by Richard and Chris (on CC), in an off-list
> review, is to add an even more "optimized" layering. As a reminder,
> they currently described layering (see link to the draft snapshot)
> encrypts a CEK via HPKE and places the encrypted CEK in layer 1.
> Then, the plaintext in layer 0 is encrypted using that CEK. This
> design was selected for use with firmware encryption in mind
> (plaintext is the firmware image in this case). Richard believes
> that there are use cases where this intermediate step is not needed.
> Consequently, there would be only one layer and HPKE is thereby
> directly applied to the plaintext.

This sounds like the "HPKE in cose_encrypt0" idea I had. If one has
cose_encrypt0 structure with HPKE alg in it, and one interprets it
in the most straightforward way, one gets an effect like this.

-Ilari