Skip to content

Commit cfadddd

Browse files
committed
JceKeyAgreeRecipientInfoGenerator now generates a fresh 1-pass ECMQV ephemeral key pair for each KeyAgreeRecipientInfo rather than reusing one per generator, via a new KeyAgreeRecipientInfoGenerator.generationComplete() cleanup hook.
1 parent 5bb898c commit cfadddd

6 files changed

Lines changed: 159 additions & 16 deletions

File tree

‎docs/releasenotes.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ Date: 2026, TBD
5858
- DSTU7624Mac did not check the MAC size against the block size, failing in doFinal for one larger than the block; the constructor now requires a positive multiple of 8 bits no larger than the block.
5959
- DSTU7624WrapEngine.unwrap had no bound check on its input and accepted a single block, whose unwrapping is empty, and wrap accepted empty input; unwrap now rejects an input shorter than two blocks or beyond the buffer, and wrap requires at least one block.
6060
- Cipher.getInstance("DSTU7624/CCM/NoPadding") and the -128/-256/-512 forms generated the generic 12 byte CCM nonce when initialised without parameters, while the OID-registered DSTU 7624 CCM ciphers generate a whole block; the mode-string form now generates a whole-block nonce too.
61+
- JceKeyAgreeRecipientInfoGenerator created its 1-pass ECMQV ephemeral key pair once per generator rather than once per message, so a generator used for more than one message sent the same ephemeral key in each and derived the same key-encryption key for every recipient each time, withdrawing the ephemeral contribution RFC 5753 sec. 3.2 relies on. The messages were well formed and decrypted correctly. A fresh key pair is now generated for each KeyAgreeRecipientInfo, still shared by all of its recipients.
6162

6263
### 2.1.3 Additional Features and Functionality
6364

‎pkix/src/main/java/org/bouncycastle/cms/KeyAgreeRecipientInfoGenerator.java‎

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -34,27 +34,34 @@ protected KeyAgreeRecipientInfoGenerator(ASN1ObjectIdentifier keyAgreementOID,
3434

3535
public RecipientInfo generate(GenericKey contentEncryptionKey) throws CMSException
3636
{
37-
OriginatorPublicKey originatorPublicKey = createOriginatorPublicKey(originatorKeyInfo);
38-
OriginatorIdentifierOrKey originator = new OriginatorIdentifierOrKey(originatorPublicKey);
39-
40-
ASN1Encodable keyEncAlgParams = null;
41-
if (CMSUtils.isDES(keyEncryptionOID) || PKCSObjectIdentifiers.id_alg_CMSRC2wrap.equals(keyEncryptionOID))
42-
{
43-
keyEncAlgParams = DERNull.INSTANCE;
44-
}
45-
else if (CMSUtils.isGOST(keyAgreementOID))
37+
try
4638
{
47-
keyEncAlgParams = new Gost2814789KeyWrapParameters(CryptoProObjectIdentifiers.id_Gost28147_89_CryptoPro_A_ParamSet);
48-
}
39+
OriginatorPublicKey originatorPublicKey = createOriginatorPublicKey(originatorKeyInfo);
40+
OriginatorIdentifierOrKey originator = new OriginatorIdentifierOrKey(originatorPublicKey);
41+
42+
ASN1Encodable keyEncAlgParams = null;
43+
if (CMSUtils.isDES(keyEncryptionOID) || PKCSObjectIdentifiers.id_alg_CMSRC2wrap.equals(keyEncryptionOID))
44+
{
45+
keyEncAlgParams = DERNull.INSTANCE;
46+
}
47+
else if (CMSUtils.isGOST(keyAgreementOID))
48+
{
49+
keyEncAlgParams = new Gost2814789KeyWrapParameters(CryptoProObjectIdentifiers.id_Gost28147_89_CryptoPro_A_ParamSet);
50+
}
4951

50-
AlgorithmIdentifier keyEncAlgorithm = new AlgorithmIdentifier(keyEncryptionOID, keyEncAlgParams);
51-
AlgorithmIdentifier keyAgreeAlgorithm = new AlgorithmIdentifier(keyAgreementOID, keyEncAlgorithm);
52+
AlgorithmIdentifier keyEncAlgorithm = new AlgorithmIdentifier(keyEncryptionOID, keyEncAlgParams);
53+
AlgorithmIdentifier keyAgreeAlgorithm = new AlgorithmIdentifier(keyAgreementOID, keyEncAlgorithm);
5254

53-
ASN1Sequence recipients = generateRecipientEncryptedKeys(keyAgreeAlgorithm, keyEncAlgorithm, contentEncryptionKey);
55+
ASN1Sequence recipients = generateRecipientEncryptedKeys(keyAgreeAlgorithm, keyEncAlgorithm, contentEncryptionKey);
5456

55-
ASN1OctetString ukm = DEROctetString.fromContentsOptional(getUserKeyingMaterial(keyAgreeAlgorithm));
57+
ASN1OctetString ukm = DEROctetString.fromContentsOptional(getUserKeyingMaterial(keyAgreeAlgorithm));
5658

57-
return new RecipientInfo(new KeyAgreeRecipientInfo(originator, ukm, keyAgreeAlgorithm, recipients));
59+
return new RecipientInfo(new KeyAgreeRecipientInfo(originator, ukm, keyAgreeAlgorithm, recipients));
60+
}
61+
finally
62+
{
63+
generationComplete();
64+
}
5865
}
5966

6067
protected OriginatorPublicKey createOriginatorPublicKey(SubjectPublicKeyInfo originatorKeyInfo)
@@ -91,4 +98,17 @@ protected abstract ASN1Sequence generateRecipientEncryptedKeys(AlgorithmIdentifi
9198
AlgorithmIdentifier keyEncAlgorithm, GenericKey contentEncryptionKey) throws CMSException;
9299

93100
protected abstract byte[] getUserKeyingMaterial(AlgorithmIdentifier keyAgreeAlgorithm) throws CMSException;
101+
102+
/**
103+
* Called at the end of every {@link #generate(GenericKey)}, whether it completed normally or not, so a
104+
* subclass can release any state it holds for the duration of a single KeyAgreeRecipientInfo (e.g. an
105+
* ephemeral key pair). The default implementation does nothing.
106+
* <p>
107+
* This is called from a finally block, so an implementation must not throw: an exception thrown here would
108+
* replace the result, or the exception, of the generate call.
109+
* </p>
110+
*/
111+
protected void generationComplete()
112+
{
113+
}
94114
}

‎pkix/src/main/java/org/bouncycastle/cms/jcajce/JceKeyAgreeRecipientInfoGenerator.java‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,12 @@ protected byte[] getUserKeyingMaterial(AlgorithmIdentifier keyAgreeAlg)
296296
return userKeyingMaterial;
297297
}
298298

299+
protected void generationComplete()
300+
{
301+
// an MQV ephemeral key pair is shared by the recipients of one KeyAgreeRecipientInfo only
302+
ephemeralKP = null;
303+
}
304+
299305
private void init(ASN1ObjectIdentifier keyAgreementOID)
300306
throws CMSException
301307
{

‎pkix/src/main/jdk1.1/org/bouncycastle/cms/jcajce/JceKeyAgreeRecipientInfoGenerator.java‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,12 @@ protected byte[] getUserKeyingMaterial(AlgorithmIdentifier keyAgreeAlg)
250250
return userKeyingMaterial;
251251
}
252252

253+
protected void generationComplete()
254+
{
255+
// an MQV ephemeral key pair is shared by the recipients of one KeyAgreeRecipientInfo only
256+
ephemeralKP = null;
257+
}
258+
253259
private void init(ASN1ObjectIdentifier keyAgreementOID)
254260
throws CMSException
255261
{

‎pkix/src/test/java/org/bouncycastle/cms/test/NewEnvelopedDataTest.java‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
import org.bouncycastle.asn1.rosstandart.RosstandartObjectIdentifiers;
8282
import org.bouncycastle.asn1.x500.X500Name;
8383
import org.bouncycastle.asn1.cms.ecc.ECCCMSSharedInfo;
84+
import org.bouncycastle.asn1.cms.ecc.MQVuserKeyingMaterial;
8485
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
8586
import org.bouncycastle.asn1.x509.Extension;
8687
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
@@ -3501,6 +3502,58 @@ public void testECMQVKeyAgreeMultiple()
35013502
confirmNumberRecipients(recipients, 2);
35023503
}
35033504

3505+
/*
3506+
* The 1-pass ECMQV ephemeral key pair is shared by the recipients of one KeyAgreeRecipientInfo, but
3507+
* a generator reused for a second message must not send the same ephemeral key again.
3508+
*/
3509+
public void testECMQVFreshEphemeralKeyPerMessage()
3510+
throws Exception
3511+
{
3512+
byte[] data = Hex.decode("504b492d4320434d5320456e76656c6f706564446174612053616d706c65");
3513+
3514+
JceKeyAgreeRecipientInfoGenerator recipientGenerator = new JceKeyAgreeRecipientInfoGenerator(CMSAlgorithm.ECMQV_SHA1KDF,
3515+
_origEcKP.getPrivate(), _origEcKP.getPublic(), CMSAlgorithm.AES128_WRAP).setProvider(BC);
3516+
3517+
recipientGenerator.addRecipient(_reciEcCert);
3518+
recipientGenerator.addRecipient(_reciEcCert2);
3519+
3520+
CMSEnvelopedDataGenerator edGen = new CMSEnvelopedDataGenerator();
3521+
3522+
edGen.addRecipientInfoGenerator(recipientGenerator);
3523+
3524+
CMSEnvelopedData ed1 = edGen.generate(
3525+
new CMSProcessableByteArray(data),
3526+
new JceCMSContentEncryptorBuilder(CMSAlgorithm.AES128_CBC).setProvider(BC).build());
3527+
CMSEnvelopedData ed2 = edGen.generate(
3528+
new CMSProcessableByteArray(data),
3529+
new JceCMSContentEncryptorBuilder(CMSAlgorithm.AES128_CBC).setProvider(BC).build());
3530+
3531+
byte[] ephemeral1 = getMQVEphemeralPublicKey(ed1);
3532+
byte[] ephemeral2 = getMQVEphemeralPublicKey(ed2);
3533+
3534+
assertFalse("ephemeral key reused across messages", Arrays.equals(ephemeral1, ephemeral2));
3535+
3536+
confirmDataReceived(ed1.getRecipientInfos(), data, _reciEcCert, _reciEcKP.getPrivate(), BC);
3537+
confirmDataReceived(ed1.getRecipientInfos(), data, _reciEcCert2, _reciEcKP2.getPrivate(), BC);
3538+
confirmDataReceived(ed2.getRecipientInfos(), data, _reciEcCert, _reciEcKP.getPrivate(), BC);
3539+
confirmDataReceived(ed2.getRecipientInfos(), data, _reciEcCert2, _reciEcKP2.getPrivate(), BC);
3540+
}
3541+
3542+
private static byte[] getMQVEphemeralPublicKey(CMSEnvelopedData ed)
3543+
throws IOException
3544+
{
3545+
EnvelopedData env = EnvelopedData.getInstance(ContentInfo.getInstance(ed.getEncoded()).getContent());
3546+
assertEquals(1, env.getRecipientInfos().size());
3547+
3548+
KeyAgreeRecipientInfo kari = KeyAgreeRecipientInfo.getInstance(
3549+
RecipientInfo.getInstance(env.getRecipientInfos().getObjectAt(0)).getInfo());
3550+
assertEquals(2, kari.getRecipientEncryptedKeys().size());
3551+
3552+
MQVuserKeyingMaterial ukm = MQVuserKeyingMaterial.getInstance(kari.getUserKeyingMaterial().getOctets());
3553+
3554+
return ukm.getEphemeralPublicKey().getPublicKeyData().getBytes();
3555+
}
3556+
35043557
private static void confirmDataReceived(RecipientInformationStore recipients,
35053558
byte[] expectedData, X509Certificate reciCert, PrivateKey reciPrivKey, String provider)
35063559
throws CMSException, NoSuchProviderException, CertificateEncodingException, IOException

‎pkix/src/test/jdk1.4/org/bouncycastle/cms/test/NewEnvelopedDataTest.java‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@
3131
import org.bouncycastle.asn1.DERUTF8String;
3232
import org.bouncycastle.asn1.cms.Attribute;
3333
import org.bouncycastle.asn1.cms.AttributeTable;
34+
import org.bouncycastle.asn1.cms.ContentInfo;
35+
import org.bouncycastle.asn1.cms.EnvelopedData;
36+
import org.bouncycastle.asn1.cms.KeyAgreeRecipientInfo;
37+
import org.bouncycastle.asn1.cms.RecipientInfo;
38+
import org.bouncycastle.asn1.cms.ecc.MQVuserKeyingMaterial;
3439
import org.bouncycastle.asn1.kisa.KISAObjectIdentifiers;
3540
import org.bouncycastle.asn1.nist.NISTObjectIdentifiers;
3641
import org.bouncycastle.asn1.ntt.NTTObjectIdentifiers;
@@ -1030,6 +1035,58 @@ public void testECMQVKeyAgreeMultiple()
10301035
confirmNumberRecipients(recipients, 2);
10311036
}
10321037

1038+
/*
1039+
* The 1-pass ECMQV ephemeral key pair is shared by the recipients of one KeyAgreeRecipientInfo, but
1040+
* a generator reused for a second message must not send the same ephemeral key again.
1041+
*/
1042+
public void testECMQVFreshEphemeralKeyPerMessage()
1043+
throws Exception
1044+
{
1045+
byte[] data = Hex.decode("504b492d4320434d5320456e76656c6f706564446174612053616d706c65");
1046+
1047+
JceKeyAgreeRecipientInfoGenerator recipientGenerator = new JceKeyAgreeRecipientInfoGenerator(CMSAlgorithm.ECMQV_SHA1KDF,
1048+
_origEcKP.getPrivate(), _origEcKP.getPublic(), CMSAlgorithm.AES128_WRAP).setProvider(BC);
1049+
1050+
recipientGenerator.addRecipient(_reciEcCert);
1051+
recipientGenerator.addRecipient(_reciEcCert2);
1052+
1053+
CMSEnvelopedDataGenerator edGen = new CMSEnvelopedDataGenerator();
1054+
1055+
edGen.addRecipientInfoGenerator(recipientGenerator);
1056+
1057+
CMSEnvelopedData ed1 = edGen.generate(
1058+
new CMSProcessableByteArray(data),
1059+
new JceCMSContentEncryptorBuilder(CMSAlgorithm.AES128_CBC).setProvider(BC).build());
1060+
CMSEnvelopedData ed2 = edGen.generate(
1061+
new CMSProcessableByteArray(data),
1062+
new JceCMSContentEncryptorBuilder(CMSAlgorithm.AES128_CBC).setProvider(BC).build());
1063+
1064+
byte[] ephemeral1 = getMQVEphemeralPublicKey(ed1);
1065+
byte[] ephemeral2 = getMQVEphemeralPublicKey(ed2);
1066+
1067+
assertFalse("ephemeral key reused across messages", Arrays.equals(ephemeral1, ephemeral2));
1068+
1069+
confirmDataReceived(ed1.getRecipientInfos(), data, _reciEcCert, _reciEcKP.getPrivate(), BC);
1070+
confirmDataReceived(ed1.getRecipientInfos(), data, _reciEcCert2, _reciEcKP2.getPrivate(), BC);
1071+
confirmDataReceived(ed2.getRecipientInfos(), data, _reciEcCert, _reciEcKP.getPrivate(), BC);
1072+
confirmDataReceived(ed2.getRecipientInfos(), data, _reciEcCert2, _reciEcKP2.getPrivate(), BC);
1073+
}
1074+
1075+
private static byte[] getMQVEphemeralPublicKey(CMSEnvelopedData ed)
1076+
throws IOException
1077+
{
1078+
EnvelopedData env = EnvelopedData.getInstance(ContentInfo.getInstance(ed.getEncoded()).getContent());
1079+
assertEquals(1, env.getRecipientInfos().size());
1080+
1081+
KeyAgreeRecipientInfo kari = KeyAgreeRecipientInfo.getInstance(
1082+
RecipientInfo.getInstance(env.getRecipientInfos().getObjectAt(0)).getInfo());
1083+
assertEquals(2, kari.getRecipientEncryptedKeys().size());
1084+
1085+
MQVuserKeyingMaterial ukm = MQVuserKeyingMaterial.getInstance(kari.getUserKeyingMaterial().getOctets());
1086+
1087+
return ukm.getEphemeralPublicKey().getPublicKeyData().getBytes();
1088+
}
1089+
10331090
private static void confirmDataReceived(RecipientInformationStore recipients,
10341091
byte[] expectedData, X509Certificate reciCert, PrivateKey reciPrivKey, String provider)
10351092
throws CMSException, NoSuchProviderException, CertificateEncodingException, IOException

0 commit comments

Comments
 (0)