Skip to content

Commit 3f77007

Browse files
committed
Further updates to CMC simplification, relates to github #1452.
1 parent f1bd404 commit 3f77007

3 files changed

Lines changed: 155 additions & 20 deletions

File tree

docs/releasenotes.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ <h3>2.2.3 Additional Features and Functionality</h3>
6262
<li>X509v3CertificateBuilder now exposes setters for the constructor arguments (setIssuer, setSerialNumber, setNotBefore, setNotAfter, setSubject, setSubjectPublicKeyInfo) to support equivalence-comparison use cases (issue #1545).</li>
6363
<li>CMS EnvelopedData now supports RFC 8418 ECDH key agreement using X25519 or X448 with HKDF (SHA-256/384/512). Three CMSAlgorithm constants (ECDH_HKDF_SHA256, ECDH_HKDF_SHA384, ECDH_HKDF_SHA512) and the corresponding KeyAgreement registrations (XDHwithSHA256HKDF / XDHwithSHA384HKDF / XDHwithSHA512HKDF) have been added (issue #1845).</li>
6464
<li>The SM2 JCE Cipher now accepts a ciphertext-format mode in the transformation string. Cipher.getInstance("SM2/C1C3C2/NoPadding", "BC") and Cipher.getInstance("SM2/C1C2C3/NoPadding", "BC") select between the two SM2Engine modes; the previous "SM2"/"SM2/NONE/NoPadding" forms continue to default to C1C2C3 (issue #1302).</li>
65-
<li>SimplePKIResponse now also accepts the unsigned Full PKI Response variant used for EST server-generated errors (RFC 7030 4.2.3 / 4.4.2): a CMS SignedData with no SignerInfos whose encapsulated content is an id-cct-PKIResponse PKIResponse SEQUENCE. New accessors getPKIResponse(), getControlAttributes(), getCmsContents() and getStatusInfoV2() expose the embedded PKIResponse content as structured TaggedAttribute / TaggedContentInfo / CMCStatusInfoV2 objects so callers no longer need to walk the raw ASN.1. A new PKIResponseBuilder produces SimplePKIResponse instances directly, with addControlAttribute / addStatusInfoV2 / addCmsContent / addOtherMsg helpers so EST error responses can be assembled without manually composing the SignedData and PKIResponse SEQUENCE. CMSSignedData has a new getSignedContentType() returning the encapsulated content type as an ASN1ObjectIdentifier alongside the existing getSignedContentTypeOID() (issue #1452).</li>
65+
<li>SimplePKIResponse now also accepts the unsigned Full PKI Response variant used for EST server-generated errors (RFC 7030 4.2.3 / 4.4.2): a CMS SignedData carrying an id-cct-PKIResponse PKIResponse SEQUENCE. New accessors getPKIResponse(), getControlAttributes(), getCmsContents() and getStatusInfoV2() return the embedded content as structured TaggedAttribute / TaggedContentInfo / CMCStatusInfoV2 objects. A new PKIResponseBuilder assembles SimplePKIResponse instances for both shapes — the Full PKI Response error case (addControlAttribute / addStatusInfoV2 / addCmsContent / addOtherMsg) and the cert-delivery success case used by EST /simpleenroll (addCertificate). CMSSignedData has a new getSignedContentType() returning the encapsulated content type as an ASN1ObjectIdentifier (issue #1452).</li>
6666
<li>org.bouncycastle.gpg.KeyGripCalculator computes the GnuPG-style 20-byte SHA-1 keygrip for a BCPGKey. The calculator is constructed with a caller-supplied SHA-1 PGPDigestCalculator. RSA public keys are supported initially (matching libgcrypt's _gcry_rsa_compute_keygrip: SHA-1 of the canonical unsigned big-endian modulus); other key types throw on calculateKeygrip() until support is added (issue #676).</li>
6767
<li>CMS key transport now supports the SM2 cipher: JceKeyTransRecipientInfoGenerator wraps the CEK and JceKeyTransRecipient unwraps it when the keyEncryptionAlgorithm is GMObjectIdentifiers.sm2encrypt_with_sm3. The ciphertext format defaults to C1C3C2 (GB/T 35276 envelope encoding) and the SM4-CBC content encryption is exposed as the new CMSAlgorithm.SM4_CBC constant.</li>
6868
<li>org.bouncycastle.asn1.pkcs.SecretBag — RFC 7292 ASN.1 holder for the PKCS#12 secretBag bag type, complementing the existing CertBag / CRLBag classes. PKCS12SecretBag and PKCS12SecretBagBuilder in org.bouncycastle.pkcs sit alongside the SafeBag / SafeBagBuilder pair: PKCS12SafeBagBuilder takes a PKCS12SecretBag in a new constructor, and PKCS12SafeBag.getBagValue() returns a PKCS12SecretBag for safe bags of type secretBag.</li>

pkix/src/main/java/org/bouncycastle/cmc/PKIResponseBuilder.java

Lines changed: 92 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import java.util.ArrayList;
55
import java.util.List;
66

7+
import org.bouncycastle.asn1.ASN1EncodableVector;
78
import org.bouncycastle.asn1.DEROctetString;
89
import org.bouncycastle.asn1.DERSet;
910
import org.bouncycastle.asn1.cmc.BodyPartID;
@@ -16,19 +17,30 @@
1617
import org.bouncycastle.asn1.cms.CMSObjectIdentifiers;
1718
import org.bouncycastle.asn1.cms.ContentInfo;
1819
import org.bouncycastle.asn1.cms.SignedData;
20+
import org.bouncycastle.cert.X509CertificateHolder;
1921

2022
/**
21-
* Builder for an unsigned Full PKI Response (RFC 5272 / RFC 7030 4.2.3 / 4.4.2):
22-
* a CMS SignedData with no SignerInfos and no certificates whose encapsulated
23-
* content is an id-cct-PKIResponse PKIResponse SEQUENCE. The product is
24-
* delivered as a {@link SimplePKIResponse}, whose structured accessors expose
25-
* the embedded PKIResponse content.
23+
* Builder for a Simple PKI Response (RFC 5272 / RFC 7030 4.2.3 / 4.4.2),
24+
* delivered as a {@link SimplePKIResponse}.
25+
* <p>
26+
* Two shapes are supported, selected automatically at {@link #build()} time:
27+
* <ul>
28+
* <li><b>Full PKI Response</b> (the error case used by EST server-generated
29+
* errors): a CMS SignedData with no SignerInfos whose encapsulated
30+
* content is an id-cct-PKIResponse PKIResponse SEQUENCE. Selected when
31+
* any control attribute, CMS content or other message has been added.</li>
32+
* <li><b>Simple PKI Response</b> (the cert-delivery case used by
33+
* /simpleenroll): a degenerate CMS SignedData with no SignerInfos, no
34+
* encapsulated content, and the issued certificates in the certificates
35+
* field. Selected when only certificates have been added.</li>
36+
* </ul>
2637
*/
2738
public class PKIResponseBuilder
2839
{
2940
private final List<TaggedAttribute> controlAttributes = new ArrayList<TaggedAttribute>();
3041
private final List<TaggedContentInfo> cmsContents = new ArrayList<TaggedContentInfo>();
3142
private final List<OtherMsg> otherMsgs = new ArrayList<OtherMsg>();
43+
private final List<X509CertificateHolder> certificates = new ArrayList<X509CertificateHolder>();
3244

3345
public PKIResponseBuilder addControlAttribute(TaggedAttribute attr)
3446
{
@@ -39,7 +51,11 @@ public PKIResponseBuilder addControlAttribute(TaggedAttribute attr)
3951
/**
4052
* Convenience for the EST server-generated error case: wrap the supplied
4153
* CMCStatusInfoV2 in a TaggedAttribute keyed by id-cmc-statusInfoV2 and
42-
* append it to the controlSequence.
54+
* append it to the controlSequence. The supplied {@code bodyPartID}
55+
* identifies the {@link TaggedAttribute} itself within the controlSequence
56+
* (per RFC 5272 sec. 3.2.1); it is structurally distinct from the
57+
* {@code bodyList} entries inside {@code CMCStatusInfoV2}, which identify
58+
* which request body parts the status pertains to.
4359
*/
4460
public PKIResponseBuilder addStatusInfoV2(BodyPartID bodyPartID, CMCStatusInfoV2 statusInfo)
4561
{
@@ -48,6 +64,26 @@ public PKIResponseBuilder addStatusInfoV2(BodyPartID bodyPartID, CMCStatusInfoV2
4864
return this;
4965
}
5066

67+
/**
68+
* Convenience overload for the simple-error case where the outer
69+
* {@link TaggedAttribute}'s bodyPartID can be inherited from the first
70+
* entry of {@code statusInfo.getBodyList()}. Behaves identically to
71+
* {@link #addStatusInfoV2(BodyPartID, CMCStatusInfoV2)} when the caller
72+
* doesn't need an independent identifier for the TaggedAttribute.
73+
*
74+
* @throws IllegalArgumentException if {@code statusInfo}'s bodyList is empty.
75+
*/
76+
public PKIResponseBuilder addStatusInfoV2(CMCStatusInfoV2 statusInfo)
77+
{
78+
BodyPartID[] bodyList = statusInfo.getBodyList();
79+
if (bodyList == null || bodyList.length == 0)
80+
{
81+
throw new IllegalArgumentException(
82+
"CMCStatusInfoV2 bodyList is empty - cannot derive outer bodyPartID");
83+
}
84+
return addStatusInfoV2(bodyList[0], statusInfo);
85+
}
86+
5187
public PKIResponseBuilder addCmsContent(TaggedContentInfo cmsContent)
5288
{
5389
cmsContents.add(cmsContent);
@@ -60,31 +96,68 @@ public PKIResponseBuilder addOtherMsg(OtherMsg otherMsg)
6096
return this;
6197
}
6298

99+
/**
100+
* Add a certificate to deliver in the response. When the builder contains
101+
* only certificates (no control attributes, no CMS contents, no other
102+
* messages), {@link #build()} emits a degenerate SignedData with no
103+
* encapsulated content and the certificates in the certificates field
104+
* (the Simple PKI Response shape used by EST /simpleenroll). When other
105+
* payload has also been added, the certificates are carried alongside the
106+
* id-cct-PKIResponse encapsulated content.
107+
*/
108+
public PKIResponseBuilder addCertificate(X509CertificateHolder cert)
109+
{
110+
certificates.add(cert);
111+
return this;
112+
}
113+
63114
public SimplePKIResponse build()
64115
throws CMCException
65116
{
66-
PKIResponse pkiResponse = new PKIResponse(
67-
controlAttributes.toArray(new TaggedAttribute[0]),
68-
cmsContents.toArray(new TaggedContentInfo[0]),
69-
otherMsgs.toArray(new OtherMsg[0]));
117+
boolean hasPayload = !controlAttributes.isEmpty()
118+
|| !cmsContents.isEmpty()
119+
|| !otherMsgs.isEmpty();
120+
121+
ASN1EncodableVector certVec = null;
122+
if (!certificates.isEmpty())
123+
{
124+
certVec = new ASN1EncodableVector();
125+
for (X509CertificateHolder ch : certificates)
126+
{
127+
certVec.add(ch.toASN1Structure());
128+
}
129+
}
70130

71131
ContentInfo encap;
72-
try
132+
if (hasPayload)
73133
{
74-
encap = new ContentInfo(CMCObjectIdentifiers.id_cct_PKIResponse,
75-
new DEROctetString(pkiResponse.getEncoded()));
134+
PKIResponse pkiResponse = new PKIResponse(
135+
controlAttributes.toArray(new TaggedAttribute[0]),
136+
cmsContents.toArray(new TaggedContentInfo[0]),
137+
otherMsgs.toArray(new OtherMsg[0]));
138+
139+
try
140+
{
141+
encap = new ContentInfo(CMCObjectIdentifiers.id_cct_PKIResponse,
142+
new DEROctetString(pkiResponse.getEncoded()));
143+
}
144+
catch (IOException e)
145+
{
146+
throw new CMCException("unable to encode PKIResponse: " + e.getMessage(), e);
147+
}
76148
}
77-
catch (IOException e)
149+
else
78150
{
79-
throw new CMCException("unable to encode PKIResponse: " + e.getMessage(), e);
151+
// Simple PKI Response: degenerate SignedData with no encap content.
152+
encap = new ContentInfo(CMSObjectIdentifiers.data, null);
80153
}
81154

82155
SignedData signedData = new SignedData(
83-
new DERSet(), // digestAlgorithms
156+
new DERSet(), // digestAlgorithms
84157
encap,
85-
null, // certificates
86-
null, // crls
87-
new DERSet()); // signerInfos
158+
certVec == null ? null : new DERSet(certVec), // certificates
159+
null, // crls
160+
new DERSet()); // signerInfos
88161

89162
return new SimplePKIResponse(new ContentInfo(CMSObjectIdentifiers.signedData, signedData));
90163
}

pkix/src/test/java/org/bouncycastle/est/test/ESTParsingTest.java

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,4 +257,66 @@ public void testParsingFullPKIErrorResponse()
257257

258258
assertEquals(0, response.getCertificates().getMatches(null).size());
259259
}
260+
261+
/**
262+
* The 1-arg addStatusInfoV2 overload derives the outer TaggedAttribute
263+
* bodyPartID from the first entry of statusInfo.getBodyList(). See the
264+
* follow-up comment on github #1452.
265+
*/
266+
public void testPKIResponseBuilderStatusInfoOnlyOverload()
267+
throws Exception
268+
{
269+
BodyPartID bodyPartID = new BodyPartID(42);
270+
CMCStatusInfoV2 statusInfo = new CMCStatusInfoV2Builder(CMCStatus.failed, bodyPartID)
271+
.setOtherInfo(CMCFailInfo.badIdentity)
272+
.setStatusString("bad identity")
273+
.build();
274+
275+
SimplePKIResponse built = new PKIResponseBuilder()
276+
.addStatusInfoV2(statusInfo)
277+
.build();
278+
279+
SimplePKIResponse response = new SimplePKIResponse(built.getEncoded());
280+
281+
TaggedAttribute[] attrs = response.getControlAttributes();
282+
assertEquals(1, attrs.length);
283+
assertEquals(CMCObjectIdentifiers.id_cmc_statusInfoV2, attrs[0].getAttrType());
284+
assertEquals(bodyPartID, attrs[0].getBodyPartID());
285+
286+
CMCStatusInfoV2 parsed = response.getStatusInfoV2();
287+
assertEquals(CMCStatus.failed, parsed.getCMCStatus());
288+
assertEquals(bodyPartID, parsed.getBodyList()[0]);
289+
}
290+
291+
/**
292+
* The cert-delivery shape: when only certificates are added, build()
293+
* emits a degenerate SignedData with no encapsulated content and the
294+
* certs in the certificates field (RFC 5272 Simple PKI Response, EST
295+
* /simpleenroll). See the follow-up comment on github #1452.
296+
*/
297+
public void testPKIResponseBuilderCertOnly()
298+
throws Exception
299+
{
300+
SimplePKIResponse caResponse = new SimplePKIResponse(cacertsResponse);
301+
Store<X509CertificateHolder> caCerts = caResponse.getCertificates();
302+
Collection<X509CertificateHolder> caCertList = caCerts.getMatches(null);
303+
assertTrue(caCertList.size() >= 1);
304+
X509CertificateHolder firstCa = caCertList.iterator().next();
305+
306+
SimplePKIResponse built = new PKIResponseBuilder()
307+
.addCertificate(firstCa)
308+
.build();
309+
310+
SimplePKIResponse response = new SimplePKIResponse(built.getEncoded());
311+
312+
// No PKIResponse encap content in this shape.
313+
assertNull("cert-only response must not carry id-cct-PKIResponse content",
314+
response.getPKIResponse());
315+
assertEquals(0, response.getControlAttributes().length);
316+
317+
// The cert went into the cert set.
318+
Collection<X509CertificateHolder> roundTrip = response.getCertificates().getMatches(null);
319+
assertEquals(1, roundTrip.size());
320+
assertEquals(firstCa, roundTrip.iterator().next());
321+
}
260322
}

0 commit comments

Comments
 (0)