diff --git a/docs/releasenotes.md b/docs/releasenotes.md index 79ded2ec5d..476f6bdbba 100644 --- a/docs/releasenotes.md +++ b/docs/releasenotes.md @@ -22,6 +22,7 @@ Date: 2026, TBD - The lightweight SubjectPublicKeyInfoFactory and PrivateKeyInfoFactory encoded a GOST R 34.10-2012 key on one of the legacy CryptoPro curves under id-GostR3410-2001, although RFC 9215 sec. 4.2 permits those curves for 2012 keys. The digestParamSet now decides: a GOST R 34.11-94 parameter set means 2001 (RFC 4491 sec. 2.3.2), a GOST R 34.11-2012 digest or none means 2012 with 256/512 taken from the curve field size, and any other value is rejected. GOST3410PublicKeyAlgParameters treats digestParamSet as OPTIONAL on both read and write per RFC 9215, and PrivateKeyInfoFactory now passes attributes through for ECGOST3410 keys (bc-csharp github #707). - The name-constraint host canonicalisation removed a single RFC 1034 root-label dot, the only empty label a name may legally carry, but nothing refused the ones that are not legal: a dNSName, rfc822Name host or uniformResourceIdentifier host such as "example.com.." kept a phantom empty label after the strip and so matched no constraint at all, escaping an excluded subtree naming the host it appears to carry. A tested name whose host carries an empty label - a second trailing dot, a doubled dot or a leading dot - is now refused outright wherever a constraint of that type is in force, rather than canonicalised into a name it is not: removing the extra dots would decide on the caller's behalf that "example.com.." names example.com, which is not how a consumer resolving or comparing the name reads it, and refusing fails closed in both directions where canonicalising would newly admit such a name under a permitted subtree. The single trailing dot is canonicalised as before, a bare "." remains the root label rather than an empty one, and the guard is scoped to the host, so the doubled dot a quoted local part may legally carry is unaffected. Constraints are untouched - one may still begin with a dot, which is how this implementation spells "subdomains only" (github PR #2436). +- The RFC 9709 content-encryption AlgorithmIdentifier, which carries the real algorithm inside the parameters of an outer id-alg-cek-hkdf-sha256, was unwrapped at only one of the points where a CMS recipient makes a decision about it. Key-size validation was corrected for plain key transport in 1.86, but the same call in the KEK, RSA-KTS and KEM recipients, and in the key-transport recipient's own ORI-KEM branch, still compared the recovered key against the outer identifier, which registers no key size, so setKeySizeValidation(true) silently checked nothing there; the setAllowedContentAlgorithms allow-list and the setMinimumTagSize floor were applied to the outer identifier on every recipient family, including the one already corrected, so neither constrained an RFC 9709 message. The unwrap now happens once for the key-size check and once for the two policy checks, and every recipient polices and validates the content-encryption algorithm the message actually carries. A recipient with no allow-list, no tag floor and no key-size validation configured behaves exactly as before; a caller who listed id-alg-cek-hkdf-sha256 in an allow-list in order to admit RFC 9709 messages must now list the content-encryption algorithms themselves. ### 2.1.3 Additional Features and Functionality diff --git a/pkix/src/main/java/org/bouncycastle/cms/AbstractRecipient.java b/pkix/src/main/java/org/bouncycastle/cms/AbstractRecipient.java index 5af7621cdf..22eef4f164 100644 --- a/pkix/src/main/java/org/bouncycastle/cms/AbstractRecipient.java +++ b/pkix/src/main/java/org/bouncycastle/cms/AbstractRecipient.java @@ -4,6 +4,7 @@ import java.util.Set; import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.cms.CMSObjectIdentifiers; import org.bouncycastle.asn1.x509.AlgorithmIdentifier; /** @@ -52,4 +53,38 @@ protected final void checkTagSize(AlgorithmIdentifier contentAlgorithm) } } } + + /** + * Apply both of the recipient's restrictions to the content-encryption algorithm the message + * names, resolving an RFC 9709 key-derivation wrapper to the content-encryption algorithm it + * carries first, so that the restrictions are applied to the algorithm the content is actually + * encrypted under. + * + * @param contentAlgorithm the content-encryption AlgorithmIdentifier taken from the message. + * @throws CMSAlgorithmNotAllowedException if the content algorithm is not in the allowed set. + * @throws CMSTagLengthException if the tag size is below the configured minimum. + */ + protected final void checkContentAlgorithm(AlgorithmIdentifier contentAlgorithm) + throws CMSException + { + AlgorithmIdentifier encAlgId; + + // RFC 9709: the EncryptedContentInfo carries an outer id-alg-cek-hkdf-sha256 wrapping the + // real inner content-encryption AlgorithmIdentifier - it is the inner one these checks apply to. + if (contentAlgorithm.getAlgorithm().equals(CMSObjectIdentifiers.id_alg_cek_hkdf_sha256)) + { + encAlgId = AlgorithmIdentifier.getInstance(contentAlgorithm.getParameters()); + } + else + { + encAlgId = contentAlgorithm; + } + + if (!isContentAlgorithmAllowed(encAlgId.getAlgorithm())) + { + throw new CMSAlgorithmNotAllowedException("content-encryption algorithm not in recipient's allowed set: " + encAlgId.getAlgorithm()); + } + + checkTagSize(encAlgId); + } } diff --git a/pkix/src/main/java/org/bouncycastle/cms/bc/BcKEKRecipient.java b/pkix/src/main/java/org/bouncycastle/cms/bc/BcKEKRecipient.java index fc21ed13c6..864af7c577 100644 --- a/pkix/src/main/java/org/bouncycastle/cms/bc/BcKEKRecipient.java +++ b/pkix/src/main/java/org/bouncycastle/cms/bc/BcKEKRecipient.java @@ -5,7 +5,6 @@ import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.x509.AlgorithmIdentifier; import org.bouncycastle.cms.AbstractRecipient; -import org.bouncycastle.cms.CMSAlgorithmNotAllowedException; import org.bouncycastle.cms.CMSException; import org.bouncycastle.cms.KEKRecipient; import org.bouncycastle.crypto.CipherParameters; @@ -42,12 +41,7 @@ public BcKEKRecipient setAllowedContentAlgorithms(Set allo protected CipherParameters extractSecretKey(AlgorithmIdentifier keyEncryptionAlgorithm, AlgorithmIdentifier contentEncryptionAlgorithm, byte[] encryptedContentEncryptionKey) throws CMSException { - if (!isContentAlgorithmAllowed(contentEncryptionAlgorithm.getAlgorithm())) - { - throw new CMSAlgorithmNotAllowedException("content-encryption algorithm not in recipient's allowed set: " + contentEncryptionAlgorithm.getAlgorithm()); - } - - checkTagSize(contentEncryptionAlgorithm); + checkContentAlgorithm(contentEncryptionAlgorithm); try { diff --git a/pkix/src/main/java/org/bouncycastle/cms/bc/BcKeyTransRecipient.java b/pkix/src/main/java/org/bouncycastle/cms/bc/BcKeyTransRecipient.java index 202007e41a..58f014f435 100644 --- a/pkix/src/main/java/org/bouncycastle/cms/bc/BcKeyTransRecipient.java +++ b/pkix/src/main/java/org/bouncycastle/cms/bc/BcKeyTransRecipient.java @@ -5,7 +5,6 @@ import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.x509.AlgorithmIdentifier; import org.bouncycastle.cms.AbstractRecipient; -import org.bouncycastle.cms.CMSAlgorithmNotAllowedException; import org.bouncycastle.cms.CMSException; import org.bouncycastle.cms.KeyTransRecipient; import org.bouncycastle.crypto.CipherParameters; @@ -43,12 +42,7 @@ public BcKeyTransRecipient setAllowedContentAlgorithms(Set protected CipherParameters extractSecretKey(AlgorithmIdentifier keyEncryptionAlgorithm, AlgorithmIdentifier encryptedKeyAlgorithm, byte[] encryptedEncryptionKey) throws CMSException { - if (!isContentAlgorithmAllowed(encryptedKeyAlgorithm.getAlgorithm())) - { - throw new CMSAlgorithmNotAllowedException("content-encryption algorithm not in recipient's allowed set: " + encryptedKeyAlgorithm.getAlgorithm()); - } - - checkTagSize(encryptedKeyAlgorithm); + checkContentAlgorithm(encryptedKeyAlgorithm); AsymmetricKeyUnwrapper unwrapper = new BcRSAAsymmetricKeyUnwrapper(keyEncryptionAlgorithm, recipientKey); diff --git a/pkix/src/main/java/org/bouncycastle/cms/bc/BcPasswordRecipient.java b/pkix/src/main/java/org/bouncycastle/cms/bc/BcPasswordRecipient.java index b4f070e8e6..a2dd5d1c84 100644 --- a/pkix/src/main/java/org/bouncycastle/cms/bc/BcPasswordRecipient.java +++ b/pkix/src/main/java/org/bouncycastle/cms/bc/BcPasswordRecipient.java @@ -8,7 +8,6 @@ import org.bouncycastle.asn1.pkcs.PBKDF2Params; import org.bouncycastle.asn1.x509.AlgorithmIdentifier; import org.bouncycastle.cms.AbstractRecipient; -import org.bouncycastle.cms.CMSAlgorithmNotAllowedException; import org.bouncycastle.cms.CMSException; import org.bouncycastle.cms.PasswordRecipient; import org.bouncycastle.crypto.InvalidCipherTextException; @@ -62,12 +61,7 @@ public BcPasswordRecipient setAllowedContentAlgorithms(Set protected KeyParameter extractSecretKey(AlgorithmIdentifier keyEncryptionAlgorithm, AlgorithmIdentifier contentEncryptionAlgorithm, byte[] derivedKey, byte[] encryptedContentEncryptionKey) throws CMSException { - if (!isContentAlgorithmAllowed(contentEncryptionAlgorithm.getAlgorithm())) - { - throw new CMSAlgorithmNotAllowedException("content-encryption algorithm not in recipient's allowed set: " + contentEncryptionAlgorithm.getAlgorithm()); - } - - checkTagSize(contentEncryptionAlgorithm); + checkContentAlgorithm(contentEncryptionAlgorithm); Wrapper keyEncryptionCipher = EnvelopedDataHelper.createRFC3211Wrapper(keyEncryptionAlgorithm.getAlgorithm()); diff --git a/pkix/src/main/java/org/bouncycastle/cms/jcajce/EnvelopedDataHelper.java b/pkix/src/main/java/org/bouncycastle/cms/jcajce/EnvelopedDataHelper.java index 065e621163..fed8c5ef50 100644 --- a/pkix/src/main/java/org/bouncycastle/cms/jcajce/EnvelopedDataHelper.java +++ b/pkix/src/main/java/org/bouncycastle/cms/jcajce/EnvelopedDataHelper.java @@ -260,7 +260,21 @@ public Key getJceKey(AlgorithmIdentifier algId, GenericKey key) public void keySizeCheck(AlgorithmIdentifier keyAlgorithm, Key key) throws CMSException { - int expectedKeySize = EnvelopedDataHelper.KEY_SIZE_PROVIDER.getKeySize(keyAlgorithm); + AlgorithmIdentifier encAlgId; + + // RFC 9709: the EncryptedContentInfo carries an outer id-alg-cek-hkdf-sha256 wrapping the + // real inner content-encryption AlgorithmIdentifier - it is the inner one that fixes the + // key size the recovered CEK is expected to have. + if (keyAlgorithm.getAlgorithm().equals(CMSObjectIdentifiers.id_alg_cek_hkdf_sha256)) + { + encAlgId = AlgorithmIdentifier.getInstance(keyAlgorithm.getParameters()); + } + else + { + encAlgId = keyAlgorithm; + } + + int expectedKeySize = EnvelopedDataHelper.KEY_SIZE_PROVIDER.getKeySize(encAlgId); if (expectedKeySize > 0) { byte[] keyEnc = null; diff --git a/pkix/src/main/java/org/bouncycastle/cms/jcajce/JceKEKRecipient.java b/pkix/src/main/java/org/bouncycastle/cms/jcajce/JceKEKRecipient.java index 7c77b863ec..db41c421d6 100644 --- a/pkix/src/main/java/org/bouncycastle/cms/jcajce/JceKEKRecipient.java +++ b/pkix/src/main/java/org/bouncycastle/cms/jcajce/JceKEKRecipient.java @@ -9,7 +9,6 @@ import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.x509.AlgorithmIdentifier; import org.bouncycastle.cms.AbstractRecipient; -import org.bouncycastle.cms.CMSAlgorithmNotAllowedException; import org.bouncycastle.cms.CMSException; import org.bouncycastle.cms.KEKRecipient; import org.bouncycastle.operator.OperatorException; @@ -133,12 +132,7 @@ public JceKEKRecipient setMinimumTagSize(int tagSizeInBits) protected Key extractSecretKey(AlgorithmIdentifier keyEncryptionAlgorithm, AlgorithmIdentifier encryptedKeyAlgorithm, byte[] encryptedContentEncryptionKey) throws CMSException { - if (!isContentAlgorithmAllowed(encryptedKeyAlgorithm.getAlgorithm())) - { - throw new CMSAlgorithmNotAllowedException("content-encryption algorithm not in recipient's allowed set: " + encryptedKeyAlgorithm.getAlgorithm()); - } - - checkTagSize(encryptedKeyAlgorithm); + checkContentAlgorithm(encryptedKeyAlgorithm); SymmetricKeyUnwrapper unwrapper = helper.createSymmetricUnwrapper(keyEncryptionAlgorithm, recipientKey); diff --git a/pkix/src/main/java/org/bouncycastle/cms/jcajce/JceKEMRecipient.java b/pkix/src/main/java/org/bouncycastle/cms/jcajce/JceKEMRecipient.java index b0fb9d1e93..1b6f136f88 100644 --- a/pkix/src/main/java/org/bouncycastle/cms/jcajce/JceKEMRecipient.java +++ b/pkix/src/main/java/org/bouncycastle/cms/jcajce/JceKEMRecipient.java @@ -12,7 +12,6 @@ import org.bouncycastle.asn1.cms.KEMRecipientInfo; import org.bouncycastle.asn1.x509.AlgorithmIdentifier; import org.bouncycastle.cms.AbstractRecipient; -import org.bouncycastle.cms.CMSAlgorithmNotAllowedException; import org.bouncycastle.cms.CMSException; import org.bouncycastle.cms.KEMRecipient; import org.bouncycastle.operator.OperatorException; @@ -176,12 +175,7 @@ public JceKEMRecipient setMinimumTagSize(int tagSizeInBits) protected Key extractSecretKey(AlgorithmIdentifier keyEncryptionAlgorithm, AlgorithmIdentifier encryptedKeyAlgorithm, byte[] encryptedEncryptionKey) throws CMSException { - if (!isContentAlgorithmAllowed(encryptedKeyAlgorithm.getAlgorithm())) - { - throw new CMSAlgorithmNotAllowedException("content-encryption algorithm not in recipient's allowed set: " + encryptedKeyAlgorithm.getAlgorithm()); - } - - checkTagSize(encryptedKeyAlgorithm); + checkContentAlgorithm(encryptedKeyAlgorithm); // TODO: note there is a move to change the type for KEMs from KeyTrans, expect this to change KEMRecipientInfo gktParams = KEMRecipientInfo.getInstance(keyEncryptionAlgorithm.getParameters()); diff --git a/pkix/src/main/java/org/bouncycastle/cms/jcajce/JceKTSKeyTransRecipient.java b/pkix/src/main/java/org/bouncycastle/cms/jcajce/JceKTSKeyTransRecipient.java index c4db858bea..1a3356e819 100644 --- a/pkix/src/main/java/org/bouncycastle/cms/jcajce/JceKTSKeyTransRecipient.java +++ b/pkix/src/main/java/org/bouncycastle/cms/jcajce/JceKTSKeyTransRecipient.java @@ -14,7 +14,6 @@ import org.bouncycastle.asn1.cms.IssuerAndSerialNumber; import org.bouncycastle.asn1.x509.AlgorithmIdentifier; import org.bouncycastle.cms.AbstractRecipient; -import org.bouncycastle.cms.CMSAlgorithmNotAllowedException; import org.bouncycastle.cms.CMSException; import org.bouncycastle.cms.KeyTransRecipient; import org.bouncycastle.cms.KeyTransRecipientId; @@ -168,12 +167,7 @@ public JceKTSKeyTransRecipient setMinimumTagSize(int tagSizeInBits) protected Key extractSecretKey(AlgorithmIdentifier keyEncryptionAlgorithm, AlgorithmIdentifier encryptedKeyAlgorithm, byte[] encryptedEncryptionKey) throws CMSException { - if (!isContentAlgorithmAllowed(encryptedKeyAlgorithm.getAlgorithm())) - { - throw new CMSAlgorithmNotAllowedException("content-encryption algorithm not in recipient's allowed set: " + encryptedKeyAlgorithm.getAlgorithm()); - } - - checkTagSize(encryptedKeyAlgorithm); + checkContentAlgorithm(encryptedKeyAlgorithm); JceKTSKeyUnwrapper unwrapper = helper.createAsymmetricUnwrapper(keyEncryptionAlgorithm, recipientKey, ANONYMOUS_SENDER, partyVInfo); diff --git a/pkix/src/main/java/org/bouncycastle/cms/jcajce/JceKeyAgreeRecipient.java b/pkix/src/main/java/org/bouncycastle/cms/jcajce/JceKeyAgreeRecipient.java index 375d2f1a79..f6e79d8053 100644 --- a/pkix/src/main/java/org/bouncycastle/cms/jcajce/JceKeyAgreeRecipient.java +++ b/pkix/src/main/java/org/bouncycastle/cms/jcajce/JceKeyAgreeRecipient.java @@ -33,7 +33,6 @@ import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; import org.bouncycastle.asn1.x9.X9ObjectIdentifiers; import org.bouncycastle.cms.AbstractKeyAgreeRecipient; -import org.bouncycastle.cms.CMSAlgorithmNotAllowedException; import org.bouncycastle.cms.CMSException; import org.bouncycastle.jcajce.spec.GOST28147WrapParameterSpec; import org.bouncycastle.jcajce.spec.MQVParameterSpec; @@ -283,12 +282,7 @@ protected Key unwrapSessionKey(ASN1ObjectIdentifier wrapAlg, SecretKey agreedKey protected Key extractSecretKey(AlgorithmIdentifier keyEncryptionAlgorithm, AlgorithmIdentifier contentEncryptionAlgorithm, SubjectPublicKeyInfo senderKey, ASN1OctetString userKeyingMaterial, byte[] encryptedContentEncryptionKey) throws CMSException { - if (!isContentAlgorithmAllowed(contentEncryptionAlgorithm.getAlgorithm())) - { - throw new CMSAlgorithmNotAllowedException("content-encryption algorithm not in recipient's allowed set: " + contentEncryptionAlgorithm.getAlgorithm()); - } - - checkTagSize(contentEncryptionAlgorithm); + checkContentAlgorithm(contentEncryptionAlgorithm); try { diff --git a/pkix/src/main/java/org/bouncycastle/cms/jcajce/JceKeyTransRecipient.java b/pkix/src/main/java/org/bouncycastle/cms/jcajce/JceKeyTransRecipient.java index 28e6f7db2f..c875e29d0c 100644 --- a/pkix/src/main/java/org/bouncycastle/cms/jcajce/JceKeyTransRecipient.java +++ b/pkix/src/main/java/org/bouncycastle/cms/jcajce/JceKeyTransRecipient.java @@ -24,7 +24,6 @@ import org.bouncycastle.asn1.cryptopro.GostR3410TransportParameters; import org.bouncycastle.asn1.x509.AlgorithmIdentifier; import org.bouncycastle.cms.AbstractKeyTransRecipient; -import org.bouncycastle.cms.CMSAlgorithmNotAllowedException; import org.bouncycastle.cms.CMSException; import org.bouncycastle.jcajce.spec.GOST28147WrapParameterSpec; import org.bouncycastle.jcajce.spec.UserKeyingMaterialSpec; @@ -190,12 +189,7 @@ public JceKeyTransRecipient setMinimumTagSize(int tagSizeInBits) protected Key extractSecretKey(AlgorithmIdentifier keyEncryptionAlgorithm, AlgorithmIdentifier encryptedKeyAlgorithm, byte[] encryptedEncryptionKey) throws CMSException { - if (!isContentAlgorithmAllowed(encryptedKeyAlgorithm.getAlgorithm())) - { - throw new CMSAlgorithmNotAllowedException("content-encryption algorithm not in recipient's allowed set: " + encryptedKeyAlgorithm.getAlgorithm()); - } - - checkTagSize(encryptedKeyAlgorithm); + checkContentAlgorithm(encryptedKeyAlgorithm); if (isGOST(keyEncryptionAlgorithm.getAlgorithm())) { @@ -282,15 +276,7 @@ else if (CMSObjectIdentifiers.id_ori_kem.equals(keyEncryptionAlgorithm.getAlgori if (validateKeySize) { - if (encryptedKeyAlgorithm.getAlgorithm().equals(CMSObjectIdentifiers.id_alg_cek_hkdf_sha256)) - { - helper.keySizeCheck( - AlgorithmIdentifier.getInstance(encryptedKeyAlgorithm.getParameters()), key); - } - else - { - helper.keySizeCheck(encryptedKeyAlgorithm, key); - } + helper.keySizeCheck(encryptedKeyAlgorithm, key); } return key; diff --git a/pkix/src/main/java/org/bouncycastle/cms/jcajce/JcePasswordRecipient.java b/pkix/src/main/java/org/bouncycastle/cms/jcajce/JcePasswordRecipient.java index f445756f66..1949fae2e8 100644 --- a/pkix/src/main/java/org/bouncycastle/cms/jcajce/JcePasswordRecipient.java +++ b/pkix/src/main/java/org/bouncycastle/cms/jcajce/JcePasswordRecipient.java @@ -13,7 +13,6 @@ import org.bouncycastle.asn1.ASN1OctetString; import org.bouncycastle.asn1.x509.AlgorithmIdentifier; import org.bouncycastle.cms.AbstractRecipient; -import org.bouncycastle.cms.CMSAlgorithmNotAllowedException; import org.bouncycastle.cms.CMSException; import org.bouncycastle.cms.PasswordRecipient; @@ -89,12 +88,7 @@ public JcePasswordRecipient setMinimumTagSize(int tagSizeInBits) protected Key extractSecretKey(AlgorithmIdentifier keyEncryptionAlgorithm, AlgorithmIdentifier contentEncryptionAlgorithm, byte[] derivedKey, byte[] encryptedContentEncryptionKey) throws CMSException { - if (!isContentAlgorithmAllowed(contentEncryptionAlgorithm.getAlgorithm())) - { - throw new CMSAlgorithmNotAllowedException("content-encryption algorithm not in recipient's allowed set: " + contentEncryptionAlgorithm.getAlgorithm()); - } - - checkTagSize(contentEncryptionAlgorithm); + checkContentAlgorithm(contentEncryptionAlgorithm); Cipher keyEncryptionCipher = helper.createRFC3211Wrapper(keyEncryptionAlgorithm.getAlgorithm()); diff --git a/pkix/src/test/java/org/bouncycastle/cms/test/NewEnvelopedDataTest.java b/pkix/src/test/java/org/bouncycastle/cms/test/NewEnvelopedDataTest.java index f60c37accd..96b6e8ae82 100644 --- a/pkix/src/test/java/org/bouncycastle/cms/test/NewEnvelopedDataTest.java +++ b/pkix/src/test/java/org/bouncycastle/cms/test/NewEnvelopedDataTest.java @@ -1767,6 +1767,277 @@ public void testKeyTransWithHKDFKeySizeValidation() assertEquals(true, Arrays.equals(data, recData)); } + public void testKEKWithHKDFKeySizeValidation() + throws Exception + { + byte[] data = "WallaWallaWashington".getBytes(); + byte[] wrongSizeKey = new byte[32]; // 256 bits of keying material, content encryption algorithm says aes128-CBC + + for (int i = 0; i != wrongSizeKey.length; i++) + { + wrongSizeKey[i] = (byte)i; + } + + SecretKey kek = new SecretKeySpec(new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, "AES"); + byte[] kekId = new byte[]{1, 2, 3, 4, 5}; + + CMSEnvelopedDataGenerator edGen = new CMSEnvelopedDataGenerator(); + + edGen.addRecipientInfoGenerator(new JceKEKRecipientInfoGenerator(kekId, kek).setProvider(BC)); + + CMSEnvelopedData ed = edGen.generate( + new CMSProcessableByteArray(data), + new JceCMSContentEncryptorBuilder(CMSAlgorithm.AES128_CBC) + .setEnableSha256HKdf(true) + .setProvider(BC).build(wrongSizeKey)); + + assertEquals(ed.getEncryptionAlgOID(), CMSObjectIdentifiers.id_alg_cek_hkdf_sha256.getId()); + assertEquals(AlgorithmIdentifier.getInstance(ed.getContentEncryptionAlgorithm().getParameters()).getAlgorithm(), CMSAlgorithm.AES128_CBC); + + RecipientInformation recipient = (RecipientInformation)ed.getRecipientInfos().getRecipients().iterator().next(); + + try + { + recipient.getContent(new JceKEKEnvelopedRecipient(kek).setKeySizeValidation(true).setProvider(BC)); + fail("CEK size not matching content encryption algorithm not picked up"); + } + catch (CMSException e) + { + assertEquals("Expected key size for algorithm OID not found in recipient.", e.getMessage()); + } + + // without key size validation the mismatched message still decrypts + byte[] recData = recipient.getContent(new JceKEKEnvelopedRecipient(kek).setKeySizeValidation(false).setProvider(BC)); + + assertEquals(true, Arrays.equals(data, recData)); + + // a CEK matching the content encryption algorithm passes validation + edGen = new CMSEnvelopedDataGenerator(); + + edGen.addRecipientInfoGenerator(new JceKEKRecipientInfoGenerator(kekId, kek).setProvider(BC)); + + ed = edGen.generate( + new CMSProcessableByteArray(data), + new JceCMSContentEncryptorBuilder(CMSAlgorithm.AES128_CBC) + .setEnableSha256HKdf(true) + .setProvider(BC).build()); + + recipient = (RecipientInformation)ed.getRecipientInfos().getRecipients().iterator().next(); + + recData = recipient.getContent(new JceKEKEnvelopedRecipient(kek).setKeySizeValidation(true).setProvider(BC)); + + assertEquals(true, Arrays.equals(data, recData)); + } + + public void testKTSKeyTransWithHKDFKeySizeValidation() + throws Exception + { + byte[] data = "WallaWallaWashington".getBytes(); + byte[] wrongSizeKey = new byte[32]; // 256 bits of keying material, content encryption algorithm says aes128-CBC + + for (int i = 0; i != wrongSizeKey.length; i++) + { + wrongSizeKey[i] = (byte)i; + } + + CMSEnvelopedDataGenerator edGen = new CMSEnvelopedDataGenerator(); + + edGen.addRecipientInfoGenerator(new JceKTSKeyTransRecipientInfoGenerator(_reciCert, "AES", 128).setProvider(BC)); + + CMSEnvelopedData ed = edGen.generate( + new CMSProcessableByteArray(data), + new JceCMSContentEncryptorBuilder(CMSAlgorithm.AES128_CBC) + .setEnableSha256HKdf(true) + .setProvider(BC).build(wrongSizeKey)); + + assertEquals(ed.getEncryptionAlgOID(), CMSObjectIdentifiers.id_alg_cek_hkdf_sha256.getId()); + assertEquals(AlgorithmIdentifier.getInstance(ed.getContentEncryptionAlgorithm().getParameters()).getAlgorithm(), CMSAlgorithm.AES128_CBC); + + RecipientInformation recipient = (RecipientInformation)ed.getRecipientInfos().getRecipients().iterator().next(); + KeyTransRecipientId rid = (KeyTransRecipientId)recipient.getRID(); + + try + { + recipient.getContent(new JceKTSKeyTransEnvelopedRecipient(_reciKP.getPrivate(), rid).setKeySizeValidation(true).setProvider(BC)); + fail("CEK size not matching content encryption algorithm not picked up"); + } + catch (CMSException e) + { + assertEquals("Expected key size for algorithm OID not found in recipient.", e.getMessage()); + } + + // without key size validation the mismatched message still decrypts + byte[] recData = recipient.getContent(new JceKTSKeyTransEnvelopedRecipient(_reciKP.getPrivate(), rid).setKeySizeValidation(false).setProvider(BC)); + + assertEquals(true, Arrays.equals(data, recData)); + + // a CEK matching the content encryption algorithm passes validation + edGen = new CMSEnvelopedDataGenerator(); + + edGen.addRecipientInfoGenerator(new JceKTSKeyTransRecipientInfoGenerator(_reciCert, "AES", 128).setProvider(BC)); + + ed = edGen.generate( + new CMSProcessableByteArray(data), + new JceCMSContentEncryptorBuilder(CMSAlgorithm.AES128_CBC) + .setEnableSha256HKdf(true) + .setProvider(BC).build()); + + recipient = (RecipientInformation)ed.getRecipientInfos().getRecipients().iterator().next(); + rid = (KeyTransRecipientId)recipient.getRID(); + + recData = recipient.getContent(new JceKTSKeyTransEnvelopedRecipient(_reciKP.getPrivate(), rid).setKeySizeValidation(true).setProvider(BC)); + + assertEquals(true, Arrays.equals(data, recData)); + } + + public void testKEMWithHKDFKeySizeValidation() + throws Exception + { + byte[] data = "WallaWallaWashington".getBytes(); + byte[] wrongSizeKey = new byte[32]; // 256 bits of keying material, content encryption algorithm says aes128-CBC + + for (int i = 0; i != wrongSizeKey.length; i++) + { + wrongSizeKey[i] = (byte)i; + } + + CMSEnvelopedDataGenerator edGen = new CMSEnvelopedDataGenerator(); + + edGen.addRecipientInfoGenerator(new JceKEMRecipientInfoGenerator(_reciMLKem768Cert, CMSAlgorithm.AES256_WRAP) + .setKDF(CMSAlgorithm.SHA256_HKDF)); + + CMSEnvelopedData ed = edGen.generate( + new CMSProcessableByteArray(data), + new JceCMSContentEncryptorBuilder(CMSAlgorithm.AES128_CBC) + .setEnableSha256HKdf(true) + .setProvider(BC).build(wrongSizeKey)); + + assertEquals(ed.getEncryptionAlgOID(), CMSObjectIdentifiers.id_alg_cek_hkdf_sha256.getId()); + assertEquals(AlgorithmIdentifier.getInstance(ed.getContentEncryptionAlgorithm().getParameters()).getAlgorithm(), CMSAlgorithm.AES128_CBC); + + RecipientInformation recipient = (RecipientInformation)ed.getRecipientInfos().getRecipients().iterator().next(); + + try + { + recipient.getContent(new JceKEMEnvelopedRecipient(_reciMLKem768KP.getPrivate()).setKeySizeValidation(true).setProvider(BC)); + fail("CEK size not matching content encryption algorithm not picked up"); + } + catch (CMSException e) + { + assertEquals("Expected key size for algorithm OID not found in recipient.", e.getMessage()); + } + + // without key size validation the mismatched message still decrypts + byte[] recData = recipient.getContent(new JceKEMEnvelopedRecipient(_reciMLKem768KP.getPrivate()).setKeySizeValidation(false).setProvider(BC)); + + assertEquals(true, Arrays.equals(data, recData)); + + // a CEK matching the content encryption algorithm passes validation + edGen = new CMSEnvelopedDataGenerator(); + + edGen.addRecipientInfoGenerator(new JceKEMRecipientInfoGenerator(_reciMLKem768Cert, CMSAlgorithm.AES256_WRAP) + .setKDF(CMSAlgorithm.SHA256_HKDF)); + + ed = edGen.generate( + new CMSProcessableByteArray(data), + new JceCMSContentEncryptorBuilder(CMSAlgorithm.AES128_CBC) + .setEnableSha256HKdf(true) + .setProvider(BC).build()); + + recipient = (RecipientInformation)ed.getRecipientInfos().getRecipients().iterator().next(); + + recData = recipient.getContent(new JceKEMEnvelopedRecipient(_reciMLKem768KP.getPrivate()).setKeySizeValidation(true).setProvider(BC)); + + assertEquals(true, Arrays.equals(data, recData)); + } + + public void testKeyTransWithHKDFAllowedContentAlgorithms() + throws Exception + { + byte[] data = "WallaWallaWashington".getBytes(); + + CMSEnvelopedDataGenerator edGen = new CMSEnvelopedDataGenerator(); + + edGen.addRecipientInfoGenerator(new JceKeyTransRecipientInfoGenerator(_reciCert).setProvider(BC)); + + CMSEnvelopedData ed = edGen.generate( + new CMSProcessableByteArray(data), + new JceCMSContentEncryptorBuilder(CMSAlgorithm.AES256_CBC) + .setEnableSha256HKdf(true) + .setProvider(BC).build()); + + assertEquals(ed.getEncryptionAlgOID(), CMSObjectIdentifiers.id_alg_cek_hkdf_sha256.getId()); + + RecipientInformation recipient = (RecipientInformation)ed.getRecipientInfos().getRecipients().iterator().next(); + + // the allowed set names the content-encryption algorithm carried by the key derivation + byte[] recData = recipient.getContent(new JceKeyTransEnvelopedRecipient(_reciKP.getPrivate()).setProvider(BC) + .setAllowedContentAlgorithms(Collections.singleton(CMSAlgorithm.AES256_CBC))); + + assertEquals(true, Arrays.equals(data, recData)); + + // when the carried content-encryption algorithm is not in the allowed set, recovery is refused + try + { + recipient.getContent(new JceKeyTransEnvelopedRecipient(_reciKP.getPrivate()).setProvider(BC) + .setAllowedContentAlgorithms(Collections.singleton(CMSAlgorithm.AES128_CBC))); + + fail("content recovered under a disallowed content-encryption algorithm"); + } + catch (CMSAlgorithmNotAllowedException e) + { + assertEquals("content-encryption algorithm not in recipient's allowed set: " + CMSAlgorithm.AES256_CBC, e.getMessage()); + } + } + + public void testKeyTransWithHKDFMinimumTagSize() + throws Exception + { + byte[] data = "WallaWallaWashington".getBytes(); + + // a 96-bit (12-octet) GCM tag - valid under RFC 5084, but below a 128-bit floor + AlgorithmParameters algParams = AlgorithmParameters.getInstance("GCM", BC); + algParams.init(new AEADParameterSpec(new byte[12], 96)); + + OutputEncryptor enc = new JceCMSContentEncryptorBuilder(NISTObjectIdentifiers.id_aes128_GCM) + .setEnableSha256HKdf(true) + .setProvider(BC).setAlgorithmParameters(algParams).build(); + + CMSEnvelopedDataGenerator edGen = new CMSEnvelopedDataGenerator(); + + edGen.addRecipientInfoGenerator(new JceKeyTransRecipientInfoGenerator(_reciCert).setProvider(BC)); + + CMSEnvelopedData ed = edGen.generate(new CMSProcessableByteArray(data), enc); + + assertEquals(ed.getEncryptionAlgOID(), CMSObjectIdentifiers.id_alg_cek_hkdf_sha256.getId()); + + AlgorithmIdentifier contentAlgId = AlgorithmIdentifier.getInstance(ed.getContentEncryptionAlgorithm().getParameters()); + + assertEquals(NISTObjectIdentifiers.id_aes128_GCM, contentAlgId.getAlgorithm()); + assertEquals(12, GCMParameters.getInstance(contentAlgId.getParameters()).getIcvLen()); + + RecipientInformation recipient = (RecipientInformation)ed.getRecipientInfos().getRecipients().iterator().next(); + + // a minimum at or below the actual tag size recovers as normal + byte[] recData = recipient.getContent(new JceKeyTransEnvelopedRecipient(_reciKP.getPrivate()).setProvider(BC) + .setMinimumTagSize(96)); + + assertEquals(true, Arrays.equals(data, recData)); + + // a minimum above the actual tag size is refused with CMSTagLengthException + try + { + recipient.getContent(new JceKeyTransEnvelopedRecipient(_reciKP.getPrivate()).setProvider(BC) + .setMinimumTagSize(128)); + + fail("content recovered under a tag shorter than the configured minimum"); + } + catch (CMSTagLengthException e) + { + assertEquals("AEAD tag size 96 bits below required minimum 128 bits", e.getMessage()); + } + } + public void testKeyTransOAEPDefault() throws Exception {