Skip to content

CMS RFC 8418 key agreement: the ukm is not used as the HKDF salt #2454

Description

@pdb0102

Summary

For the RFC 8418 key agreement schemes (dhSinglePass-stdDH-hkdf-sha256/384/512-scheme, X25519
and X448), the CMS layer derives the key-encryption key with HKDF using the ukm only inside the
ECC-CMS-SharedInfo (the HKDF info), never as the HKDF salt. RFC 8418 section 2.2 requires both:

The ECC-CMS-SharedInfo structure optionally includes the ukm. If the ukm is present, the ukm
is also used as the HKDF salt. HKDF uses an appropriate number of zero octets when no salt is
provided.

In summary, to produce the pairwise key-encryption key, KEK:

if ukm is provided, then salt = ukm, else salt is not provided
PRK = HKDF-Extract(salt, K)
KEK = HKDF-Expand(PRK, DER(ECC-CMS-SharedInfo), SizeInOctets(KEK))

Messages without a ukm interoperate with an implementation that follows the text; messages with a
ukm do not, in either direction.

Where

  • pkix/src/main/java/org/bouncycastle/cms/CMSUtils.java, lines 99 to 101: the three RFC 8418
    scheme OIDs are added to the EC algorithm set, so they take the EC branch below.
  • pkix/src/main/java/org/bouncycastle/cms/jcajce/JceKeyAgreeRecipientInfoGenerator.java, lines
    180 to 183 (the sender): agreementParamSpec = new UserKeyingMaterialSpec(ukmKeyingMaterial),
    the one-argument constructor, where ukmKeyingMaterial is the DER ECC-CMS-SharedInfo built
    with the ukm as entityUInfo.
  • pkix/src/main/java/org/bouncycastle/cms/jcajce/JceKeyAgreeRecipient.java, lines 235 to 246
    (the recipient): the same one-argument construction.
  • The agreement layer would honour a salt: UserKeyingMaterialSpec has a
    (byte[] userKeyingMaterial, byte[] salt) constructor, and
    prov/src/main/java/org/bouncycastle/jcajce/provider/asymmetric/util/BaseAgreementSpi.java,
    line 429, passes ukmParametersSalt into HKDFParameters. It is never set from the CMS code.

How to reproduce

  1. Generate an X25519 recipient key pair and a certificate for it. Build an EnvelopedData with
    JceKeyAgreeRecipientInfoGenerator(new ASN1ObjectIdentifier("1.2.840.113549.1.9.16.3.19"), originatorPrivate, originatorPublic, CMSAlgorithm.AES128_WRAP), addRecipient(certificate),
    setUserKeyingMaterial(ukm) with any ukm, and JceCMSContentEncryptorBuilder(CMSAlgorithm.AES256_CBC).
    (The attached bc-x25519-hkdf-sha256-ukm.der is such a message from 1.86, ukm
    cafed1ea0529; bc-x25519-recipient-key.der is the recipient's PKCS#8 key. I attached the three files in zip file
    bc-rfc8418-ukm-fixtures.zip)
  2. Independently: derive the X25519 shared secret K from the recipient's private key and the
    originator key in the KeyAgreeRecipientInfo; build DER(ECC-CMS-SharedInfo) with keyInfo = the
    KeyWrapAlgorithm identifier as encoded in the message, entityUInfo = the ukm, suppPubInfo =
    128 as a 4-octet big-endian length; compute KEK = HKDF-SHA256(K, salt, info =
    DER(ECC-CMS-SharedInfo), 16).
  3. With salt = ukm, as the RFC says, AES-KW unwrap of the message's encryptedKey fails the
    integrity check. With no salt it succeeds. The same holds for X448 with hkdf-sha512 and for
    X25519 with hkdf-sha384.

Expected

With a ukm present, HKDF-Extract runs with salt = ukm, on both the generating and the receiving
side, as RFC 8418 section 2.2 specifies.

Suggested fix

In the two CMS sites, when the key agreement OID is one of the three RFC 8418 schemes and a ukm
is present, construct new UserKeyingMaterialSpec(ukmKeyingMaterial, userKeyingMaterial) (the
SharedInfo as before, the raw ukm as the salt); the agreement layer already passes the salt
through. Messages without a ukm are unchanged.

Version

bcprov, bcpkix, bcutil jdk18on 1.86, Temurin 21.0.12; the same code is on main today (9/17/2026).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions