Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions core/src/main/java/io/grpc/internal/JsonParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package io.grpc.internal;

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

import com.google.gson.stream.JsonReader;
Expand All @@ -41,7 +42,8 @@ private JsonParser() {}

/**
* Parses a json string, returning either a {@code Map<String, ?>}, {@code List<?>},
* {@code String}, {@code Double}, {@code Boolean}, or {@code null}.
* {@code String}, {@code Double}, {@code Boolean}, or {@code null}. Fails if duplicate names
* found.
*/
public static Object parse(String raw) throws IOException {
JsonReader jr = new JsonReader(new StringReader(raw));
Expand Down Expand Up @@ -81,6 +83,7 @@ private static Object parseRecursive(JsonReader jr) throws IOException {
Map<String, Object> obj = new LinkedHashMap<>();
while (jr.hasNext()) {
String name = jr.nextName();
checkArgument(!obj.containsKey(name), "Duplicate key found: %s", name);
Object value = parseRecursive(jr);
obj.put(name, value);
}
Expand All @@ -105,4 +108,4 @@ private static Void parseJsonNull(JsonReader jr) throws IOException {
jr.nextNull();
return null;
}
}
}
190 changes: 189 additions & 1 deletion core/src/main/java/io/grpc/internal/SpiffeUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,42 @@
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;

import com.google.common.base.Optional;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

/**
* Helper utility to work with SPIFFE URIs.
* Provides utilities to manage SPIFFE bundles, extract SPIFFE IDs from X.509 certificate chains,
* and parse SPIFFE IDs.
* @see <a href="https://github.com/spiffe/spiffe/blob/master/standards/SPIFFE-ID.md">Standard</a>
*/
public final class SpiffeUtil {

private static final Integer URI_SAN_TYPE = 6;
private static final String USE_PARAMETER_VALUE = "x509-svid";
private static final String KTY_PARAMETER_VALUE = "RSA";
private static final String CERTIFICATE_PREFIX = "-----BEGIN CERTIFICATE-----\n";
private static final String CERTIFICATE_SUFFIX = "-----END CERTIFICATE-----";
private static final String PREFIX = "spiffe://";

private SpiffeUtil() {}
Expand Down Expand Up @@ -96,6 +123,137 @@
+ " ([a-zA-Z0-9.-_])");
}

/**
* Returns the SPIFFE ID from the leaf certificate, if present.
*
* @param certChain certificate chain to extract SPIFFE ID from
*/
public static Optional<SpiffeId> extractSpiffeId(X509Certificate[] certChain)
throws CertificateParsingException {
checkArgument(checkNotNull(certChain, "certChain").length > 0, "certChain can't be empty");
Collection<List<?>> subjectAltNames = certChain[0].getSubjectAlternativeNames();
if (subjectAltNames == null) {
return Optional.absent();
}
String uri = null;
// Search for the unique URI SAN.
for (List<?> altName : subjectAltNames) {
if (altName.size() < 2 ) {
continue;

Check warning on line 142 in core/src/main/java/io/grpc/internal/SpiffeUtil.java

View check run for this annotation

Codecov / codecov/patch

core/src/main/java/io/grpc/internal/SpiffeUtil.java#L142

Added line #L142 was not covered by tests
}
if (URI_SAN_TYPE.equals(altName.get(0))) {
if (uri != null) {
throw new IllegalArgumentException("Multiple URI SAN values found in the leaf cert.");
}
uri = (String) altName.get(1);
}
}
if (uri == null) {
return Optional.absent();

Check warning on line 152 in core/src/main/java/io/grpc/internal/SpiffeUtil.java

View check run for this annotation

Codecov / codecov/patch

core/src/main/java/io/grpc/internal/SpiffeUtil.java#L152

Added line #L152 was not covered by tests
}
return Optional.of(parse(uri));
}

/**
* Loads a SPIFFE trust bundle from a file, parsing it from the JSON format.
* In case of success, returns {@link SpiffeBundle}.
* If any element of the JSON content is invalid or unsupported, an
* {@link IllegalArgumentException} is thrown and the entire Bundle is considered invalid.
*
* @param trustBundleFile the file path to the JSON file containing the trust bundle
* @see <a href="https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Trust_Domain_and_Bundle.md">JSON format</a>
* @see <a href="https://github.com/spiffe/spiffe/blob/main/standards/X509-SVID.md#61-publishing-spiffe-bundle-elements">JWK entry format</a>
* @see <a href="https://datatracker.ietf.org/doc/html/rfc7517#appendix-B">x5c (certificate) parameter</a>
*/
public static SpiffeBundle loadTrustBundleFromFile(String trustBundleFile) throws IOException {
Map<String, ?> trustDomainsNode = readTrustDomainsFromFile(trustBundleFile);
Map<String, List<X509Certificate>> trustBundleMap = new HashMap<>();
Map<String, Long> sequenceNumbers = new HashMap<>();
for (String trustDomainName : trustDomainsNode.keySet()) {
Map<String, ?> domainNode = JsonUtil.getObject(trustDomainsNode, trustDomainName);
if (domainNode.size() == 0) {
trustBundleMap.put(trustDomainName, Collections.emptyList());
continue;
}
Long sequenceNumber = JsonUtil.getNumberAsLong(domainNode, "spiffe_sequence");
sequenceNumbers.put(trustDomainName, sequenceNumber == null ? -1L : sequenceNumber);
List<Map<String, ?>> keysNode = JsonUtil.getListOfObjects(domainNode, "keys");
if (keysNode == null || keysNode.size() == 0) {
trustBundleMap.put(trustDomainName, Collections.emptyList());
continue;

Check warning on line 183 in core/src/main/java/io/grpc/internal/SpiffeUtil.java

View check run for this annotation

Codecov / codecov/patch

core/src/main/java/io/grpc/internal/SpiffeUtil.java#L182-L183

Added lines #L182 - L183 were not covered by tests
}
trustBundleMap.put(trustDomainName, extractCert(keysNode, trustDomainName));
}
return new SpiffeBundle(sequenceNumbers, trustBundleMap);
}

private static Map<String, ?> readTrustDomainsFromFile(String filePath) throws IOException {
Path path = Paths.get(checkNotNull(filePath, "trustBundleFile"));
String json = new String(Files.readAllBytes(path), StandardCharsets.UTF_8);
Object jsonObject = JsonParser.parse(json);
if (!(jsonObject instanceof Map)) {
throw new IllegalArgumentException(
"SPIFFE Trust Bundle should be a JSON object. Found: "
+ (jsonObject == null ? null : jsonObject.getClass()));
}
@SuppressWarnings("unchecked")
Map<String, ?> root = (Map<String, ?>)jsonObject;
Map<String, ?> trustDomainsNode = JsonUtil.getObject(root, "trust_domains");
checkNotNull(trustDomainsNode, "Mandatory trust_domains element is missing");
checkArgument(trustDomainsNode.size() > 0, "Mandatory trust_domains element is missing");
return trustDomainsNode;
}

private static void checkJwkEntry(Map<String, ?> jwkNode, String trustDomainName) {
String kty = JsonUtil.getString(jwkNode, "kty");
if (kty == null || !kty.equals(KTY_PARAMETER_VALUE)) {
throw new IllegalArgumentException(String.format("'kty' parameter must be '%s' but '%s' "
+ "found. Certificate loading for trust domain '%s' failed.", KTY_PARAMETER_VALUE,
kty, trustDomainName));
}
if (jwkNode.containsKey("kid")) {
throw new IllegalArgumentException(String.format("'kid' parameter must not be set. "
+ "Certificate loading for trust domain '%s' failed.", trustDomainName));
}
String use = JsonUtil.getString(jwkNode, "use");
if (use == null || !use.equals(USE_PARAMETER_VALUE)) {
throw new IllegalArgumentException(String.format("'use' parameter must be '%s' but '%s' "
+ "found. Certificate loading for trust domain '%s' failed.", USE_PARAMETER_VALUE,
use, trustDomainName));
}
}

private static List<X509Certificate> extractCert(List<Map<String, ?>> keysNode,
String trustDomainName) {
List<X509Certificate> result = new ArrayList<>();
for (Map<String, ?> keyNode : keysNode) {
checkJwkEntry(keyNode, trustDomainName);
List<String> rawCerts = JsonUtil.getListOfStrings(keyNode, "x5c");
if (rawCerts == null) {
break;

Check warning on line 233 in core/src/main/java/io/grpc/internal/SpiffeUtil.java

View check run for this annotation

Codecov / codecov/patch

core/src/main/java/io/grpc/internal/SpiffeUtil.java#L233

Added line #L233 was not covered by tests
}
if (rawCerts.size() != 1) {
throw new IllegalArgumentException(String.format("Exactly 1 certificate is expected, but "
+ "%s found. Certificate loading for trust domain '%s' failed.", rawCerts.size(),
trustDomainName));
}
InputStream stream = new ByteArrayInputStream((CERTIFICATE_PREFIX + rawCerts.get(0) + "\n"
+ CERTIFICATE_SUFFIX)
.getBytes(StandardCharsets.UTF_8));
try {
Collection<? extends Certificate> certs = CertificateFactory.getInstance("X509")
.generateCertificates(stream);
X509Certificate[] certsArray = certs.toArray(new X509Certificate[0]);
assert certsArray.length == 1;
result.add(certsArray[0]);
} catch (CertificateException e) {
throw new IllegalArgumentException(String.format("Certificate can't be parsed. Certificate "
+ "loading for trust domain '%s' failed.", trustDomainName), e);
}
}
return result;
}

/**
* Represents a SPIFFE ID as defined in the SPIFFE standard.
* @see <a href="https://github.com/spiffe/spiffe/blob/master/standards/SPIFFE-ID.md">Standard</a>
Expand All @@ -119,4 +277,34 @@
}
}

/**
* Represents a SPIFFE trust bundle; that is, a map from trust domain to set of trusted
* certificates. Only trust domain's sequence numbers and x509 certificates are supported.
* @see <a href="https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Trust_Domain_and_Bundle.md#4-spiffe-bundle-format">Standard</a>
*/
public static final class SpiffeBundle {

private final ImmutableMap<String, Long> sequenceNumbers;

private final ImmutableMap<String, ImmutableList<X509Certificate>> bundleMap;

private SpiffeBundle(Map<String, Long> sequenceNumbers,
Map<String, List<X509Certificate>> trustDomainMap) {
this.sequenceNumbers = ImmutableMap.copyOf(sequenceNumbers);
ImmutableMap.Builder<String, ImmutableList<X509Certificate>> builder = ImmutableMap.builder();
for (Map.Entry<String, List<X509Certificate>> entry : trustDomainMap.entrySet()) {
builder.put(entry.getKey(), ImmutableList.copyOf(entry.getValue()));
}
this.bundleMap = builder.build();
}

public ImmutableMap<String, Long> getSequenceNumbers() {
return sequenceNumbers;
}

public ImmutableMap<String, ImmutableList<X509Certificate>> getBundleMap() {
return bundleMap;
}
}

}
9 changes: 8 additions & 1 deletion core/src/test/java/io/grpc/internal/JsonParserTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -123,4 +123,11 @@ public void objectStringName() throws IOException {

assertEquals(expected, JsonParser.parse("{\"hi\": 2}"));
}
}

@Test
public void duplicate() throws IOException {
thrown.expect(IllegalArgumentException.class);

JsonParser.parse("{\"hi\": 2, \"hi\": 3}");
}
}
Loading