Skip to content

Commit 63cd29d

Browse files
committed
Added support for RFC 5126, relates to github issue #275
added restrictions around JCA/BC crypto classes to CLAUDE.md
1 parent 4e3d735 commit 63cd29d

18 files changed

Lines changed: 2402 additions & 0 deletions

CLAUDE.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,25 @@ When moving existing example code into `misc/`, remember to drop any matching `e
105105

106106
`BouncyCastleProvider` (in `prov`) registers algorithms by string name through `ConfigurableProvider.addAlgorithm("Cipher.SM2", "...GMCipherSpi$SM2")` etc. Per-algorithm registration code lives in `prov/src/main/java/org/bouncycastle/jcajce/provider/{asymmetric,symmetric,digest,keystore,...}/<Family>.java`. The corresponding `*Spi` classes (CipherSpi, KeyFactorySpi, KeyPairGeneratorSpi, etc.) are siblings under the same package. When adding or fixing a JCE-visible behaviour, the registration `Family.java` is the entry point; the underlying lightweight engine usually lives in `core/src/main/java/org/bouncycastle/crypto/engines/`.
107107

108+
### Package layering: `.bc` (lightweight) vs `.jcajce` (JCA/JCE)
109+
110+
The high-level modules (`pkix`, `pg`, `mail`/`jmail`, `tls`, `mls`) split their public surface by which low-level crypto stack a class touches:
111+
112+
- **`.bc` subpackages** — lightweight implementations using `org.bouncycastle.crypto.*` engines / signers / digests directly. Examples: `org.bouncycastle.cms.bc`, `org.bouncycastle.openpgp.bc`, `org.bouncycastle.operator.bc`.
113+
- **`.jcajce` subpackages** — JCA/JCE implementations that call `java.security.*` / `javax.crypto.*` classes (typically through a `JcaJceHelper` so the provider is overridable). Examples: `org.bouncycastle.cms.jcajce`, `org.bouncycastle.openpgp.operator.jcajce`, `org.bouncycastle.operator.jcajce`.
114+
- **Top-level packages** (e.g. `org.bouncycastle.cms`, `org.bouncycastle.openpgp`, `org.bouncycastle.cades`, `org.bouncycastle.cert`) — JCA-free abstractions. They may take `DigestCalculatorProvider` / `ContentSigner` / `X509CertificateHolder` etc., but must not import `java.security.MessageDigest`, `java.security.Signature`, `javax.crypto.Cipher`, or `java.security.cert.X509Certificate`.
115+
116+
The only JCA class allowed to be referenced from a non-`.jcajce` package is `java.security.SecureRandom`. Everything else must be in a `.jcajce` package.
117+
118+
Practical implications when adding code:
119+
120+
- Need an algorithm digest in a top-level utility? Take a `DigestCalculatorProvider` parameter and call `provider.get(algId).getOutputStream().write(...)` — never `MessageDigest.getInstance(...)`.
121+
- Need to verify a signature in a top-level utility? Take a `SignerInfoVerifier` (or similar operator) — never `Signature.getInstance(...)`.
122+
- Need to wrap an existing `Jca*` builder? Either (a) wrap the JCA-free parent (e.g. wrap `SignerInfoGeneratorBuilder` instead of `JcaSignerInfoGeneratorBuilder`) so the class can stay at the top, or (b) move the class into the `.jcajce` subpackage.
123+
- A top-level class that does need to expose a JCA-friendly factory method should ship the factory in its `.jcajce` peer instead of pulling JCA into the top package.
124+
125+
The rule applies uniformly to `pkix` (`cms`, `cades`, `tsp`, `cert`, `operator`, ...), `pg`, `mail`/`jmail`, `tls`, and `mls`. When adding a new package under any of these modules, decide on the split up-front: if any class needs `java.security` / `javax.crypto` beyond `SecureRandom`, the package should be a `.jcajce` subpackage, with a JCA-free top-level parent if appropriate.
126+
108127
### Test conventions
109128

110129
- Most tests extend `org.bouncycastle.util.test.SimpleTest` (not JUnit). They override `performTest()` and call `fail(msg)` / `isTrue(msg, cond)` / `areEqual(a, b)`. They are *not* discovered by Gradle directly — they're invoked from JUnit `AllTests` / `RegressionTest` wrappers.

docs/releasenotes.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ <h3>2.1.2 Defects Fixed</h3>
5252
</ul>
5353
<h3>2.2.3 Additional Features and Functionality</h3>
5454
<ul>
55+
<li>Initial CAdES (CMS Advanced Electronic Signatures) high-level builders, in the new org.bouncycastle.cades package, covering all four CAdES baseline levels (ETSI EN 319 122-1 / RFC 5126): B-B (CAdESSignerInfoGeneratorBuilder + CAdESSignedDataGenerator: mandatory ESS signing-certificate-v2 reference plus optional commitment-type / signature-policy / signer-location / content-hints), B-T (CAdESSignatureTimestampUtil: id-aa-signatureTimeStampToken over the SignerInfo signature value, with caller-fetched RFC 3161 token), B-LT (CAdESLongTermValuesUtil: id-aa-ets-certificateRefs / certValues / revocationRefs / revocationValues, supporting both CRLs and OCSP responses) and B-LTA (CAdESArchiveTimestampUtil: id-aa-ets-archiveTimestampV2 over the ETSI TS 101 733 v1.7.4 Annex A.2 canonicalisation, with archive-timestamps stripped so chains are renewable). CAdESLevelDetector inspects a SignerInformation and reports the attained level. The newer ETSI EN 319 122-1 v3 archive-timestamp form (id-aa-ets-archiveTimestampV3) is not yet supported. The low-level ASN.1 types under org.bouncycastle.asn1.esf and org.bouncycastle.asn1.ess remain available for callers needing finer-grained control (issue #275).</li>
5556
<li>The system/security property "org.bouncycastle.pkcs1.strict_digestinfo" (also exposed as Properties.PKCS1_STRICT_DIGESTINFO) lets callers opt in to strict RFC 8017 Appendix A.2.4 enforcement when verifying RSA PKCS#1 v1.5 signatures: when set to "true", DigestInfo encodings whose AlgorithmIdentifier omits the required NULL parameters octets are rejected. Default (unset / "false") preserves the legacy lenient fallback that accepts the two-byte-shorter encoding for compatibility with implementations that have historically produced it. Affects both the BC JCE provider's DigestSignatureSpi (e.g. Signature.getInstance("SHA256withRSA", "BC")) and the lightweight crypto RSADigestSigner (issue #2273).</li>
5657
<li>KeyPurposeId constants for the four Extended Key Usage KeyPurposeIds defined in RFC 9809 sec. 3: id_kp_configSigning (id-kp 41, signing general-purpose configuration files), id_kp_trustAnchorConfigSigning (id-kp 42, signing trust anchor configuration files), id_kp_updatePackageSigning (id-kp 43, signing software / firmware update packages) and id_kp_safetyCommunication (id-kp 44, authenticating peers for safety-critical communication). The matching human-readable names are also registered in X509CertificateFormatter so the new EKUs print symbolically.</li>
5758
<li>FalconPrivateKeyParameters.getPublicKeyParameters() returns the matching FalconPublicKeyParameters for a private key, mirroring MLDSAPrivateKeyParameters.getPublicKeyParameters(). Lets wallet / HSM code recover the public key from a stored private key without re-running keygen (issue #2297).</li>
Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
package org.bouncycastle.cades;
2+
3+
import java.io.IOException;
4+
import java.io.OutputStream;
5+
import java.util.ArrayList;
6+
import java.util.Collection;
7+
import java.util.Enumeration;
8+
import java.util.Iterator;
9+
import java.util.List;
10+
11+
import org.bouncycastle.asn1.ASN1Encodable;
12+
import org.bouncycastle.asn1.ASN1EncodableVector;
13+
import org.bouncycastle.asn1.ASN1Encoding;
14+
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
15+
import org.bouncycastle.asn1.ASN1OctetString;
16+
import org.bouncycastle.asn1.ASN1Primitive;
17+
import org.bouncycastle.asn1.ASN1Set;
18+
import org.bouncycastle.asn1.DERSet;
19+
import org.bouncycastle.asn1.cms.Attribute;
20+
import org.bouncycastle.asn1.cms.AttributeTable;
21+
import org.bouncycastle.asn1.cms.ContentInfo;
22+
import org.bouncycastle.asn1.cms.SignedData;
23+
import org.bouncycastle.asn1.esf.ESFAttributes;
24+
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
25+
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
26+
import org.bouncycastle.cms.CMSSignedData;
27+
import org.bouncycastle.cms.SignerId;
28+
import org.bouncycastle.cms.SignerInformation;
29+
import org.bouncycastle.cms.SignerInformationStore;
30+
import org.bouncycastle.operator.DigestCalculator;
31+
import org.bouncycastle.operator.DigestCalculatorProvider;
32+
import org.bouncycastle.operator.OperatorCreationException;
33+
import org.bouncycastle.tsp.TimeStampToken;
34+
35+
/**
36+
* Helpers for upgrading a CAdES B-LT signature to B-LTA by attaching an
37+
* archive-time-stamp covering the entire SignedData.
38+
* <p>
39+
* The helper emits the ETSI TS&nbsp;101&nbsp;733&nbsp;v1.7.4 "v2" form
40+
* ({@code id-aa-ets-archiveTimestampV2}, OID {@code 1.2.840.113549.1.9.16.2.48}).
41+
* The newer ETSI EN&nbsp;319&nbsp;122-1 "v3" form, which embeds an
42+
* {@code ats-hash-index-v3} signed attribute inside the TSA token, is not
43+
* yet supported &mdash; v3 generation requires custom signed-attribute
44+
* injection on the TSA token that is not yet exposed by the {@code tsp}
45+
* module.
46+
* <p>
47+
* The v2 imprint is the digest, under the caller-supplied algorithm, of
48+
* the canonical concatenation defined by ETSI TS&nbsp;101&nbsp;733 Annex A:
49+
* <ol>
50+
* <li>the content octets of the encapsulated content (omitted if the
51+
* signed-data is detached or has no eContent),</li>
52+
* <li>each {@code Certificate} from the {@code certificates} field, in
53+
* wire-encoding order, as its own DER encoding,</li>
54+
* <li>each {@code CertificateList} from the {@code crls} field, in
55+
* wire-encoding order, as its own DER encoding,</li>
56+
* <li>for each {@code SignerInfo} (in wire-encoding order), its DER
57+
* encoding with any archive-time-stamp attributes removed from
58+
* the {@code unsignedAttrs} field.</li>
59+
* </ol>
60+
* Typical caller flow:
61+
* <ol>
62+
* <li>{@link #computeArchiveTimestampImprint(CMSSignedData, AlgorithmIdentifier,
63+
* DigestCalculatorProvider)} returns the digest bytes.</li>
64+
* <li>The caller obtains a {@link TimeStampToken} from a TSA over those
65+
* bytes (transport is out of scope for BC).</li>
66+
* <li>{@link #applyArchiveTimestamp(CMSSignedData, SignerId, TimeStampToken)}
67+
* returns a new CMSSignedData with the token attached as an
68+
* {@code id-aa-ets-archiveTimestampV2} unsigned attribute on the
69+
* chosen signer.</li>
70+
* </ol>
71+
*/
72+
public final class CAdESArchiveTimestampUtil
73+
{
74+
/** ETSI TS 101 733 v1.7.4 id-aa-ets-archiveTimestampV2 OID. */
75+
public static final ASN1ObjectIdentifier id_aa_ets_archiveTimestampV2 =
76+
ESFAttributes.archiveTimestampV2;
77+
78+
private CAdESArchiveTimestampUtil()
79+
{
80+
}
81+
82+
/**
83+
* Digest the canonical archive-time-stamp v2 input for {@code signed}
84+
* under {@code digestAlg}.
85+
*/
86+
public static byte[] computeArchiveTimestampImprint(CMSSignedData signed,
87+
AlgorithmIdentifier digestAlg,
88+
DigestCalculatorProvider digCalcProv)
89+
throws CAdESException, OperatorCreationException, IOException
90+
{
91+
if (signed == null)
92+
{
93+
throw new NullPointerException("signed");
94+
}
95+
if (digestAlg == null)
96+
{
97+
throw new NullPointerException("digestAlg");
98+
}
99+
100+
DigestCalculator dc = digCalcProv.get(digestAlg);
101+
OutputStream out = dc.getOutputStream();
102+
feedCanonicalInput(signed, out);
103+
out.close();
104+
return dc.getDigest();
105+
}
106+
107+
/**
108+
* Attach an archive-time-stamp v2 to the signer matched by
109+
* {@code signerId}. If the signer already has one or more
110+
* archive-timestamp attributes the new token is appended into the
111+
* existing attribute's value-set; archive-timestamps form an ordered
112+
* chain so this preserves earlier timestamps.
113+
*/
114+
public static CMSSignedData applyArchiveTimestamp(CMSSignedData signed,
115+
SignerId signerId,
116+
TimeStampToken token)
117+
throws CAdESException
118+
{
119+
if (signed == null)
120+
{
121+
throw new NullPointerException("signed");
122+
}
123+
if (signerId == null)
124+
{
125+
throw new NullPointerException("signerId");
126+
}
127+
if (token == null)
128+
{
129+
throw new NullPointerException("token");
130+
}
131+
132+
SignerInformationStore signers = signed.getSignerInfos();
133+
Collection<SignerInformation> matched = signers.getSigners(signerId);
134+
if (matched.isEmpty())
135+
{
136+
throw new CAdESException("no signer matched in CMSSignedData");
137+
}
138+
139+
ContentInfo tokenCi;
140+
try
141+
{
142+
tokenCi = ContentInfo.getInstance(
143+
ASN1Primitive.fromByteArray(token.getEncoded()));
144+
}
145+
catch (IOException e)
146+
{
147+
throw new CAdESException("unable to encode TimeStampToken: " + e.getMessage(), e);
148+
}
149+
150+
List<SignerInformation> rebuilt = new ArrayList<SignerInformation>(signers.size());
151+
for (Iterator<SignerInformation> it = signers.getSigners().iterator(); it.hasNext(); )
152+
{
153+
SignerInformation cur = it.next();
154+
if (matched.contains(cur))
155+
{
156+
rebuilt.add(appendArchiveTimestamp(cur, tokenCi));
157+
}
158+
else
159+
{
160+
rebuilt.add(cur);
161+
}
162+
}
163+
return CMSSignedData.replaceSigners(signed, new SignerInformationStore(rebuilt));
164+
}
165+
166+
private static SignerInformation appendArchiveTimestamp(SignerInformation signer,
167+
ContentInfo tokenCi)
168+
{
169+
AttributeTable unsigned = signer.getUnsignedAttributes();
170+
171+
Attribute existing = unsigned == null
172+
? null
173+
: unsigned.get(id_aa_ets_archiveTimestampV2);
174+
175+
ASN1EncodableVector vals = new ASN1EncodableVector();
176+
if (existing != null)
177+
{
178+
ASN1Set prev = existing.getAttrValues();
179+
for (int i = 0; i != prev.size(); i++)
180+
{
181+
vals.add(prev.getObjectAt(i));
182+
}
183+
}
184+
vals.add(tokenCi);
185+
Attribute merged = new Attribute(id_aa_ets_archiveTimestampV2, new DERSet(vals));
186+
187+
ASN1EncodableVector all = unsigned == null
188+
? new ASN1EncodableVector()
189+
: unsigned.toASN1EncodableVector();
190+
191+
ASN1EncodableVector outVec = new ASN1EncodableVector();
192+
for (int i = 0; i != all.size(); ++i)
193+
{
194+
Attribute a = Attribute.getInstance(all.get(i));
195+
if (!id_aa_ets_archiveTimestampV2.equals(a.getAttrType()))
196+
{
197+
outVec.add(a);
198+
}
199+
}
200+
outVec.add(merged);
201+
202+
return SignerInformation.replaceUnsignedAttributes(signer, new AttributeTable(outVec));
203+
}
204+
205+
/**
206+
* Walk the SignedData and feed the canonical archive-time-stamp v2
207+
* input bytes to {@code out}.
208+
*/
209+
private static void feedCanonicalInput(CMSSignedData signed, OutputStream out)
210+
throws CAdESException, IOException
211+
{
212+
SignedData sd;
213+
try
214+
{
215+
sd = SignedData.getInstance(signed.toASN1Structure().getContent());
216+
}
217+
catch (Exception e)
218+
{
219+
throw new CAdESException("unable to read SignedData: " + e.getMessage(),
220+
e instanceof Exception ? (Exception)e : new RuntimeException(e));
221+
}
222+
223+
// (1) eContent octets, if present.
224+
ASN1Encodable encap = sd.getEncapContentInfo().getContent();
225+
if (encap instanceof ASN1OctetString)
226+
{
227+
out.write(((ASN1OctetString)encap).getOctets());
228+
}
229+
230+
// (2) certificates, in wire-encoding order.
231+
ASN1Set certs = sd.getCertificates();
232+
if (certs != null)
233+
{
234+
Enumeration e = certs.getObjects();
235+
while (e.hasMoreElements())
236+
{
237+
ASN1Encodable c = (ASN1Encodable)e.nextElement();
238+
out.write(c.toASN1Primitive().getEncoded(ASN1Encoding.DER));
239+
}
240+
}
241+
242+
// (3) CRLs, in wire-encoding order.
243+
ASN1Set crls = sd.getCRLs();
244+
if (crls != null)
245+
{
246+
Enumeration e = crls.getObjects();
247+
while (e.hasMoreElements())
248+
{
249+
ASN1Encodable c = (ASN1Encodable)e.nextElement();
250+
out.write(c.toASN1Primitive().getEncoded(ASN1Encoding.DER));
251+
}
252+
}
253+
254+
// (4) Each SignerInfo, with archive-time-stamps stripped from
255+
// unsignedAttrs and re-DER-encoded.
256+
for (SignerInformation s : signed.getSignerInfos().getSigners())
257+
{
258+
SignerInformation stripped = stripArchiveTimestamps(s);
259+
out.write(stripped.toASN1Structure().getEncoded(ASN1Encoding.DER));
260+
}
261+
}
262+
263+
private static SignerInformation stripArchiveTimestamps(SignerInformation signer)
264+
{
265+
AttributeTable unsigned = signer.getUnsignedAttributes();
266+
if (unsigned == null)
267+
{
268+
return signer;
269+
}
270+
271+
boolean hasAny = unsigned.get(id_aa_ets_archiveTimestampV2) != null
272+
|| unsigned.get(PKCSObjectIdentifiers.id_aa_ets_archiveTimestamp) != null;
273+
if (!hasAny)
274+
{
275+
return signer;
276+
}
277+
278+
ASN1EncodableVector kept = new ASN1EncodableVector();
279+
ASN1EncodableVector all = unsigned.toASN1EncodableVector();
280+
for (int i = 0; i != all.size(); ++i)
281+
{
282+
Attribute a = Attribute.getInstance(all.get(i));
283+
ASN1ObjectIdentifier type = a.getAttrType();
284+
if (!id_aa_ets_archiveTimestampV2.equals(type)
285+
&& !PKCSObjectIdentifiers.id_aa_ets_archiveTimestamp.equals(type))
286+
{
287+
kept.add(a);
288+
}
289+
}
290+
AttributeTable filtered = kept.size() == 0 ? null : new AttributeTable(kept);
291+
return SignerInformation.replaceUnsignedAttributes(signer, filtered);
292+
}
293+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package org.bouncycastle.cades;
2+
3+
import org.bouncycastle.cms.CMSException;
4+
5+
/**
6+
* Exception thrown when a CAdES builder cannot assemble a profile (e.g. the
7+
* signing certificate cannot be digested, the configured signature policy is
8+
* malformed, etc.). Sub-classes {@link CMSException} so callers that already
9+
* catch CMS exceptions can keep doing so.
10+
*/
11+
public class CAdESException
12+
extends CMSException
13+
{
14+
public CAdESException(String msg)
15+
{
16+
super(msg);
17+
}
18+
19+
public CAdESException(String msg, Exception cause)
20+
{
21+
super(msg, cause);
22+
}
23+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package org.bouncycastle.cades;
2+
3+
/**
4+
* CAdES baseline profile levels per ETSI EN&nbsp;319&nbsp;122-1, with the
5+
* older RFC&nbsp;5126 names alongside for callers that still target the
6+
* legacy specification. Each level is a strict superset of the one before it.
7+
* <ul>
8+
* <li>{@link #B_B} ({@code BES}) &mdash; basic electronic signature: CMS
9+
* SignedData with the mandatory ESS signing-certificate(-v2) reference
10+
* and optional commitment-type, signature-policy, signer-location and
11+
* content-hints signed attributes.</li>
12+
* <li>{@link #B_T} ({@code T}) &mdash; B-B plus an
13+
* {@code id-aa-signatureTimeStampToken} unsigned attribute carrying an
14+
* RFC&nbsp;3161 token over the SignerInfo signature value.</li>
15+
* <li>{@link #B_LT} ({@code C} / {@code X-L}) &mdash; B-T plus complete
16+
* certificate / revocation references and the corresponding certificate
17+
* / revocation value sets needed for offline long-term validation.</li>
18+
* <li>{@link #B_LTA} ({@code A}) &mdash; B-LT plus one or more
19+
* {@code id-aa-ets-archiveTimestampV3} unsigned attributes that protect
20+
* the entire signature against weakening of the original signature
21+
* algorithm.</li>
22+
* </ul>
23+
*/
24+
public enum CAdESLevel
25+
{
26+
/** ETSI EN&nbsp;319&nbsp;122-1 B-B / RFC&nbsp;5126 BES. */
27+
B_B,
28+
/** ETSI EN&nbsp;319&nbsp;122-1 B-T / RFC&nbsp;5126 T. */
29+
B_T,
30+
/** ETSI EN&nbsp;319&nbsp;122-1 B-LT / RFC&nbsp;5126 C-X-L. */
31+
B_LT,
32+
/** ETSI EN&nbsp;319&nbsp;122-1 B-LTA / RFC&nbsp;5126 A. */
33+
B_LTA;
34+
35+
/** Legacy RFC&nbsp;5126 alias for {@link #B_B}. */
36+
public static final CAdESLevel BES = B_B;
37+
/** Legacy RFC&nbsp;5126 alias for {@link #B_T}. */
38+
public static final CAdESLevel T = B_T;
39+
}

0 commit comments

Comments
 (0)