io.grpc.internal.SpiffeUtil Maven / Gradle / Ivy
The newest version!
/*
* Copyright 2024 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.grpc.internal;
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;
/**
* Provides utilities to manage SPIFFE bundles, extract SPIFFE IDs from X.509 certificate chains,
* and parse SPIFFE IDs.
* @see Standard
*/
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() {}
/**
* Parses a URI string, applies validation rules described in SPIFFE standard, and, in case of
* success, returns parsed TrustDomain and Path.
*
* @param uri a String representing a SPIFFE ID
*/
public static SpiffeId parse(String uri) {
doInitialUriValidation(uri);
checkArgument(uri.toLowerCase(Locale.US).startsWith(PREFIX), "Spiffe Id must start with "
+ PREFIX);
String domainAndPath = uri.substring(PREFIX.length());
String trustDomain;
String path;
if (!domainAndPath.contains("/")) {
trustDomain = domainAndPath;
path = "";
} else {
String[] parts = domainAndPath.split("/", 2);
trustDomain = parts[0];
path = parts[1];
checkArgument(!path.isEmpty(), "Path must not include a trailing '/'");
}
validateTrustDomain(trustDomain);
validatePath(path);
if (!path.isEmpty()) {
path = "/" + path;
}
return new SpiffeId(trustDomain, path);
}
private static void doInitialUriValidation(String uri) {
checkArgument(checkNotNull(uri, "uri").length() > 0, "Spiffe Id can't be empty");
checkArgument(uri.length() <= 2048, "Spiffe Id maximum length is 2048 characters");
checkArgument(!uri.contains("#"), "Spiffe Id must not contain query fragments");
checkArgument(!uri.contains("?"), "Spiffe Id must not contain query parameters");
}
private static void validateTrustDomain(String trustDomain) {
checkArgument(!trustDomain.isEmpty(), "Trust Domain can't be empty");
checkArgument(trustDomain.length() < 256, "Trust Domain maximum length is 255 characters");
checkArgument(trustDomain.matches("[a-z0-9._-]+"),
"Trust Domain must contain only letters, numbers, dots, dashes, and underscores"
+ " ([a-z0-9.-_])");
}
private static void validatePath(String path) {
if (path.isEmpty()) {
return;
}
checkArgument(!path.endsWith("/"), "Path must not include a trailing '/'");
for (String segment : Splitter.on("/").split(path)) {
validatePathSegment(segment);
}
}
private static void validatePathSegment(String pathSegment) {
checkArgument(!pathSegment.isEmpty(), "Individual path segments must not be empty");
checkArgument(!(pathSegment.equals(".") || pathSegment.equals("..")),
"Individual path segments must not be relative path modifiers (i.e. ., ..)");
checkArgument(pathSegment.matches("[a-zA-Z0-9._-]+"),
"Individual path segments must contain only letters, numbers, dots, dashes, and underscores"
+ " ([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 extractSpiffeId(X509Certificate[] certChain)
throws CertificateParsingException {
checkArgument(checkNotNull(certChain, "certChain").length > 0, "certChain can't be empty");
Collection> 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;
}
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();
}
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 JSON format
* @see JWK entry format
* @see x5c (certificate) parameter
*/
public static SpiffeBundle loadTrustBundleFromFile(String trustBundleFile) throws IOException {
Map trustDomainsNode = readTrustDomainsFromFile(trustBundleFile);
Map> trustBundleMap = new HashMap<>();
Map sequenceNumbers = new HashMap<>();
for (String trustDomainName : trustDomainsNode.keySet()) {
Map 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
© 2015 - 2024 Weber Informatics LLC | Privacy Policy