Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/releasenotes.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ Date: 2026, TBD
- 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).
- 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.

- 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.

### 2.1.3 Additional Features and Functionality

- 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).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@

import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.nist.NISTObjectIdentifiers;
import org.bouncycastle.asn1.sec.SECObjectIdentifiers;
import org.bouncycastle.asn1.teletrust.TeleTrusTObjectIdentifiers;
import org.bouncycastle.its.operator.ECDSAEncoder;
import org.bouncycastle.its.operator.ITSContentSigner;
import org.bouncycastle.oer.OEREncoder;
Expand All @@ -16,7 +14,6 @@
import org.bouncycastle.oer.its.ieee1609dot2.basetypes.HashAlgorithm;
import org.bouncycastle.oer.its.ieee1609dot2.basetypes.HashedId8;
import org.bouncycastle.oer.its.ieee1609dot2.IssuerIdentifier;
import org.bouncycastle.oer.its.ieee1609dot2.basetypes.PublicVerificationKey;
import org.bouncycastle.oer.its.ieee1609dot2.basetypes.Signature;
import org.bouncycastle.oer.its.ieee1609dot2.ToBeSignedCertificate;
import org.bouncycastle.oer.its.ieee1609dot2.VerificationKeyIndicator;
Expand Down Expand Up @@ -62,18 +59,6 @@ public ITSCertificate build(CertificateId certificateId, ITSPublicVerificationKe

ToBeSignedCertificate tbsCertificate = tbsBldr.createToBeSignedCertificate();

ToBeSignedCertificate signerCert = null;
VerificationKeyIndicator verificationKeyIndicator;
if (signer.isForSelfSigning())
{
verificationKeyIndicator = tbsCertificate.getVerifyKeyIndicator();
}
else
{
signerCert = signer.getAssociatedCertificate().toASN1Structure().getToBeSigned();
verificationKeyIndicator = signerCert.getVerifyKeyIndicator();
}

OutputStream sOut = signer.getOutputStream();

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

Signature sig = null; // TODO: signature actually optional.
switch (verificationKeyIndicator.getChoice())
{
case PublicVerificationKey.ecdsaNistP256:
sig = ECDSAEncoder.toITS(SECObjectIdentifiers.secp256r1, signer.getSignature());
break;
case PublicVerificationKey.ecdsaBrainpoolP256r1:
sig = ECDSAEncoder.toITS(TeleTrusTObjectIdentifiers.brainpoolP256r1, signer.getSignature());
break;
case PublicVerificationKey.ecdsaBrainpoolP384r1:
sig = ECDSAEncoder.toITS(TeleTrusTObjectIdentifiers.brainpoolP384r1, signer.getSignature());
break;
default:
throw new IllegalStateException("unknown key type");
}
// TODO: signature actually optional.
Signature sig = ECDSAEncoder.toITS(signer.getCurveID(), signer.getSignature());

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

Expand Down
149 changes: 149 additions & 0 deletions pkix/src/test/java/org/bouncycastle/its/test/ITSBasicTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@

import junit.framework.TestCase;
import org.bouncycastle.asn1.ASN1Object;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.DERIA5String;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.nist.NISTNamedCurves;
import org.bouncycastle.asn1.sec.SECObjectIdentifiers;
import org.bouncycastle.asn1.teletrust.TeleTrusTNamedCurves;
import org.bouncycastle.asn1.teletrust.TeleTrusTObjectIdentifiers;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.Digest;
Expand Down Expand Up @@ -56,6 +59,7 @@
import org.bouncycastle.oer.its.ieee1609dot2.basetypes.SequenceOfPsidSsp;
import org.bouncycastle.oer.its.ieee1609dot2.basetypes.SequenceOfPsidSspRange;
import org.bouncycastle.oer.its.ieee1609dot2.basetypes.ServiceSpecificPermissions;
import org.bouncycastle.oer.its.ieee1609dot2.basetypes.Signature;
import org.bouncycastle.oer.its.ieee1609dot2.basetypes.SubjectAssurance;
import org.bouncycastle.oer.its.ieee1609dot2.basetypes.SymmAlgorithm;
import org.bouncycastle.oer.its.ieee1609dot2.basetypes.Time32;
Expand Down Expand Up @@ -303,6 +307,151 @@ public void testBuildSelfSigned()
TestCase.assertTrue(valid);
}

public void testBuildSelfSignedCurves()
throws Exception
{
ASN1ObjectIdentifier[] curveIDs = new ASN1ObjectIdentifier[]
{
SECObjectIdentifiers.secp256r1,
TeleTrusTObjectIdentifiers.brainpoolP256r1,
TeleTrusTObjectIdentifiers.brainpoolP384r1
};
X9ECParameters[] curveParams = new X9ECParameters[]
{
NISTNamedCurves.getByOID(SECObjectIdentifiers.secp256r1),
TeleTrusTNamedCurves.getByOID(TeleTrusTObjectIdentifiers.brainpoolP256r1),
TeleTrusTNamedCurves.getByOID(TeleTrusTObjectIdentifiers.brainpoolP384r1)
};
int[] sigChoices = new int[]
{
Signature.ecdsaNistP256Signature,
Signature.ecdsaBrainpoolP256r1Signature,
Signature.ecdsaBrainpoolP384r1Signature
};

for (int i = 0; i != curveIDs.length; i++)
{
ECNamedDomainParameters domainParameters = new ECNamedDomainParameters(curveIDs[i], curveParams[i]);

ECKeyPairGenerator generator = new ECKeyPairGenerator();
generator.init(new ECKeyGenerationParameters(domainParameters, new SecureRandom()));
AsymmetricCipherKeyPair kp = generator.generateKeyPair();

ECPublicKeyParameters publicVerificationKey = (ECPublicKeyParameters)kp.getPublic();
ECPrivateKeyParameters privateKeyParameters = (ECPrivateKeyParameters)kp.getPrivate();

ToBeSignedCertificate.Builder tbsBuilder = new ToBeSignedCertificate.Builder();
tbsBuilder.setAppPermissions(
SequenceOfPsidSsp.builder()
.setItem(PsidSsp.builder()
.setPsid(new Psid(622))
.setSsp(ServiceSpecificPermissions
.bitmapSsp(new BitmapSsp(new DEROctetString(Hex.decode("0101"))))
)
.createPsidSsp())
.createSequenceOfPsidSsp());
tbsBuilder.setAssuranceLevel(new SubjectAssurance(new byte[]{(byte)0xC0}));
tbsBuilder.setCrlSeries(new CrlSeries(1));

ITSContentSigner itsContentSigner = new BcITSContentSigner(
new ECPrivateKeyParameters(privateKeyParameters.getD(), domainParameters));
BcITSExplicitCertificateBuilder itsCertificateBuilder = new BcITSExplicitCertificateBuilder(itsContentSigner, tbsBuilder);

itsCertificateBuilder.setValidityPeriod(ITSValidityPeriod.from(new Date()).plusYears(1));

ITSCertificate newCert = itsCertificateBuilder.build(
CertificateId.name(new Hostname("Legion of the BouncyCastle CA")),
publicVerificationKey);

assertEquals(sigChoices[i], newCert.toASN1Structure().getSignature().getChoice());

BcITSContentVerifierProvider provider = new BcITSContentVerifierProvider(newCert);

assertTrue(newCert.isSignatureValid(provider));

//
// an end-entity certificate issued under the certificate just built is
// signed with the same key, so it carries the same signature choice.
//
ITSContentSigner caContentSigner = new BcITSContentSigner(
new ECPrivateKeyParameters(privateKeyParameters.getD(), domainParameters), newCert);
BcITSExplicitCertificateBuilder eeBuilder = new BcITSExplicitCertificateBuilder(caContentSigner, tbsBuilder);

eeBuilder.setValidityPeriod(ITSValidityPeriod.from(new Date()).plusYears(1));

ITSCertificate eeCert = eeBuilder.build(
CertificateId.name(new Hostname("Legion of the BouncyCastle EE")),
(ECPublicKeyParameters)generator.generateKeyPair().getPublic());

assertEquals(sigChoices[i], eeCert.toASN1Structure().getSignature().getChoice());

assertTrue(eeCert.isSignatureValid(new BcITSContentVerifierProvider(newCert)));
}
}

public void testBuildUnderImplicitIssuer()
throws Exception
{
ECNamedDomainParameters domainParameters = new ECNamedDomainParameters(
SECObjectIdentifiers.secp256r1, NISTNamedCurves.getByOID(SECObjectIdentifiers.secp256r1));

ECKeyPairGenerator generator = new ECKeyPairGenerator();
generator.init(new ECKeyGenerationParameters(domainParameters, new SecureRandom()));

AsymmetricCipherKeyPair rootKp = generator.generateKeyPair();

ToBeSignedCertificate.Builder tbsBuilder = new ToBeSignedCertificate.Builder();
tbsBuilder.setAppPermissions(
SequenceOfPsidSsp.builder()
.setItem(PsidSsp.builder()
.setPsid(new Psid(622))
.setSsp(ServiceSpecificPermissions
.bitmapSsp(new BitmapSsp(new DEROctetString(Hex.decode("0101"))))
)
.createPsidSsp())
.createSequenceOfPsidSsp());
tbsBuilder.setAssuranceLevel(new SubjectAssurance(new byte[]{(byte)0xC0}));
tbsBuilder.setCrlSeries(new CrlSeries(1));

ITSContentSigner rootContentSigner = new BcITSContentSigner(
new ECPrivateKeyParameters(((ECPrivateKeyParameters)rootKp.getPrivate()).getD(), domainParameters));
BcITSExplicitCertificateBuilder rootBuilder = new BcITSExplicitCertificateBuilder(rootContentSigner, tbsBuilder);

rootBuilder.setValidityPeriod(ITSValidityPeriod.from(new Date()).plusYears(1));

ITSCertificate rootCert = rootBuilder.build(
CertificateId.name(new Hostname("Legion of the BouncyCastle CA")),
(ECPublicKeyParameters)rootKp.getPublic());

//
// an implicit certificate carries a reconstructionValue, so its verification
// key indicator choice is 1 - which must not be read as a curve.
//
BcITSImplicitCertificateBuilder subBuilder = new BcITSImplicitCertificateBuilder(rootCert, tbsBuilder);

subBuilder.setValidityPeriod(ITSValidityPeriod.from(new Date()).plusYears(1));

ITSCertificate subCert = subBuilder.build(
CertificateId.name(new Hostname("Legion of the BouncyCastle SUB")), BigInteger.ONE, BigIntegers.TWO);

assertEquals(VerificationKeyIndicator.reconstructionValue,
subCert.toASN1Structure().getToBeSigned().getVerifyKeyIndicator().getChoice());

AsymmetricCipherKeyPair subKp = generator.generateKeyPair();

ITSContentSigner subContentSigner = new BcITSContentSigner(
new ECPrivateKeyParameters(((ECPrivateKeyParameters)subKp.getPrivate()).getD(), domainParameters), subCert);
BcITSExplicitCertificateBuilder eeBuilder = new BcITSExplicitCertificateBuilder(subContentSigner, tbsBuilder);

eeBuilder.setValidityPeriod(ITSValidityPeriod.from(new Date()).plusYears(1));

ITSCertificate eeCert = eeBuilder.build(
CertificateId.name(new Hostname("Legion of the BouncyCastle EE")),
(ECPublicKeyParameters)generator.generateKeyPair().getPublic());

assertEquals(Signature.ecdsaNistP256Signature, eeCert.toASN1Structure().getSignature().getChoice());
}

public void testSelfSignedCA()
throws Exception
{
Expand Down
Loading