19
19
import static com .google .common .base .Preconditions .checkArgument ;
20
20
import static com .google .common .base .Preconditions .checkNotNull ;
21
21
22
+ import com .google .common .base .Optional ;
22
23
import com .google .common .base .Splitter ;
24
+ import com .google .common .collect .ImmutableList ;
25
+ import com .google .common .collect .ImmutableMap ;
26
+ import java .io .ByteArrayInputStream ;
27
+ import java .io .IOException ;
28
+ import java .io .InputStream ;
29
+ import java .nio .charset .StandardCharsets ;
30
+ import java .nio .file .Files ;
31
+ import java .nio .file .Path ;
32
+ import java .nio .file .Paths ;
33
+ import java .security .cert .Certificate ;
34
+ import java .security .cert .CertificateException ;
35
+ import java .security .cert .CertificateFactory ;
36
+ import java .security .cert .CertificateParsingException ;
37
+ import java .security .cert .X509Certificate ;
38
+ import java .util .ArrayList ;
39
+ import java .util .Collection ;
40
+ import java .util .Collections ;
41
+ import java .util .HashMap ;
42
+ import java .util .List ;
23
43
import java .util .Locale ;
44
+ import java .util .Map ;
24
45
25
46
/**
26
- * Helper utility to work with SPIFFE URIs.
47
+ * Provides utilities to manage SPIFFE bundles, extract SPIFFE IDs from X.509 certificate chains,
48
+ * and parse SPIFFE IDs.
27
49
* @see <a href="https://github.com/spiffe/spiffe/blob/master/standards/SPIFFE-ID.md">Standard</a>
28
50
*/
29
51
public final class SpiffeUtil {
30
52
53
+ private static final Integer URI_SAN_TYPE = 6 ;
54
+ private static final String USE_PARAMETER_VALUE = "x509-svid" ;
55
+ private static final String KTY_PARAMETER_VALUE = "RSA" ;
56
+ private static final String CERTIFICATE_PREFIX = "-----BEGIN CERTIFICATE-----\n " ;
57
+ private static final String CERTIFICATE_SUFFIX = "-----END CERTIFICATE-----" ;
31
58
private static final String PREFIX = "spiffe://" ;
32
59
33
60
private SpiffeUtil () {}
@@ -96,6 +123,137 @@ private static void validatePathSegment(String pathSegment) {
96
123
+ " ([a-zA-Z0-9.-_])" );
97
124
}
98
125
126
+ /**
127
+ * Returns the SPIFFE ID from the leaf certificate, if present.
128
+ *
129
+ * @param certChain certificate chain to extract SPIFFE ID from
130
+ */
131
+ public static Optional <SpiffeId > extractSpiffeId (X509Certificate [] certChain )
132
+ throws CertificateParsingException {
133
+ checkArgument (checkNotNull (certChain , "certChain" ).length > 0 , "certChain can't be empty" );
134
+ Collection <List <?>> subjectAltNames = certChain [0 ].getSubjectAlternativeNames ();
135
+ if (subjectAltNames == null ) {
136
+ return Optional .absent ();
137
+ }
138
+ String uri = null ;
139
+ // Search for the unique URI SAN.
140
+ for (List <?> altName : subjectAltNames ) {
141
+ if (altName .size () < 2 ) {
142
+ continue ;
143
+ }
144
+ if (URI_SAN_TYPE .equals (altName .get (0 ))) {
145
+ if (uri != null ) {
146
+ throw new IllegalArgumentException ("Multiple URI SAN values found in the leaf cert." );
147
+ }
148
+ uri = (String ) altName .get (1 );
149
+ }
150
+ }
151
+ if (uri == null ) {
152
+ return Optional .absent ();
153
+ }
154
+ return Optional .of (parse (uri ));
155
+ }
156
+
157
+ /**
158
+ * Loads a SPIFFE trust bundle from a file, parsing it from the JSON format.
159
+ * In case of success, returns {@link SpiffeBundle}.
160
+ * If any element of the JSON content is invalid or unsupported, an
161
+ * {@link IllegalArgumentException} is thrown and the entire Bundle is considered invalid.
162
+ *
163
+ * @param trustBundleFile the file path to the JSON file containing the trust bundle
164
+ * @see <a href="https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Trust_Domain_and_Bundle.md">JSON format</a>
165
+ * @see <a href="https://github.com/spiffe/spiffe/blob/main/standards/X509-SVID.md#61-publishing-spiffe-bundle-elements">JWK entry format</a>
166
+ * @see <a href="https://datatracker.ietf.org/doc/html/rfc7517#appendix-B">x5c (certificate) parameter</a>
167
+ */
168
+ public static SpiffeBundle loadTrustBundleFromFile (String trustBundleFile ) throws IOException {
169
+ Map <String , ?> trustDomainsNode = readTrustDomainsFromFile (trustBundleFile );
170
+ Map <String , List <X509Certificate >> trustBundleMap = new HashMap <>();
171
+ Map <String , Long > sequenceNumbers = new HashMap <>();
172
+ for (String trustDomainName : trustDomainsNode .keySet ()) {
173
+ Map <String , ?> domainNode = JsonUtil .getObject (trustDomainsNode , trustDomainName );
174
+ if (domainNode .size () == 0 ) {
175
+ trustBundleMap .put (trustDomainName , Collections .emptyList ());
176
+ continue ;
177
+ }
178
+ Long sequenceNumber = JsonUtil .getNumberAsLong (domainNode , "spiffe_sequence" );
179
+ sequenceNumbers .put (trustDomainName , sequenceNumber == null ? -1L : sequenceNumber );
180
+ List <Map <String , ?>> keysNode = JsonUtil .getListOfObjects (domainNode , "keys" );
181
+ if (keysNode == null || keysNode .size () == 0 ) {
182
+ trustBundleMap .put (trustDomainName , Collections .emptyList ());
183
+ continue ;
184
+ }
185
+ trustBundleMap .put (trustDomainName , extractCert (keysNode , trustDomainName ));
186
+ }
187
+ return new SpiffeBundle (sequenceNumbers , trustBundleMap );
188
+ }
189
+
190
+ private static Map <String , ?> readTrustDomainsFromFile (String filePath ) throws IOException {
191
+ Path path = Paths .get (checkNotNull (filePath , "trustBundleFile" ));
192
+ String json = new String (Files .readAllBytes (path ), StandardCharsets .UTF_8 );
193
+ Object jsonObject = JsonParser .parse (json );
194
+ if (!(jsonObject instanceof Map )) {
195
+ throw new IllegalArgumentException (
196
+ "SPIFFE Trust Bundle should be a JSON object. Found: "
197
+ + (jsonObject == null ? null : jsonObject .getClass ()));
198
+ }
199
+ @ SuppressWarnings ("unchecked" )
200
+ Map <String , ?> root = (Map <String , ?>)jsonObject ;
201
+ Map <String , ?> trustDomainsNode = JsonUtil .getObject (root , "trust_domains" );
202
+ checkNotNull (trustDomainsNode , "Mandatory trust_domains element is missing" );
203
+ checkArgument (trustDomainsNode .size () > 0 , "Mandatory trust_domains element is missing" );
204
+ return trustDomainsNode ;
205
+ }
206
+
207
+ private static void checkJwkEntry (Map <String , ?> jwkNode , String trustDomainName ) {
208
+ String kty = JsonUtil .getString (jwkNode , "kty" );
209
+ if (kty == null || !kty .equals (KTY_PARAMETER_VALUE )) {
210
+ throw new IllegalArgumentException (String .format ("'kty' parameter must be '%s' but '%s' "
211
+ + "found. Certificate loading for trust domain '%s' failed." , KTY_PARAMETER_VALUE ,
212
+ kty , trustDomainName ));
213
+ }
214
+ if (jwkNode .containsKey ("kid" )) {
215
+ throw new IllegalArgumentException (String .format ("'kid' parameter must not be set. "
216
+ + "Certificate loading for trust domain '%s' failed." , trustDomainName ));
217
+ }
218
+ String use = JsonUtil .getString (jwkNode , "use" );
219
+ if (use == null || !use .equals (USE_PARAMETER_VALUE )) {
220
+ throw new IllegalArgumentException (String .format ("'use' parameter must be '%s' but '%s' "
221
+ + "found. Certificate loading for trust domain '%s' failed." , USE_PARAMETER_VALUE ,
222
+ use , trustDomainName ));
223
+ }
224
+ }
225
+
226
+ private static List <X509Certificate > extractCert (List <Map <String , ?>> keysNode ,
227
+ String trustDomainName ) {
228
+ List <X509Certificate > result = new ArrayList <>();
229
+ for (Map <String , ?> keyNode : keysNode ) {
230
+ checkJwkEntry (keyNode , trustDomainName );
231
+ List <String > rawCerts = JsonUtil .getListOfStrings (keyNode , "x5c" );
232
+ if (rawCerts == null ) {
233
+ break ;
234
+ }
235
+ if (rawCerts .size () != 1 ) {
236
+ throw new IllegalArgumentException (String .format ("Exactly 1 certificate is expected, but "
237
+ + "%s found. Certificate loading for trust domain '%s' failed." , rawCerts .size (),
238
+ trustDomainName ));
239
+ }
240
+ InputStream stream = new ByteArrayInputStream ((CERTIFICATE_PREFIX + rawCerts .get (0 ) + "\n "
241
+ + CERTIFICATE_SUFFIX )
242
+ .getBytes (StandardCharsets .UTF_8 ));
243
+ try {
244
+ Collection <? extends Certificate > certs = CertificateFactory .getInstance ("X509" )
245
+ .generateCertificates (stream );
246
+ X509Certificate [] certsArray = certs .toArray (new X509Certificate [0 ]);
247
+ assert certsArray .length == 1 ;
248
+ result .add (certsArray [0 ]);
249
+ } catch (CertificateException e ) {
250
+ throw new IllegalArgumentException (String .format ("Certificate can't be parsed. Certificate "
251
+ + "loading for trust domain '%s' failed." , trustDomainName ), e );
252
+ }
253
+ }
254
+ return result ;
255
+ }
256
+
99
257
/**
100
258
* Represents a SPIFFE ID as defined in the SPIFFE standard.
101
259
* @see <a href="https://github.com/spiffe/spiffe/blob/master/standards/SPIFFE-ID.md">Standard</a>
@@ -119,4 +277,34 @@ public String getPath() {
119
277
}
120
278
}
121
279
280
+ /**
281
+ * Represents a SPIFFE trust bundle; that is, a map from trust domain to set of trusted
282
+ * certificates. Only trust domain's sequence numbers and x509 certificates are supported.
283
+ * @see <a href="https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Trust_Domain_and_Bundle.md#4-spiffe-bundle-format">Standard</a>
284
+ */
285
+ public static final class SpiffeBundle {
286
+
287
+ private final ImmutableMap <String , Long > sequenceNumbers ;
288
+
289
+ private final ImmutableMap <String , ImmutableList <X509Certificate >> bundleMap ;
290
+
291
+ private SpiffeBundle (Map <String , Long > sequenceNumbers ,
292
+ Map <String , List <X509Certificate >> trustDomainMap ) {
293
+ this .sequenceNumbers = ImmutableMap .copyOf (sequenceNumbers );
294
+ ImmutableMap .Builder <String , ImmutableList <X509Certificate >> builder = ImmutableMap .builder ();
295
+ for (Map .Entry <String , List <X509Certificate >> entry : trustDomainMap .entrySet ()) {
296
+ builder .put (entry .getKey (), ImmutableList .copyOf (entry .getValue ()));
297
+ }
298
+ this .bundleMap = builder .build ();
299
+ }
300
+
301
+ public ImmutableMap <String , Long > getSequenceNumbers () {
302
+ return sequenceNumbers ;
303
+ }
304
+
305
+ public ImmutableMap <String , ImmutableList <X509Certificate >> getBundleMap () {
306
+ return bundleMap ;
307
+ }
308
+ }
309
+
122
310
}
0 commit comments