Skip to content

Commit c21ae79

Browse files
committed
Take the ITS explicit certificate signature choice from the content signer's curve
1 parent 94270ff commit c21ae79

3 files changed

Lines changed: 153 additions & 30 deletions

File tree

‎docs/releasenotes.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ Date: 2026, TBD
3939
- The PKCS#12 key stores wrote the MAC key-derivation parameters of a file they had loaded into every file they wrote afterwards, under whatever password the caller stored with. For PKCS12-PBMAC1 that carried the loaded file's PBKDF2 salt, iteration count, key length and PRF, because the parameters were minted only when the store held none and were then assigned back, so the branch ran once per store object rather than once per write - which also meant one store reused a single PBKDF2 salt across every write, including writes under different passwords, with no file loaded at all. The classic store inherited the MAC salt length and digest algorithm the same way, so a file declaring a zero-length MAC salt was re-stored with one, and a file it had loaded under RFC 9579 handed on that file's PBKDF2 salt too, both stores reading PBMAC1. The values were also latched before the MAC was verified and were not cleared by the load(null, null) a caller must issue to recover, so a file that failed the check left them behind for the caller's own file. The PBKDF2 salt and the MAC salt are now generated for every write, the MAC salt at no fewer than 8 octets, nothing is latched until the file has verified, and an AlgorithmIdentifier supplied through a PKCS12StoreParameter is still written as it was given. A loaded file's PRF, key length, digest algorithm and MacData iteration count are still kept, the last as before; the PBKDF2 count is kept where it is at least the count being written with and raised to it otherwise, since the file being re-stored is not the one that count was chosen for - RFC 9579's own test vectors ask for 2048. That count is now org.bouncycastle.pkcs12.pbkdf2_it_count, default 65,536, the write-side counterpart for a PBMAC1 MAC of what org.bouncycastle.pkcs12.store_it_count is for the PBE. Reading is unaffected: a file's MAC is verified with the parameters it carries, whatever they are (github #2450).
4040
- Cipher.SM9 took its data-encapsulation mode for decryption from the ciphertext rather than from the mode the Cipher was configured with. The GM/T 0080-2020 SM9Cipher structure names the mode in an enType field, but GM/T 0044.4 defines the authenticator as C3 = MAC(K2, C2), over the encapsulated message alone, so enType is not covered by it: re-encoding a ciphertext with the other enType leaves C1, C3 and C2 untouched and steers the recipient into the other mode. GM/T 0044.4 takes K1 and K2 from a single KDF output of klen = mlen + K2_len bits in stream mode and K1_len + K2_len bits in SM4 mode, where K1_len = 128, so when C2 is 16 bytes long the two modes make the identical KDF call and derive the same K1 and K2: a one-block SM4 ciphertext relabelled as stream mode passes the MAC check and the recipient returns K1 xor C2 - from which both the SM4 key K1 and the padded plaintext block follow, wherever that output is observable. CipherSpi now decrypts in the configured mode and rejects a ciphertext whose enType disagrees with it, so the mode is symmetric between encryption and decryption. That check compares two values a relabelling attacker can make agree, and so does not by itself protect a recipient whose Cipher is configured for stream mode - the relabelled one-block ciphertext then matches the configuration - so SM9Engine additionally refuses a 16-byte C2, the one C2 length at which the two modes collide, in both modes and both directions: on decryption, and on encryption a 16-byte message in stream mode and a message of fewer than 16 bytes, which pads to one block, in SM4 mode. Refusing the length on decryption protects the recipient that does so, but the message a relabelled ciphertext gives away is the SM4-mode sender's, who cannot tell whether the recipient's implementation refuses it, which is why the SM4 mode no longer produces one; messages of every other length are unchanged in both modes. A stream-mode ciphertext must accordingly be decrypted through a stream-mode Cipher ("SM9/XOR/NoPadding") rather than the SM4-mode default that Cipher.getInstance("SM9") gives; a message of fewer than 16 bytes has to be sent in stream mode, and one of exactly 16 bytes - a 128-bit key, say - in SM4 mode; and a ciphertext made by an earlier version whose C2 is 16 bytes long is no longer decrypted, whichever mode wrote it - a one-block SM4-mode ciphertext, or a stream-mode one carrying a 16-byte message. The SM9 KEM is unaffected.
4141

42+
- An ITS explicit certificate was signed with a Signature CHOICE picked by switching a VerificationKeyIndicator CHOICE - verificationKey 0, reconstructionValue 1 - against the PublicVerificationKey curve constants ecdsaNistP256 0, ecdsaBrainpoolP256r1 1 and ecdsaBrainpoolP384r1 2, so the signing key's curve never entered the decision: a brainpoolP256r1 certificate was issued tagged ecdsaNistP256Signature and then refused by BC's own ITS content verifier, nothing could be signed with a brainpoolP384r1 key at all, because the 32-byte NIST P-256 encoding was applied to a 384-bit r, and an end-entity certificate signed with a P-256 key under an implicit certificate was tagged ecdsaBrainpoolP256r1Signature, the reconstructionValue index colliding with that constant. ITSExplicitCertificateBuilder now takes the curve from the content signer, as ETSISignedDataBuilder already did, and the ITS tests cover all three curves.
43+
4244
### 2.1.3 Additional Features and Functionality
4345

4446
- The CRMF certificate request message controls now include the RFC 4211 sec. 6.6 protocolEncrKey control, which names the key a CA is to encrypt its response with: org.bouncycastle.cert.crmf.ProtocolEncrKeyControl carries the SubjectPublicKeyInfo the control is defined to take, and CertificateRequestMessage.getControl() recognises id-regCtrl-protocolEncrKey alongside the regToken, authenticator and pkiArchiveOptions controls it already returned (github PR #2443).

‎pkix/src/main/java/org/bouncycastle/its/ITSExplicitCertificateBuilder.java‎

Lines changed: 2 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55

66
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
77
import org.bouncycastle.asn1.nist.NISTObjectIdentifiers;
8-
import org.bouncycastle.asn1.sec.SECObjectIdentifiers;
9-
import org.bouncycastle.asn1.teletrust.TeleTrusTObjectIdentifiers;
108
import org.bouncycastle.its.operator.ECDSAEncoder;
119
import org.bouncycastle.its.operator.ITSContentSigner;
1210
import org.bouncycastle.oer.OEREncoder;
@@ -16,7 +14,6 @@
1614
import org.bouncycastle.oer.its.ieee1609dot2.basetypes.HashAlgorithm;
1715
import org.bouncycastle.oer.its.ieee1609dot2.basetypes.HashedId8;
1816
import org.bouncycastle.oer.its.ieee1609dot2.IssuerIdentifier;
19-
import org.bouncycastle.oer.its.ieee1609dot2.basetypes.PublicVerificationKey;
2017
import org.bouncycastle.oer.its.ieee1609dot2.basetypes.Signature;
2118
import org.bouncycastle.oer.its.ieee1609dot2.ToBeSignedCertificate;
2219
import org.bouncycastle.oer.its.ieee1609dot2.VerificationKeyIndicator;
@@ -62,18 +59,6 @@ public ITSCertificate build(CertificateId certificateId, ITSPublicVerificationKe
6259

6360
ToBeSignedCertificate tbsCertificate = tbsBldr.createToBeSignedCertificate();
6461

65-
ToBeSignedCertificate signerCert = null;
66-
VerificationKeyIndicator verificationKeyIndicator;
67-
if (signer.isForSelfSigning())
68-
{
69-
verificationKeyIndicator = tbsCertificate.getVerifyKeyIndicator();
70-
}
71-
else
72-
{
73-
signerCert = signer.getAssociatedCertificate().toASN1Structure().getToBeSigned();
74-
verificationKeyIndicator = signerCert.getVerifyKeyIndicator();
75-
}
76-
7762
OutputStream sOut = signer.getOutputStream();
7863

7964
try
@@ -87,21 +72,8 @@ public ITSCertificate build(CertificateId certificateId, ITSPublicVerificationKe
8772
throw new IllegalArgumentException("cannot produce certificate signature");
8873
}
8974

90-
Signature sig = null; // TODO: signature actually optional.
91-
switch (verificationKeyIndicator.getChoice())
92-
{
93-
case PublicVerificationKey.ecdsaNistP256:
94-
sig = ECDSAEncoder.toITS(SECObjectIdentifiers.secp256r1, signer.getSignature());
95-
break;
96-
case PublicVerificationKey.ecdsaBrainpoolP256r1:
97-
sig = ECDSAEncoder.toITS(TeleTrusTObjectIdentifiers.brainpoolP256r1, signer.getSignature());
98-
break;
99-
case PublicVerificationKey.ecdsaBrainpoolP384r1:
100-
sig = ECDSAEncoder.toITS(TeleTrusTObjectIdentifiers.brainpoolP384r1, signer.getSignature());
101-
break;
102-
default:
103-
throw new IllegalStateException("unknown key type");
104-
}
75+
// TODO: signature actually optional.
76+
Signature sig = ECDSAEncoder.toITS(signer.getCurveID(), signer.getSignature());
10577

10678
CertificateBase.Builder baseBldr = new CertificateBase.Builder();
10779

‎pkix/src/test/java/org/bouncycastle/its/test/ITSBasicTest.java‎

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,13 @@
88

99
import junit.framework.TestCase;
1010
import org.bouncycastle.asn1.ASN1Object;
11+
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
1112
import org.bouncycastle.asn1.DERIA5String;
1213
import org.bouncycastle.asn1.DEROctetString;
1314
import org.bouncycastle.asn1.nist.NISTNamedCurves;
1415
import org.bouncycastle.asn1.sec.SECObjectIdentifiers;
16+
import org.bouncycastle.asn1.teletrust.TeleTrusTNamedCurves;
17+
import org.bouncycastle.asn1.teletrust.TeleTrusTObjectIdentifiers;
1518
import org.bouncycastle.asn1.x9.X9ECParameters;
1619
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
1720
import org.bouncycastle.crypto.Digest;
@@ -56,6 +59,7 @@
5659
import org.bouncycastle.oer.its.ieee1609dot2.basetypes.SequenceOfPsidSsp;
5760
import org.bouncycastle.oer.its.ieee1609dot2.basetypes.SequenceOfPsidSspRange;
5861
import org.bouncycastle.oer.its.ieee1609dot2.basetypes.ServiceSpecificPermissions;
62+
import org.bouncycastle.oer.its.ieee1609dot2.basetypes.Signature;
5963
import org.bouncycastle.oer.its.ieee1609dot2.basetypes.SubjectAssurance;
6064
import org.bouncycastle.oer.its.ieee1609dot2.basetypes.SymmAlgorithm;
6165
import org.bouncycastle.oer.its.ieee1609dot2.basetypes.Time32;
@@ -303,6 +307,151 @@ public void testBuildSelfSigned()
303307
TestCase.assertTrue(valid);
304308
}
305309

310+
public void testBuildSelfSignedCurves()
311+
throws Exception
312+
{
313+
ASN1ObjectIdentifier[] curveIDs = new ASN1ObjectIdentifier[]
314+
{
315+
SECObjectIdentifiers.secp256r1,
316+
TeleTrusTObjectIdentifiers.brainpoolP256r1,
317+
TeleTrusTObjectIdentifiers.brainpoolP384r1
318+
};
319+
X9ECParameters[] curveParams = new X9ECParameters[]
320+
{
321+
NISTNamedCurves.getByOID(SECObjectIdentifiers.secp256r1),
322+
TeleTrusTNamedCurves.getByOID(TeleTrusTObjectIdentifiers.brainpoolP256r1),
323+
TeleTrusTNamedCurves.getByOID(TeleTrusTObjectIdentifiers.brainpoolP384r1)
324+
};
325+
int[] sigChoices = new int[]
326+
{
327+
Signature.ecdsaNistP256Signature,
328+
Signature.ecdsaBrainpoolP256r1Signature,
329+
Signature.ecdsaBrainpoolP384r1Signature
330+
};
331+
332+
for (int i = 0; i != curveIDs.length; i++)
333+
{
334+
ECNamedDomainParameters domainParameters = new ECNamedDomainParameters(curveIDs[i], curveParams[i]);
335+
336+
ECKeyPairGenerator generator = new ECKeyPairGenerator();
337+
generator.init(new ECKeyGenerationParameters(domainParameters, new SecureRandom()));
338+
AsymmetricCipherKeyPair kp = generator.generateKeyPair();
339+
340+
ECPublicKeyParameters publicVerificationKey = (ECPublicKeyParameters)kp.getPublic();
341+
ECPrivateKeyParameters privateKeyParameters = (ECPrivateKeyParameters)kp.getPrivate();
342+
343+
ToBeSignedCertificate.Builder tbsBuilder = new ToBeSignedCertificate.Builder();
344+
tbsBuilder.setAppPermissions(
345+
SequenceOfPsidSsp.builder()
346+
.setItem(PsidSsp.builder()
347+
.setPsid(new Psid(622))
348+
.setSsp(ServiceSpecificPermissions
349+
.bitmapSsp(new BitmapSsp(new DEROctetString(Hex.decode("0101"))))
350+
)
351+
.createPsidSsp())
352+
.createSequenceOfPsidSsp());
353+
tbsBuilder.setAssuranceLevel(new SubjectAssurance(new byte[]{(byte)0xC0}));
354+
tbsBuilder.setCrlSeries(new CrlSeries(1));
355+
356+
ITSContentSigner itsContentSigner = new BcITSContentSigner(
357+
new ECPrivateKeyParameters(privateKeyParameters.getD(), domainParameters));
358+
BcITSExplicitCertificateBuilder itsCertificateBuilder = new BcITSExplicitCertificateBuilder(itsContentSigner, tbsBuilder);
359+
360+
itsCertificateBuilder.setValidityPeriod(ITSValidityPeriod.from(new Date()).plusYears(1));
361+
362+
ITSCertificate newCert = itsCertificateBuilder.build(
363+
CertificateId.name(new Hostname("Legion of the BouncyCastle CA")),
364+
publicVerificationKey);
365+
366+
assertEquals(sigChoices[i], newCert.toASN1Structure().getSignature().getChoice());
367+
368+
BcITSContentVerifierProvider provider = new BcITSContentVerifierProvider(newCert);
369+
370+
assertTrue(newCert.isSignatureValid(provider));
371+
372+
//
373+
// an end-entity certificate issued under the certificate just built is
374+
// signed with the same key, so it carries the same signature choice.
375+
//
376+
ITSContentSigner caContentSigner = new BcITSContentSigner(
377+
new ECPrivateKeyParameters(privateKeyParameters.getD(), domainParameters), newCert);
378+
BcITSExplicitCertificateBuilder eeBuilder = new BcITSExplicitCertificateBuilder(caContentSigner, tbsBuilder);
379+
380+
eeBuilder.setValidityPeriod(ITSValidityPeriod.from(new Date()).plusYears(1));
381+
382+
ITSCertificate eeCert = eeBuilder.build(
383+
CertificateId.name(new Hostname("Legion of the BouncyCastle EE")),
384+
(ECPublicKeyParameters)generator.generateKeyPair().getPublic());
385+
386+
assertEquals(sigChoices[i], eeCert.toASN1Structure().getSignature().getChoice());
387+
388+
assertTrue(eeCert.isSignatureValid(new BcITSContentVerifierProvider(newCert)));
389+
}
390+
}
391+
392+
public void testBuildUnderImplicitIssuer()
393+
throws Exception
394+
{
395+
ECNamedDomainParameters domainParameters = new ECNamedDomainParameters(
396+
SECObjectIdentifiers.secp256r1, NISTNamedCurves.getByOID(SECObjectIdentifiers.secp256r1));
397+
398+
ECKeyPairGenerator generator = new ECKeyPairGenerator();
399+
generator.init(new ECKeyGenerationParameters(domainParameters, new SecureRandom()));
400+
401+
AsymmetricCipherKeyPair rootKp = generator.generateKeyPair();
402+
403+
ToBeSignedCertificate.Builder tbsBuilder = new ToBeSignedCertificate.Builder();
404+
tbsBuilder.setAppPermissions(
405+
SequenceOfPsidSsp.builder()
406+
.setItem(PsidSsp.builder()
407+
.setPsid(new Psid(622))
408+
.setSsp(ServiceSpecificPermissions
409+
.bitmapSsp(new BitmapSsp(new DEROctetString(Hex.decode("0101"))))
410+
)
411+
.createPsidSsp())
412+
.createSequenceOfPsidSsp());
413+
tbsBuilder.setAssuranceLevel(new SubjectAssurance(new byte[]{(byte)0xC0}));
414+
tbsBuilder.setCrlSeries(new CrlSeries(1));
415+
416+
ITSContentSigner rootContentSigner = new BcITSContentSigner(
417+
new ECPrivateKeyParameters(((ECPrivateKeyParameters)rootKp.getPrivate()).getD(), domainParameters));
418+
BcITSExplicitCertificateBuilder rootBuilder = new BcITSExplicitCertificateBuilder(rootContentSigner, tbsBuilder);
419+
420+
rootBuilder.setValidityPeriod(ITSValidityPeriod.from(new Date()).plusYears(1));
421+
422+
ITSCertificate rootCert = rootBuilder.build(
423+
CertificateId.name(new Hostname("Legion of the BouncyCastle CA")),
424+
(ECPublicKeyParameters)rootKp.getPublic());
425+
426+
//
427+
// an implicit certificate carries a reconstructionValue, so its verification
428+
// key indicator choice is 1 - which must not be read as a curve.
429+
//
430+
BcITSImplicitCertificateBuilder subBuilder = new BcITSImplicitCertificateBuilder(rootCert, tbsBuilder);
431+
432+
subBuilder.setValidityPeriod(ITSValidityPeriod.from(new Date()).plusYears(1));
433+
434+
ITSCertificate subCert = subBuilder.build(
435+
CertificateId.name(new Hostname("Legion of the BouncyCastle SUB")), BigInteger.ONE, BigIntegers.TWO);
436+
437+
assertEquals(VerificationKeyIndicator.reconstructionValue,
438+
subCert.toASN1Structure().getToBeSigned().getVerifyKeyIndicator().getChoice());
439+
440+
AsymmetricCipherKeyPair subKp = generator.generateKeyPair();
441+
442+
ITSContentSigner subContentSigner = new BcITSContentSigner(
443+
new ECPrivateKeyParameters(((ECPrivateKeyParameters)subKp.getPrivate()).getD(), domainParameters), subCert);
444+
BcITSExplicitCertificateBuilder eeBuilder = new BcITSExplicitCertificateBuilder(subContentSigner, tbsBuilder);
445+
446+
eeBuilder.setValidityPeriod(ITSValidityPeriod.from(new Date()).plusYears(1));
447+
448+
ITSCertificate eeCert = eeBuilder.build(
449+
CertificateId.name(new Hostname("Legion of the BouncyCastle EE")),
450+
(ECPublicKeyParameters)generator.generateKeyPair().getPublic());
451+
452+
assertEquals(Signature.ecdsaNistP256Signature, eeCert.toASN1Structure().getSignature().getChoice());
453+
}
454+
306455
public void testSelfSignedCA()
307456
throws Exception
308457
{

0 commit comments

Comments
 (0)