1919import static com .google .common .base .Preconditions .checkArgument ;
2020import static com .google .common .base .Preconditions .checkNotNull ;
2121
22+ import com .google .common .base .Optional ;
2223import 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 ;
2343import java .util .Locale ;
44+ import java .util .Map ;
2445
2546/**
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.
2749 * @see <a href="https://github.com/spiffe/spiffe/blob/master/standards/SPIFFE-ID.md">Standard</a>
2850 */
2951public final class SpiffeUtil {
3052
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-----" ;
3158 private static final String PREFIX = "spiffe://" ;
3259
3360 private SpiffeUtil () {}
@@ -96,6 +123,137 @@ private static void validatePathSegment(String pathSegment) {
96123 + " ([a-zA-Z0-9.-_])" );
97124 }
98125
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+
99257 /**
100258 * Represents a SPIFFE ID as defined in the SPIFFE standard.
101259 * @see <a href="https://github.com/spiffe/spiffe/blob/master/standards/SPIFFE-ID.md">Standard</a>
@@ -119,4 +277,34 @@ public String getPath() {
119277 }
120278 }
121279
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+
122310}
0 commit comments