Skip to content

Add AuthEnvelopedData (RFC 5083) so AES-GCM CMS is interoperable #531

Description

@XhstormR

Summary

PKI.js 3.4.0 has no class for the CMS authenticated-enveloped-data content type
(id-ct-authEnvelopedData, 1.2.840.113549.1.9.16.1.23, RFC 5083). As a result:

So AES-GCM in PKI.js is currently a closed loop: it only round-trips against itself. This is not a
theoretical concern — OpenSSL's cms code has an explicit guard against AEAD ciphers in
EnvelopedData (see the experiment in §3).

I would like AuthEnvelopedData to be a first-class class alongside EnvelopedData.

1. Environment

pkijs 3.4.0 (https://cdn.jsdelivr.net/npm/pkijs@3.4.0/+esm), setEngine("browser")
asn1js 3.0.7
Runtime Chromium (Web Crypto), secure context
Peer OpenSSL 4.0.1 (9 Jun 2026)

Confirmation that no such class exists in 3.4.0:

Object.keys(pkijs).filter(k => /auth/i.test(k))
// ["AuthenticatedSafe", "AuthorityKeyIdentifier", "id_AuthorityInfoAccess", "id_AuthorityKeyIdentifier"]

Object.keys(pkijs).filter(k => /gcm/i.test(k))
// []

// ContentInfo has no constant for the authEnvelopedData OID:
// DATA=1.2.840.113549.1.7.1, SIGNED_DATA=…1.7.2, ENVELOPED_DATA=…1.7.3, ENCRYPTED_DATA=…1.7.6

2. Current behavior

2.1 Direction A — reading OpenSSL AES-GCM output fails

Producer (a KEK recipient is used only to keep the fixture free of certificates and private keys;
-recip cert.pem yields the same id-smime-ct-authEnvelopedData container — see @gnarea's
-aes-128-gcm -recip sample in #287. -pwri_password cannot be combined with GCM at all:
OpenSSL fails with unsupported kek algorithm:crypto/cms/cms_pwri.c:75, since the PWRI key-wrap
cipher follows the -aes-* option and must not be AEAD):

printf 'Hello, AuthEnvelopedData!\n' > repro.txt
KEK=000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
openssl cms -encrypt -aes-256-gcm -binary \
  -secretkey "$KEK" -secretkeyid 6b656b31 \
  -in repro.txt -outform DER -out repro-gcm.cms

OpenSSL emits id-smime-ct-authEnvelopedData with the ICV in the separate mac field:

CMS_ContentInfo:
  contentType: id-smime-ct-authEnvelopedData (1.2.840.113549.1.9.16.1.23)
  d.authEnvelopedData:
    version: 0
    recipientInfos:
      d.kekri: … keyEncryptionAlgorithm: id-aes256-wrap …
    authEncryptedContentInfo:
      contentType: pkcs7-data (1.2.840.113549.1.7.1)
      contentEncryptionAlgorithm:
        algorithm: aes-256-gcm (2.16.840.1.101.3.4.1.46)
        parameter: SEQUENCE:
    0:d=0  hl=2 l=  17 cons: SEQUENCE
    2:d=1  hl=2 l=  12 prim:  OCTET STRING      [HEX DUMP]:8AA510847B756EDFF74E33AF
   16:d=1  hl=2 l=   1 prim:  INTEGER           :10
      encryptedContent:                       <-- 26 bytes = plaintext length, no tag appended
        0000 - 98 cc 1f 28 ca 6f ff 10-2f 7f 6a 25 d4 f0 7b   ...(.o../.j%..{
        000f - 84 40 dc d7 07 13 4d bb-4c 18 86               .@....M.L..
    authAttrs: <ABSENT>
    mac:
      0000 - cf 57 a1 d2 f5 5f 18 61-58 e1 2d 04 1c 7b b0 1a
    unauthAttrs: <ABSENT>

PKI.js side:

const ci = pkijs.ContentInfo.fromBER(der);
// ci.contentType === "1.2.840.113549.1.9.16.1.23"   // parses, but nothing can consume it

new pkijs.EnvelopedData({ schema: ci.content });
// Error: Object's schema was not verified against input data for EnvelopedData

The schema failure is correct and expected — AuthEnvelopedData is a different SEQUENCE
(authAttrs [1] and mac are absent from EnvelopedData). There is simply no class to use here.

2.2 Direction B — PKI.js AES-GCM output is rejected

const ed = new pkijs.EnvelopedData();
ed.addRecipientByPreDefinedData(kek256, {
  keyIdentifier: kekId,
  keyEncryptionAlgorithm: { name: "AES-KW", length: 256 },
}, 1);
await ed.encrypt({ name: "AES-GCM", length: 256 }, plaintext);   // 26-byte plaintext
new pkijs.ContentInfo({ contentType: pkijs.ContentInfo.ENVELOPED_DATA, content: ed.toSchema() });

Measured properties of the result:

Property PKI.js 3.4.0 RFC 5083 / 5084
ContentInfo.contentType 1.2.840.113549.1.7.3 (id-envelopedData) 1.2.840.113549.1.9.16.1.23
contentEncryptionAlgorithm.parameters bare OCTET STRING (16 bytes) GCMParameters ::= SEQUENCE { aes-nonce OCTET STRING, aes-ICVlen … }
Nonce length 16 (src/EnvelopedData.ts:800, // For AES we need IV 16 bytes long) 12 recommended
ICV location trailing 16 bytes of encryptedContent (42 = 26 + 16) separate mac field
AAD none DER of authAttrs re-tagged SET OF, or zero bits if absent
version 2 MUST be 0

OpenSSL rejects it:

$ openssl cms -decrypt -inform DER -in pkijs-gcm.cms -secretkey "$KEK" -secretkeyid 6b656b31
Error decrypting CMS structure
…:asn1 encoding routines:ossl_asn1_type_get_octetstring_int:data is wrong:crypto/asn1/evp_asn1.c:203:
…:digital envelope routines:evp_cipher_asn1_to_param_ex:cipher parameter error:crypto/evp/evp_lib.c:201:
…:CMS routines:ossl_cms_EncryptedContent_init_bio:cipher parameter initialisation error:crypto/cms/cms_enc.c:103:

The relevant code paths are src/EnvelopedData.ts:798-836 (encrypt) and :1540-1566 (decrypt).
The decrypt path reads the IV as contentEncryptionAlgorithm.algorithmParams.valueBlock.valueHex
(line 1548) and calls crypto.decrypt({ name, iv }, key, wholeBuffer) with no additionalData and
no tagLength, which only works because Web Crypto's AES-GCM accepts ciphertext || tag
concatenated — i.e. the current behavior is a side effect of the Web Crypto calling convention, not
a CMS encoding decision.

3. Why fixing the GCMParameters encoding alone is not sufficient

#287 and PR #487 address the OCTET STRING vs SEQUENCE encoding of the AES-GCM
AlgorithmIdentifier parameters. That fix is necessary but provably insufficient for interop.

Experiment: I patched the PKI.js output byte-for-byte, replacing the bare OCTET STRING with a
conformant GCMParameters SEQUENCE ({ OCTET STRING(12), INTEGER 16 }) and left everything else
alone. OpenSSL now parses the parameters correctly (parameter: SEQUENCE: in -cmsout -print) and
then refuses on the container instead:

Error decrypting CMS structure
…:CMS routines:ossl_cms_EncryptedContent_init_bio:cipher aead in enveloped data:crypto/cms/cms_enc.c:108:

cipher aead in enveloped data is an explicit guard: an AEAD cipher inside EnvelopedData is
refused regardless of how its parameters are encoded. Interop requires the AuthEnvelopedData
container, which is exactly what is missing. (Note RFC 5084 itself does not spell out a prohibition
— it simply profiles AES-GCM/AES-CCM for the authenticated-enveloped-data content type — but
implementations enforce the separation, as above.)

4. Requested feature

  1. AuthEnvelopedData class (RFC 5083 §2), mirroring the existing EnvelopedData API surface
    so the recipient-management code can be shared:

    AuthEnvelopedData ::= SEQUENCE {
      version                 CMSVersion,
      originatorInfo      [0] IMPLICIT OriginatorInfo OPTIONAL,
      recipientInfos          RecipientInfos,
      authEncryptedContentInfo EncryptedContentInfo,
      authAttrs           [1] IMPLICIT AuthAttributes OPTIONAL,
      mac                     MessageAuthenticationCode,
      unauthAttrs         [2] IMPLICIT UnauthAttributes OPTIONAL }
    
    MessageAuthenticationCode ::= OCTET STRING
    

    version MUST be 0 (RFC 5083 §2.1).

  2. GCMParams / CCMParams AlgorithmIdentifier parameter classes (RFC 5084 §3.2):

    GCMParameters ::= SEQUENCE {
      aes-nonce        OCTET STRING, -- recommended size is 12 octets
      aes-ICVlen       AES-GCM-ICVlen DEFAULT 12 }
    
    AES-GCM-ICVlen ::= INTEGER (12 | 13 | 14 | 15 | 16)
    

    OIDs: id-aes128-GCM 2.16.840.1.101.3.4.1.6, id-aes192-GCM 2.16.840.1.101.3.4.1.26,
    id-aes256-GCM 2.16.840.1.101.3.4.1.46. If PR Emit GCMParameters SEQUENCE for AES-GCM AlgorithmIdentifier (RFC 5084) #487 lands, this item is already covered and
    AuthEnvelopedData can just consume it.

  3. ContentInfo.AUTH_ENVELOPED_DATA = "1.2.840.113549.1.9.16.1.23" plus an
    id_ContentType_AuthEnvelopedData export, matching the existing constants.

  4. AAD handling per RFC 5083 §2.1: when authAttrs is present its DER encoding is used as the
    AAD input with the universal SET OF tag substituted for the IMPLICIT [1] tag; when absent,
    "zero bits of input are provided for the AAD input".

  5. Optionally, route EnvelopedData.encrypt() away from AEAD algorithms — either throw with a
    pointer to AuthEnvelopedData, or keep the current behavior behind an explicit
    backward-compatibility flag. Silently emitting a structure no other implementation accepts is
    the worst of the three options.

Proposed API sketch

Deliberately isomorphic to EnvelopedData so existing user code is a near drop-in:

const aed = new pkijs.AuthEnvelopedData();

aed.addRecipientByCertificate(cert, { oaepHashAlgorithm: "SHA-256" });
// or
aed.addRecipientByPreDefinedData(kek, { keyIdentifier, keyEncryptionAlgorithm }, 1);

// authAttrs is optional; when supplied it is DER-encoded as SET OF for the AAD
await aed.encrypt(
  { name: "AES-GCM", length: 256, tagLength: 128, nonceLength: 12 },
  plaintext,
  /* authAttrs?: Attribute[] */
);

const ci = new pkijs.ContentInfo({
  contentType: pkijs.ContentInfo.AUTH_ENVELOPED_DATA,
  content: aed.toSchema(),
});

// reading
const parsed = new pkijs.AuthEnvelopedData({ schema: ci.content });
const plain = await parsed.decrypt(0, { preDefinedData: kek });
// or { recipientPrivateKey }

Implementation notes

The cryptographic work is already available; no new primitive is needed.

  • Web Crypto's AES-GCM already exposes additionalData and tagLength, so the AAD and the ICV
    length are directly expressible.
  • subtle.encrypt returns ciphertext || tag. Producing AuthEnvelopedData therefore only
    requires splitting the trailing aes-ICVlen bytes into mac, and re-concatenating them
    before subtle.decrypt. That is the entire delta over the existing GCM code path.
  • Nonce should default to 12 octets (RFC 5084 recommendation), not the 16 currently hardcoded at
    src/EnvelopedData.ts:800.
  • aes-ICVlen has DEFAULT 12, so a conformant encoder must omit the INTEGER when it is 12 and
    readers must apply the default when it is absent. (OpenSSL uses 16 and encodes it explicitly.)
  • Recipient handling (KeyTransRecipientInfo / KeyAgreeRecipientInfo / KEKRecipientInfo /
    PasswordRecipientInfo) is identical to EnvelopedData; only the content-encryption step and the
    outer SEQUENCE differ. Factoring the recipient logic out of EnvelopedData would let
    AuthEnvelopedData reuse it verbatim.
  • AES-CCM (also profiled by RFC 5084) has no Web Crypto support, so it can be left out of scope
    or handled only on the parsing side.

5. Reproduction fixtures

Both are self-contained. KEK = 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f,
key identifier = 6b656b31 ("kek1"), plaintext = Hello, AuthEnvelopedData!\n.

A. OpenSSL AuthEnvelopedData that PKI.js 3.4.0 cannot read (186 bytes, DER, base64):

MIG3BgsqhkiG9w0BCRABF6CBpzCBpAIBADFEokICAQQwBgQEa2VrMTALBglghkgBZQMEAS0EKPk2
9tsiaUO4SIyC3/DIREZ1TdEy2PA60T8dVVn9Iq+AIRPGGXUSjS4wRwYJKoZIhvcNAQcBMB4GCWCG
SAFlAwQBLjARBAyKpRCEe3Vu3/dOM68CARCAGpjMHyjKb/8QL39qJdTwe4RA3NcHE027TBiGBBDP
V6HS9V8YYVjhLQQce7Aa

Round-trips under OpenSSL:
openssl cms -decrypt -inform DER -in repro-gcm.cms -secretkey "$KEK" -secretkeyid 6b656b31
Hello, AuthEnvelopedData!

B. PKI.js 3.4.0 AES-GCM output that OpenSSL rejects (192 bytes, indefinite-length BER, base64):

MIAGCSqGSIb3DQEHA6CAMIACAQIxRqJEAgEEMAYEBGtlazEwDQYJYIZIAWUDBAEtBQAEKBUUaA48
4j6Yh7Yfo2w8F+/DoTbbd/1xz30iMWU57Q//mtJH7V5KN0gwgAYJKoZIhvcNAQcBMB0GCWCGSAFl
AwQBLgQQNGuumqcE22azhHkQwbFW0KCABCoA/0viwN5D7h1pMQuIB0nl8hc4NfajsYI2tJvb0OCX
NY4NTqzO01LAJBYAAAAAAAAAAAAA

PKI.js decrypts this one successfully (EnvelopedData.decrypt(0, { preDefinedData })
Hello, AuthEnvelopedData!\n), which is precisely the problem: the format is self-consistent and
externally unusable.

6. Relationship to existing issues

  • GCMParams in EnvelopedData required to be OCTET STRING instead of SEQUENCE #287 — "GCMParams in EnvelopedData required to be OCTET STRING instead of SEQUENCE" (open
    since 2020-09-23). Same root area, narrower scope: it is about the AlgorithmIdentifier
    parameter encoding. §3 above shows that fixing that encoding does not yield interop by itself.
    This request is the follow-up to my own comment on that thread.

    Note that the 2020 discussion on that thread already converged on this content type being the
    fix, and a PR was invited. @rmhrisk, 2020-09-24:

    It seems our allowance for GCM in the use of EnvelopedData is the root of the interop issue. I
    agree the PR is about adding support for this new type.

    and @YuryStrozhevsky the same day: "But PKIJS support EnvelopedData only. No support yet for the
    Authenticated-Enveloped Data." That PR was never opened. I am filing this separately because
    GCMParams in EnvelopedData required to be OCTET STRING instead of SEQUENCE #287's title and scope are the parameter encoding, so the container gap keeps getting conflated
    with it — @elkman asked for a status in 2025 and @TaaviE offered to contribute parts of it in
    2026, both on that thread, both unanswered.

  • PR Emit GCMParameters SEQUENCE for AES-GCM AlgorithmIdentifier (RFC 5084) #487 — "Emit GCMParameters SEQUENCE for AES-GCM AlgorithmIdentifier (RFC 5084)" (open).
    Complementary: it can provide item 2 of §4. It does not add the RFC 5083 container, and per §3 the
    output still will not interoperate without it.

  • Review rfc5751 bis and ensure PKIjs supports associated data structures #79 — "Review rfc5751 bis and ensure PKIjs supports associated data structures" (open since
    2017-01-06). AuthEnvelopedData is the concrete missing piece behind that general request.

What would help most right now is a maintainer decision on three points, so that whoever implements
this (@TaaviE offered on #287, and PR #487 is already touching the adjacent code) does not have to
guess:

  1. A separate AuthEnvelopedData class, or a mode flag on EnvelopedData?
  2. Is refactoring the recipient-handling code out of EnvelopedData for reuse acceptable?
  3. Which of the three options in §4 item 5 for the existing non-conformant EnvelopedData +
    AES-GCM path — throw, flag, or leave as is?

The two fixtures in §5 plus the byte-splice experiment in §3 are reproducible as-is and can serve as
test vectors for a PR. I am happy to produce more (other key sizes, authAttrs present, non-default
aes-ICVlen) on request.

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