Skip to content

Commit 22e9e8d

Browse files
committed
Added additional methods to SimplePKIResponse and builder class, relates to github #1452
1 parent 527488a commit 22e9e8d

5 files changed

Lines changed: 245 additions & 2 deletions

File tree

docs/releasenotes.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ <h3>2.2.3 Additional Features and Functionality</h3>
4040
<li>X509v3CertificateBuilder now exposes setters for the constructor arguments (setIssuer, setSerialNumber, setNotBefore, setNotAfter, setSubject, setSubjectPublicKeyInfo) to support equivalence-comparison use cases (issue #1545).</li>
4141
<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>
4242
<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>
43+
<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>
4344
</ul>
4445

4546
<a id="r1rv84"><h3>2.2.1 Version</h3></a>
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package org.bouncycastle.cmc;
2+
3+
import java.io.IOException;
4+
import java.util.ArrayList;
5+
import java.util.List;
6+
7+
import org.bouncycastle.asn1.DEROctetString;
8+
import org.bouncycastle.asn1.DERSet;
9+
import org.bouncycastle.asn1.cmc.BodyPartID;
10+
import org.bouncycastle.asn1.cmc.CMCObjectIdentifiers;
11+
import org.bouncycastle.asn1.cmc.CMCStatusInfoV2;
12+
import org.bouncycastle.asn1.cmc.OtherMsg;
13+
import org.bouncycastle.asn1.cmc.PKIResponse;
14+
import org.bouncycastle.asn1.cmc.TaggedAttribute;
15+
import org.bouncycastle.asn1.cmc.TaggedContentInfo;
16+
import org.bouncycastle.asn1.cms.CMSObjectIdentifiers;
17+
import org.bouncycastle.asn1.cms.ContentInfo;
18+
import org.bouncycastle.asn1.cms.SignedData;
19+
20+
/**
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.
26+
*/
27+
public class PKIResponseBuilder
28+
{
29+
private final List<TaggedAttribute> controlAttributes = new ArrayList<TaggedAttribute>();
30+
private final List<TaggedContentInfo> cmsContents = new ArrayList<TaggedContentInfo>();
31+
private final List<OtherMsg> otherMsgs = new ArrayList<OtherMsg>();
32+
33+
public PKIResponseBuilder addControlAttribute(TaggedAttribute attr)
34+
{
35+
controlAttributes.add(attr);
36+
return this;
37+
}
38+
39+
/**
40+
* Convenience for the EST server-generated error case: wrap the supplied
41+
* CMCStatusInfoV2 in a TaggedAttribute keyed by id-cmc-statusInfoV2 and
42+
* append it to the controlSequence.
43+
*/
44+
public PKIResponseBuilder addStatusInfoV2(BodyPartID bodyPartID, CMCStatusInfoV2 statusInfo)
45+
{
46+
controlAttributes.add(new TaggedAttribute(
47+
bodyPartID, CMCObjectIdentifiers.id_cmc_statusInfoV2, new DERSet(statusInfo)));
48+
return this;
49+
}
50+
51+
public PKIResponseBuilder addCmsContent(TaggedContentInfo cmsContent)
52+
{
53+
cmsContents.add(cmsContent);
54+
return this;
55+
}
56+
57+
public PKIResponseBuilder addOtherMsg(OtherMsg otherMsg)
58+
{
59+
otherMsgs.add(otherMsg);
60+
return this;
61+
}
62+
63+
public SimplePKIResponse build()
64+
throws CMCException
65+
{
66+
PKIResponse pkiResponse = new PKIResponse(
67+
controlAttributes.toArray(new TaggedAttribute[0]),
68+
cmsContents.toArray(new TaggedContentInfo[0]),
69+
otherMsgs.toArray(new OtherMsg[0]));
70+
71+
ContentInfo encap;
72+
try
73+
{
74+
encap = new ContentInfo(CMCObjectIdentifiers.id_cct_PKIResponse,
75+
new DEROctetString(pkiResponse.getEncoded()));
76+
}
77+
catch (IOException e)
78+
{
79+
throw new CMCException("unable to encode PKIResponse: " + e.getMessage(), e);
80+
}
81+
82+
SignedData signedData = new SignedData(
83+
new DERSet(), // digestAlgorithms
84+
encap,
85+
null, // certificates
86+
null, // crls
87+
new DERSet()); // signerInfos
88+
89+
return new SimplePKIResponse(new ContentInfo(CMSObjectIdentifiers.signedData, signedData));
90+
}
91+
}

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

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
import java.io.IOException;
44

55
import org.bouncycastle.asn1.ASN1Primitive;
6+
import org.bouncycastle.asn1.cmc.CMCObjectIdentifiers;
7+
import org.bouncycastle.asn1.cmc.CMCStatusInfoV2;
8+
import org.bouncycastle.asn1.cmc.PKIResponse;
9+
import org.bouncycastle.asn1.cmc.TaggedAttribute;
10+
import org.bouncycastle.asn1.cmc.TaggedContentInfo;
611
import org.bouncycastle.asn1.cms.ContentInfo;
712
import org.bouncycastle.cert.X509CRLHolder;
813
import org.bouncycastle.cert.X509CertificateHolder;
@@ -15,13 +20,18 @@
1520
* Carrier for a Simple PKI Response.
1621
* <p>
1722
* A Simple PKI Response is defined in RFC 5272 as a CMS SignedData object with no EncapsulatedContentInfo
18-
* and no SignerInfos attached.
23+
* and no SignerInfos attached. As a convenience this class also recognises the unsigned Full PKI Response
24+
* variant used for EST server-generated errors (RFC 7030 4.2.3 / 4.4.2): a CMS SignedData with no
25+
* SignerInfos whose encapsulated content is an id-cct-PKIResponse PKIResponse SEQUENCE. The structured
26+
* accessors {@link #getPKIResponse()}, {@link #getControlAttributes()}, {@link #getCmsContents()} and
27+
* {@link #getStatusInfoV2()} return the embedded PKIResponse content when present.
1928
* </p>
2029
*/
2130
public class SimplePKIResponse
2231
implements Encodable
2332
{
2433
private final CMSSignedData certificateResponse;
34+
private final PKIResponse pkiResponse;
2535

2636
private static ContentInfo parseBytes(byte[] responseEncoding)
2737
throws CMCException
@@ -69,7 +79,24 @@ public SimplePKIResponse(ContentInfo signedData)
6979
{
7080
throw new CMCException("malformed response: SignerInfo structures found");
7181
}
72-
if (certificateResponse.getSignedContent() != null)
82+
83+
if (certificateResponse.getSignedContent() == null)
84+
{
85+
this.pkiResponse = null;
86+
}
87+
else if (CMCObjectIdentifiers.id_cct_PKIResponse.equals(certificateResponse.getSignedContentType()))
88+
{
89+
try
90+
{
91+
this.pkiResponse = PKIResponse.getInstance(
92+
ASN1Primitive.fromByteArray((byte[])certificateResponse.getSignedContent().getContent()));
93+
}
94+
catch (Exception e)
95+
{
96+
throw new CMCException("malformed response: " + e.getMessage(), e);
97+
}
98+
}
99+
else
73100
{
74101
throw new CMCException("malformed response: Signed Content found");
75102
}
@@ -95,6 +122,76 @@ public Store<X509CRLHolder> getCRLs()
95122
return certificateResponse.getCRLs();
96123
}
97124

125+
/**
126+
* Return the embedded PKIResponse content, if present.
127+
*
128+
* @return the parsed PKIResponse, or null if the SignedData has no encapsulated PKIResponse.
129+
*/
130+
public PKIResponse getPKIResponse()
131+
{
132+
return pkiResponse;
133+
}
134+
135+
/**
136+
* Return the controlSequence of the embedded PKIResponse as an array of TaggedAttribute, or
137+
* an empty array if no PKIResponse is present.
138+
*/
139+
public TaggedAttribute[] getControlAttributes()
140+
{
141+
if (pkiResponse == null)
142+
{
143+
return new TaggedAttribute[0];
144+
}
145+
146+
int size = pkiResponse.getControlSequence().size();
147+
TaggedAttribute[] attrs = new TaggedAttribute[size];
148+
for (int i = 0; i != size; i++)
149+
{
150+
attrs[i] = TaggedAttribute.getInstance(pkiResponse.getControlSequence().getObjectAt(i));
151+
}
152+
return attrs;
153+
}
154+
155+
/**
156+
* Return the cmsSequence of the embedded PKIResponse as an array of TaggedContentInfo, or
157+
* an empty array if no PKIResponse is present.
158+
*/
159+
public TaggedContentInfo[] getCmsContents()
160+
{
161+
if (pkiResponse == null)
162+
{
163+
return new TaggedContentInfo[0];
164+
}
165+
166+
int size = pkiResponse.getCmsSequence().size();
167+
TaggedContentInfo[] arr = new TaggedContentInfo[size];
168+
for (int i = 0; i != size; i++)
169+
{
170+
arr[i] = TaggedContentInfo.getInstance(pkiResponse.getCmsSequence().getObjectAt(i));
171+
}
172+
return arr;
173+
}
174+
175+
/**
176+
* Convenience accessor for the first id-cmc-statusInfoV2 attribute in the PKIResponse
177+
* controlSequence (typical of an EST server-generated error response).
178+
*
179+
* @return the CMCStatusInfoV2 if present, otherwise null.
180+
*/
181+
public CMCStatusInfoV2 getStatusInfoV2()
182+
{
183+
TaggedAttribute[] attrs = getControlAttributes();
184+
for (int i = 0; i != attrs.length; i++)
185+
{
186+
if (CMCObjectIdentifiers.id_cmc_statusInfoV2.equals(attrs[i].getAttrType())
187+
&& attrs[i].getAttrValues().size() != 0)
188+
{
189+
return CMCStatusInfoV2.getInstance(attrs[i].getAttrValues().getObjectAt(0));
190+
}
191+
}
192+
return null;
193+
}
194+
98195
/**
99196
* return the ASN.1 encoded representation of this object.
100197
*/

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,17 @@ public String getSignedContentTypeOID()
365365
return signedData.getEncapContentInfo().getContentType().getId();
366366
}
367367

368+
/**
369+
* Return the ASN1ObjectIdentifier associated with the encapsulated content info structure
370+
* carried in the signed data.
371+
*
372+
* @return the OID for the content type.
373+
*/
374+
public ASN1ObjectIdentifier getSignedContentType()
375+
{
376+
return signedData.getEncapContentInfo().getContentType();
377+
}
378+
368379
public CMSTypedData getSignedContent()
369380
{
370381
return signedContent;

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

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,20 @@
66
import junit.framework.TestCase;
77
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
88
import org.bouncycastle.asn1.ASN1String;
9+
import org.bouncycastle.asn1.cmc.BodyPartID;
10+
import org.bouncycastle.asn1.cmc.CMCFailInfo;
11+
import org.bouncycastle.asn1.cmc.CMCObjectIdentifiers;
12+
import org.bouncycastle.asn1.cmc.CMCStatus;
13+
import org.bouncycastle.asn1.cmc.CMCStatusInfoV2;
14+
import org.bouncycastle.asn1.cmc.CMCStatusInfoV2Builder;
15+
import org.bouncycastle.asn1.cmc.TaggedAttribute;
916
import org.bouncycastle.asn1.pkcs.Attribute;
1017
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
1118
import org.bouncycastle.asn1.x500.X500Name;
1219
import org.bouncycastle.asn1.x9.X9ObjectIdentifiers;
1320
import org.bouncycastle.cert.X509CertificateHolder;
1421
import org.bouncycastle.cert.selector.X509CertificateHolderSelector;
22+
import org.bouncycastle.cmc.PKIResponseBuilder;
1523
import org.bouncycastle.cmc.SimplePKIResponse;
1624
import org.bouncycastle.est.CSRAttributesResponse;
1725
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
@@ -214,4 +222,39 @@ public void testParsingSimpleEnrolResponse()
214222

215223
assertEquals(1, certs.getMatches(new X509CertificateHolderSelector(new X500Name("CN=estExampleCA NwN"), new BigInteger("21"))).size());
216224
}
225+
226+
public void testParsingFullPKIErrorResponse()
227+
throws Exception
228+
{
229+
// Build the unsigned Full PKI Response (RFC 7030 server-generated error) containing
230+
// a CMCStatusInfoV2 attribute via PKIResponseBuilder, then round-trip it through
231+
// SimplePKIResponse(byte[]) to check the structured accessors expose the same
232+
// content. See github #1452.
233+
BodyPartID bodyPartID = new BodyPartID(1);
234+
CMCStatusInfoV2 statusInfo = new CMCStatusInfoV2Builder(CMCStatus.failed, bodyPartID)
235+
.setOtherInfo(CMCFailInfo.badIdentity)
236+
.setStatusString("bad identity")
237+
.build();
238+
239+
SimplePKIResponse built = new PKIResponseBuilder()
240+
.addStatusInfoV2(bodyPartID, statusInfo)
241+
.build();
242+
243+
SimplePKIResponse response = new SimplePKIResponse(built.getEncoded());
244+
245+
assertNotNull("PKIResponse should be exposed", response.getPKIResponse());
246+
assertEquals(1, response.getControlAttributes().length);
247+
assertEquals(0, response.getCmsContents().length);
248+
249+
TaggedAttribute attr = response.getControlAttributes()[0];
250+
assertEquals(CMCObjectIdentifiers.id_cmc_statusInfoV2, attr.getAttrType());
251+
252+
CMCStatusInfoV2 parsed = response.getStatusInfoV2();
253+
assertNotNull("statusInfoV2 should be exposed", parsed);
254+
assertEquals(CMCStatus.failed, parsed.getCMCStatus());
255+
assertEquals("bad identity", parsed.getStatusStringUTF8().getString());
256+
assertTrue(parsed.hasOtherInfo());
257+
258+
assertEquals(0, response.getCertificates().getMatches(null).size());
259+
}
217260
}

0 commit comments

Comments
 (0)