Skip to content

Commit eb7ad2e

Browse files
committed
Replaced the CMS SignedData version-preservation property with a CMSSignedData.asVersion(int) method that returns a copy with the version field forced, so a producer (e.g. Authenticode needing version 1) can pin it explicitly per-instance while replaceSigners/addDigestAlgorithm keep recomputing the RFC 5652 version by default, relates to github #2344.
1 parent d520021 commit eb7ad2e

6 files changed

Lines changed: 53 additions & 75 deletions

File tree

core/src/main/java/org/bouncycastle/util/Properties.java

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -294,17 +294,6 @@ public class Properties
294294
*/
295295
public static final String BKS_MAX_IT_COUNT = "org.bouncycastle.bks.max_it_count";
296296

297-
/**
298-
* When set to "true", {@code CMSSignedData.replaceSigners} / {@code addDigestAlgorithm}
299-
* carry the original {@code SignedData} version over to the rebuilt structure verbatim
300-
* instead of recomputing it. The default (unset / "false") recomputes the CMS version
301-
* from the content, which is the correct behaviour per RFC 5652. The opt-in preserves a
302-
* deliberately-set version for interop with producers that pin it - notably Microsoft
303-
* Authenticode, whose signatures must stay at version 1 even though their non-{@code id-data}
304-
* eContentType would otherwise compute to version 3. Relates to github #2344.
305-
*/
306-
public static final String CMS_SIGNEDDATA_PRESERVE_VERSION = "org.bouncycastle.cms.signeddata.preserve_version";
307-
308297
private Properties()
309298
{
310299
}

core/src/main/jdk1.4/org/bouncycastle/util/Properties.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ public class Properties
2727
public static final String PKCS12_IGNORE_USELESS_PASSWD = "org.bouncycastle.pkcs12.ignore_useless_passwd";
2828
public static final String PKCS12_MAX_IT_COUNT = "org.bouncycastle.pkcs12.max_it_count";
2929
public static final String BKS_MAX_IT_COUNT = "org.bouncycastle.bks.max_it_count";
30-
public static final String CMS_SIGNEDDATA_PRESERVE_VERSION = "org.bouncycastle.cms.signeddata.preserve_version";
3130
public static final String X509_CRL_CACHE_TTL = "org.bouncycastle.x509.crl_cache_ttl";
3231
public static final String X509_ENABLE_CRLDP = "org.bouncycastle.x509.enableCRLDP";
3332
public static final String PKCS12_ALLOW_SUN_SECRET_KEYS = "org.bouncycastle.pkcs12.allow_sun_secret_keys";

docs/releasenotes.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ <h3>2.1.3 Additional Features and Functionality</h3>
215215
<li>ARIA-GCM/CCM and SM4-GCM/CCM can now be used as CMS content-encryption algorithms for AuthEnvelopedData. Previously only AES-GCM/CCM (and ChaCha20-Poly1305) were recognised as AEAD on the generate side, ARIA had no OID-addressable AEAD AlgorithmParameters, and SM4 had no GCM/CCM Cipher at all; an attempt to round-trip CMS AuthEnvelopedData under any of these failed. The BC provider now registers OID-addressable SM4-GCM/SM4-CCM Ciphers, AlgorithmParameters and AlgorithmParameterGenerators (GMObjectIdentifiers.sms4_gcm 1.2.156.10197.1.104.8 / sms4_ccm .104.9) and ARIA-GCM/CCM AlgorithmParameters/AlgorithmParameterGenerators (NSRIObjectIdentifiers.id_aria128/192/256_gcm and _ccm), the CMS encrypt path recognises all eight OIDs as AEAD and generates the correct RFC 5084 nonce/parameters for them, and the lightweight org.bouncycastle.crypto.util.CipherFactory / CipherKeyGeneratorFactory / AlgorithmIdentifierFactory recognise them too, so both the JCA/JCE (JceCMSContentEncryptorBuilder) and lightweight (BcCMSContentEncryptorBuilder) content-encryptor builders can now produce and recover CMS AuthEnvelopedData under ARIA-GCM/CCM and SM4-GCM/CCM. Decode-side recognition was added in the same release.</li>
216216
<li>CMS recipients can now be restricted to a set of acceptable content-encryption algorithms. The new fluent setter setAllowedContentAlgorithms(Set&lt;ASN1ObjectIdentifier&gt;) is available on all JCA/JCE recipients (JceKeyTransRecipient, JceKeyAgreeRecipient, JceKEMRecipient, JcePasswordRecipient, JceKEKRecipient) and all lightweight recipients (BcKeyTransRecipient, BcKEKRecipient, BcPasswordRecipient); when set, an attempt to recover content protected under any other content-encryption (or, for AuthenticatedData, content-MAC) algorithm is refused before any key unwrap or content processing, throwing the new org.bouncycastle.cms.CMSAlgorithmNotAllowedException (a subclass of CMSException, so existing catch blocks are unaffected). This lets a caller reject an attacker downgrading the content-encryption algorithm carried in the recipient info to a weaker one. The shared state and check live on a new common base class org.bouncycastle.cms.AbstractRecipient; with no allowed set configured (the default) behaviour is unchanged and every algorithm is accepted.</li>
217217
<li>CMS recipients can now require a minimum AEAD authentication tag size when recovering AuthEnvelopedData. The new fluent setter setMinimumTagSize(int tagSizeInBits) is available on the JCA/JCE recipients (JceKeyTransRecipient, JceKeyAgreeRecipient, JceKEMRecipient, JcePasswordRecipient, JceKEKRecipient); when set, recovering content whose AES-GCM/CCM content-encryption algorithm carries a shorter ICV tag is refused before any decryption, throwing the new org.bouncycastle.cms.CMSTagLengthException (a subclass of CMSException). This is a caller-chosen floor layered on top of the existing global org.bouncycastle.gcm.allow_short_tags policy, letting a recipient reject an attacker downgrading the tag length (e.g. to 32 or 64 bits). The check (on org.bouncycastle.cms.AbstractRecipient) is a no-op for non-AEAD content and when no minimum is set, so default behaviour is unchanged.</li>
218-
<li>CMSSignedData.replaceSigners() and addDigestAlgorithm() recompute the SignedData version from the content by default (the correct value per RFC 5652), but can now be told to carry the original version over verbatim by setting the org.bouncycastle.cms.signeddata.preserve_version system/security property (Properties.CMS_SIGNEDDATA_PRESERVE_VERSION) to "true". This is for interop with producers that pin the version irrespective of content - notably Microsoft Authenticode, whose signatures must stay at version 1 even though their non-id-data eContentType would otherwise compute to version 3. The org.bouncycastle.asn1.cms.SignedData structure gains a constructor taking an explicit ASN1Integer version for this purpose (issue #2344).</li>
218+
<li>CMSSignedData.replaceSigners() and addDigestAlgorithm() recompute the SignedData version from the content per RFC 5652 (a non-id-data eContentType, for example, computes to version 3). A producer that needs to pin a specific version can now call the new CMSSignedData.asVersion(int), which returns a copy of the message with the SignedData version field forced to the given value and everything else unchanged. This covers interop with profiles that require a fixed version irrespective of content - notably Microsoft Authenticode, whose signatures must carry version 1 even though their SPC_INDIRECT_DATA eContentType would otherwise compute to version 3. The org.bouncycastle.asn1.cms.SignedData structure gains a constructor taking an explicit ASN1Integer version to support this (issue #2344).</li>
219219
<li>The Argon2 password-based KDF (RFC 9106) is now available through the BC JCE provider as SecretKeyFactory "ARGON2", complementing the existing lightweight org.bouncycastle.crypto.generators.Argon2BytesGenerator. Parameters are supplied via the new org.bouncycastle.jcajce.spec.Argon2KeySpec (variant Argon2d/Argon2i/Argon2id, version 1.0/1.3, salt, iterations, memory cost in KiB, parallelism and output key length in bits, plus optional secret and additional data), following the style of the existing SCRYPT SecretKeyFactory and ScryptKeySpec. The attacker-cost guards on the lightweight generator still apply: the memory exponent is capped by org.bouncycastle.argon2.max_memory_exp (default 24, i.e. 16 GiB).</li>
220220
<li>The BCJSSE hostname verifier no longer falls back to matching the subject CN of a server certificate when the certificate carries no SubjectAltName dNSName entry; by default only a matching SAN dNSName (or, for an IP literal, an iPAddress SAN) satisfies HTTPS endpoint identification. RFC 9525 sec. 6.3 deprecates CN as a TLS server identifier and CAB Forum Baseline Requirements 7.1.4.2 require SAN dNSName entries for publicly-trusted server certs; the CN fallback was also a Name-Constraints bypass surface, as RFC 5280 name constraints only constrain SubjectAltName entries of the constrained type, so a dNSName-constrained sub-CA could issue a SAN-less leaf carrying an unrelated hostname in CN. The legacy SunJSSE-compatible CN matching remains available, as an opt-in for certificates that only identify the server via CN, by setting the org.bouncycastle.jsse.hostname_check_cn_fallback system/security property (Properties.JSSE_HOSTNAME_CHECK_CN_FALLBACK) to "true".</li>
221221
<li>PKIXNameConstraintValidator gains an opt-in relaxed directoryName matching mode for GSMA SGP.22 v2.5 (Remote SIM Provisioning) certificate chains, gated behind the new org.bouncycastle.x509.sgp22_name_constraints system property (default off). When enabled, a permitted-subtree RDN is satisfied by any matching subject RDN regardless of position, additional subject attributes beyond those named in the subtree are tolerated (SGP.22 sec. 4.5.2.1.0.2), and a serialNumber RDN is matched with a startsWith comparison wherever it appears (SGP.22 sec. 4.5.2.1.0.3). This is deliberately looser than the contiguous-prefix DN matching mandated by RFC 5280 sec. 7.1. A pre-existing, ungated SGP.22 serialNumber startsWith concession in the strict matching path has been moved behind the same property, so default validation is now fully RFC 5280 strict (a single serialNumber name constraint is compared by equality, not prefix, unless the property is set) (issue #2327).</li>

pkix/src/main/java/org/bouncycastle/cms/CMSSignedData.java

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import org.bouncycastle.asn1.ASN1Encodable;
1717
import org.bouncycastle.asn1.ASN1EncodableVector;
1818
import org.bouncycastle.asn1.ASN1InputStream;
19+
import org.bouncycastle.asn1.ASN1Integer;
1920
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
2021
import org.bouncycastle.asn1.ASN1OctetString;
2122
import org.bouncycastle.asn1.ASN1Set;
@@ -32,7 +33,6 @@
3233
import org.bouncycastle.operator.DigestAlgorithmIdentifierFinder;
3334
import org.bouncycastle.operator.OperatorCreationException;
3435
import org.bouncycastle.util.Encodable;
35-
import org.bouncycastle.util.Properties;
3636
import org.bouncycastle.util.Store;
3737

3838
/**
@@ -250,6 +250,29 @@ public int getVersion()
250250
return signedData.getVersion().intValueExact();
251251
}
252252

253+
/**
254+
* Return a copy of this CMSSignedData with the SignedData version field forced to the given
255+
* value, leaving every other field unchanged.
256+
* <p>
257+
* The version is normally recomputed from the content per RFC 5652 sec. 5.1 (for example, a
258+
* non-id-data eContentType implies version 3), including by {@link #replaceSigners} and
259+
* {@link #addDigestAlgorithm}. This method lets a producer pin a specific version for interop
260+
* with profiles that require one - notably Microsoft Authenticode, whose signatures must carry
261+
* version 1 even though their SPC_INDIRECT_DATA eContentType would otherwise compute to 3.
262+
*
263+
* @param version the CMSVersion value to set.
264+
* @return a new CMSSignedData carrying the supplied version.
265+
*/
266+
public CMSSignedData asVersion(int version)
267+
{
268+
SignedData current = this.signedData;
269+
SignedData newContent = new SignedData(new ASN1Integer(version), current.getDigestAlgorithms(),
270+
current.getEncapContentInfo(), current.getCertificates(), current.getCRLs(), current.getSignerInfos());
271+
272+
return new CMSSignedData(this.contentInfo.getContentType(), newContent, this.signedContent,
273+
this.signerInfoStore);
274+
}
275+
253276
/**
254277
* return the collection of signers that are associated with the
255278
* signatures for the message.
@@ -566,7 +589,7 @@ public static CMSSignedData addDigestAlgorithm(CMSSignedData signedData, Algorit
566589

567590
SignedData oldContent = signedData.signedData;
568591

569-
SignedData newContent = createSignedData(oldContent, digestSet, oldContent.getEncapContentInfo(),
592+
SignedData newContent = new SignedData(digestSet, oldContent.getEncapContentInfo(),
570593
oldContent.getCertificates(), oldContent.getCRLs(), oldContent.getSignerInfos());
571594

572595
return new CMSSignedData(signedData.contentInfo.getContentType(), newContent, signedData.getSignedContent(),
@@ -633,30 +656,13 @@ public static CMSSignedData replaceSigners(CMSSignedData signedData, SignerInfor
633656

634657
SignedData oldContent = signedData.signedData;
635658

636-
SignedData newContent = createSignedData(oldContent, digestSet, oldContent.getEncapContentInfo(),
659+
SignedData newContent = new SignedData(digestSet, oldContent.getEncapContentInfo(),
637660
oldContent.getCertificates(), oldContent.getCRLs(), signerSet);
638661

639662
return new CMSSignedData(signedData.contentInfo.getContentType(), newContent, signedData.getSignedContent(),
640663
signerInformationStore);
641664
}
642665

643-
/**
644-
* Rebuild a SignedData from an existing one, replacing the digest and/or signer sets. By
645-
* default the CMS version is recomputed from the content (correct per RFC 5652); when
646-
* {@link Properties#CMS_SIGNEDDATA_PRESERVE_VERSION} is set the original version is carried
647-
* over verbatim, for interop with producers that pin it (e.g. Authenticode). See github #2344.
648-
*/
649-
private static SignedData createSignedData(SignedData oldContent, ASN1Set digestSet, ContentInfo contentInfo,
650-
ASN1Set certificates, ASN1Set crls, ASN1Set signerSet)
651-
{
652-
if (Properties.isOverrideSet(Properties.CMS_SIGNEDDATA_PRESERVE_VERSION))
653-
{
654-
return new SignedData(oldContent.getVersion(), digestSet, contentInfo, certificates, crls, signerSet);
655-
}
656-
657-
return new SignedData(digestSet, contentInfo, certificates, crls, signerSet);
658-
}
659-
660666
private static void compareAndReplaceAlgIds(AlgorithmIdentifier[] oldDigestAlgIds, AlgorithmIdentifier[] newDigestAlgIds)
661667
{
662668
for (int i = 0; i != newDigestAlgIds.length; i++)

pkix/src/test/java/org/bouncycastle/cms/test/NewSignedDataTest.java

Lines changed: 21 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,6 @@
114114
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
115115
import org.bouncycastle.test.TestResourceFinder;
116116
import org.bouncycastle.util.CollectionStore;
117-
import org.bouncycastle.util.Properties;
118117
import org.bouncycastle.util.Store;
119118
import org.bouncycastle.util.encoders.Base64;
120119
import org.bouncycastle.util.io.Streams;
@@ -834,13 +833,12 @@ public static Test suite()
834833
return new CMSTestSetup(new TestSuite(NewSignedDataTest.class));
835834
}
836835

837-
public void testReplaceSignersPreservesVersion()
836+
public void testAsVersion()
838837
throws Exception
839838
{
840-
// Microsoft Authenticode signatures pin SignedData version 1 even though their
841-
// eContentType (SPC_INDIRECT_DATA, not id-data) computes to version 3. By default
842-
// replaceSigners / addDigestAlgorithm recompute the correct CMS version, but
843-
// Properties.CMS_SIGNEDDATA_PRESERVE_VERSION opts in to carrying the original over.
839+
// replaceSigners recomputes the CMS version per RFC 5652 (a non-id-data eContentType such
840+
// as Authenticode's SPC_INDIRECT_DATA computes to version 3), but a producer can pin a
841+
// specific version with CMSSignedData.asVersion(int) - e.g. Authenticode requires 1.
844842
// Regression test for github #2344.
845843
ASN1ObjectIdentifier spcIndirectData = new ASN1ObjectIdentifier("1.3.6.1.4.1.311.2.1.4");
846844

@@ -856,41 +854,28 @@ public void testReplaceSignersPreservesVersion()
856854
gen.addSignerInfoGenerator(sigGenBuilder.build(sha1Signer, _origCert));
857855
gen.addCertificates(certs);
858856

859-
CMSSignedData generated = gen.generate(new CMSProcessableByteArray(spcIndirectData, "hello world".getBytes()), true);
857+
CMSSignedData s = gen.generate(new CMSProcessableByteArray(spcIndirectData, "hello world".getBytes()), true);
860858

861-
// Rebuild the SignedData with the version forced to 1 (as Authenticode producers do),
862-
// via raw ASN.1 so the fixture does not itself depend on the code path under test.
863-
ASN1Sequence sdSeq = ASN1Sequence.getInstance(generated.toASN1Structure().getContent());
864-
ASN1EncodableVector v = new ASN1EncodableVector();
865-
v.add(new ASN1Integer(1));
866-
for (int i = 1; i != sdSeq.size(); i++)
867-
{
868-
v.add(sdSeq.getObjectAt(i));
869-
}
870-
CMSSignedData authenticode = new CMSSignedData(
871-
new ContentInfo(CMSObjectIdentifiers.signedData, new DLSequence(v)));
859+
// a non-id-data eContentType yields version 3 both on generation and after replaceSigners.
860+
assertEquals("generated version", 3, s.getVersion());
861+
CMSSignedData replaced = CMSSignedData.replaceSigners(s, s.getSignerInfos());
862+
assertEquals("replaceSigners recomputes to version 3", 3, replaced.getVersion());
872863

873-
assertEquals("fixture must start at version 1", 1, authenticode.getVersion());
864+
// asVersion(1) pins the Authenticode version without disturbing anything else.
865+
CMSSignedData pinned = replaced.asVersion(1);
866+
assertEquals("asVersion pins the version", 1, pinned.getVersion());
874867

875-
// Default: recompute the correct CMS version (3 for a non-id-data eContentType).
876-
CMSSignedData recomputed = CMSSignedData.replaceSigners(authenticode, authenticode.getSignerInfos());
877-
assertEquals("default replaceSigners must recompute the version", 3, recomputed.getVersion());
868+
// the content type, certificates and signer are untouched, and the signature still verifies.
869+
assertEquals(spcIndirectData, pinned.getSignedContent().getContentType());
870+
assertEquals(replaced.getCertificates().getMatches(null).size(), pinned.getCertificates().getMatches(null).size());
878871

879-
// Opt-in: preserve the original version verbatim.
880-
System.setProperty(Properties.CMS_SIGNEDDATA_PRESERVE_VERSION, "true");
881-
try
882-
{
883-
CMSSignedData preservedReplace = CMSSignedData.replaceSigners(authenticode, authenticode.getSignerInfos());
884-
assertEquals("preserve flag must keep replaceSigners at version 1", 1, preservedReplace.getVersion());
872+
SignerInformation signer = (SignerInformation)pinned.getSignerInfos().getSigners().iterator().next();
873+
X509CertificateHolder cert = (X509CertificateHolder)pinned.getCertificates().getMatches(signer.getSID()).iterator().next();
874+
assertTrue("signature valid after asVersion",
875+
signer.verify(new JcaSimpleSignerInfoVerifierBuilder().setProvider(BC).build(cert)));
885876

886-
CMSSignedData preservedAdd = CMSSignedData.addDigestAlgorithm(authenticode,
887-
new AlgorithmIdentifier(NISTObjectIdentifiers.id_sha512));
888-
assertEquals("preserve flag must keep addDigestAlgorithm at version 1", 1, preservedAdd.getVersion());
889-
}
890-
finally
891-
{
892-
System.getProperties().remove(Properties.CMS_SIGNEDDATA_PRESERVE_VERSION);
893-
}
877+
// asVersion round-trips through an encode/decode.
878+
assertEquals(1, new CMSSignedData(pinned.getEncoded()).getVersion());
894879
}
895880

896881
public void setUp()

util/src/main/java/org/bouncycastle/asn1/cms/SignedData.java

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -120,12 +120,11 @@ public SignedData(
120120
}
121121

122122
/**
123-
* Construct a SignedData with an explicit, caller-supplied version rather than a
124-
* recomputed one. This is for transforming an existing SignedData (e.g. replacing the
125-
* signer set) where the original version must be carried over verbatim - recomputing it
126-
* via {@link #calculateVersion} can change a value the producer set deliberately (for
127-
* example Microsoft Authenticode signatures pin version 1 even though their eContentType
128-
* is not id-data, which would otherwise compute to version 3).
123+
* Construct a SignedData with an explicit, caller-supplied version rather than a recomputed
124+
* one. This lets a producer pin a specific version for a profile that requires one - for
125+
* example Microsoft Authenticode signatures pin version 1 even though their eContentType is
126+
* not id-data, which {@link #calculateVersion} would otherwise map to version 3. The
127+
* higher-level entry point is {@code CMSSignedData.asVersion(int)}.
129128
*/
130129
public SignedData(
131130
ASN1Integer version,

0 commit comments

Comments
 (0)