core: SpiffeUtil API for extracting Spiffe URI and loading TrustBundl… · grpc/grpc-java@4be69e3

@@ -19,15 +19,42 @@

1919

import static com.google.common.base.Preconditions.checkArgument;

2020

import static com.google.common.base.Preconditions.checkNotNull;

212122+

import com.google.common.base.Optional;

2223

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;

2343

import java.util.Locale;

44+

import java.util.Map;

24452546

/**

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

*/

2951

public final class SpiffeUtil {

305253+

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://";

32593360

private SpiffeUtil() {}

@@ -96,6 +123,137 @@ private static void validatePathSegment(String pathSegment) {

96123

+ " ([a-zA-Z0-9.-_])");

97124

}

98125126+

/**

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

}

121279280+

/**

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

}