-
Notifications
You must be signed in to change notification settings - Fork 3.9k
core: SpiffeUtil API for extracting Spiffe URI and loading TrustBundles #11575
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
59b0409
3769f52
ae6940f
088eb08
821810f
3a32040
a12f262
0fec530
435e95a
67eeccc
62bef40
03b9b49
3487ef1
726ceaf
cbd6bc6
edc5182
ceb0340
47d9f83
8dc45ef
e1a3b43
821bed1
b8af2bd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
…traction of SPIFFE uri from leaf certs
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,15 +19,43 @@ | |
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; | ||
import java.util.logging.Level; | ||
import java.util.logging.Logger; | ||
|
||
/** | ||
* Helper utility to work with SPIFFE URIs. | ||
* Provides utilities to load, extract, and parse SPIFFE ID according the SPIFFE ID standard. | ||
matthewstevenson88 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* @see <a href="https://github.com/spiffe/spiffe/blob/master/standards/SPIFFE-ID.md">Standard</a> | ||
*/ | ||
public final class SpiffeUtil { | ||
|
||
private static final Logger log = Logger.getLogger(SpiffeUtil.class.getName()); | ||
|
||
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 PREFIX = "spiffe://"; | ||
|
||
private SpiffeUtil() {} | ||
|
@@ -96,6 +124,139 @@ private static void validatePathSegment(String pathSegment) { | |
+ " ([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) | ||
matthewstevenson88 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
throws CertificateParsingException { | ||
checkArgument(checkNotNull(certChain, "certChain").length > 0, "CertChain can't be empty"); | ||
matthewstevenson88 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Collection<List<?>> subjectAltNames = certChain[0].getSubjectAlternativeNames(); | ||
if (subjectAltNames == null) { | ||
return Optional.absent(); | ||
} | ||
String uri = null; | ||
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 trust domains, their associated sequence numbers, and X.509 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Two minor presentation comments:
|
||
* certificates. | ||
* | ||
* @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> | ||
*/ | ||
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 == null || domainNode.size() == 0) { | ||
ejona86 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
trustBundleMap.put(trustDomainName, Collections.emptyList()); | ||
continue; | ||
} | ||
Long sequenceNumber = JsonUtil.getNumberAsLong(domainNode, "sequence_number"); | ||
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; | ||
} | ||
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.parseNoDuplicates(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"); | ||
checkArgument(checkNotNull(trustDomainsNode, | ||
ejona86 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"Mandatory trust_domains element is missing").size() > 0, | ||
"Mandatory trust_domains element is missing"); | ||
return trustDomainsNode; | ||
} | ||
|
||
private static boolean checkJwkEntry(Map<String, ?> jwkNode, String trustDomainName) { | ||
String kty = JsonUtil.getString(jwkNode, "kty"); | ||
if (kty == null || !kty.equals(KTY_PARAMETER_VALUE)) { | ||
log.log(Level.SEVERE, String.format("'kty' parameter must be '%s' but '%s' found. " | ||
+ "Skipping certificate loading for trust domain '%s'.", KTY_PARAMETER_VALUE, kty, | ||
trustDomainName)); | ||
return false; | ||
} | ||
String kid = JsonUtil.getString(jwkNode, "kid"); | ||
if (kid != null && !kid.equals("")) { | ||
ejona86 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
log.log(Level.SEVERE, String.format("'kid' parameter must not be set but value '%s' " | ||
+ "found. Skipping certificate loading for trust domain '%s'.", kid, | ||
trustDomainName)); | ||
return false; | ||
} | ||
String use = JsonUtil.getString(jwkNode, "use"); | ||
if (use == null || !use.equals(USE_PARAMETER_VALUE)) { | ||
log.log(Level.SEVERE, String.format("'use' parameter must be '%s' but '%s' found. " | ||
+ "Skipping certificate loading for trust domain '%s'.", USE_PARAMETER_VALUE, use, | ||
trustDomainName)); | ||
return false; | ||
} | ||
return true; | ||
} | ||
|
||
private static List<X509Certificate> extractCert(List<Map<String, ?>> keysNode, | ||
String trustDomainName) { | ||
List<X509Certificate> result = new ArrayList<>(); | ||
for (Map<String, ?> keyNode : keysNode) { | ||
if (!checkJwkEntry(keyNode, trustDomainName)) { | ||
break; | ||
} | ||
String rawCert = JsonUtil.getString(keyNode, "x5c"); | ||
if (rawCert == null) { | ||
break; | ||
} | ||
InputStream stream = new ByteArrayInputStream(rawCert.getBytes(StandardCharsets.UTF_8)); | ||
try { | ||
Collection<? extends Certificate> certs = CertificateFactory.getInstance("X509") | ||
.generateCertificates(stream); | ||
if (certs.size() != 1) { | ||
log.log(Level.SEVERE, String.format("Exactly 1 certificate is expected, but %s found for " | ||
+ "domain %s.", certs.size(), trustDomainName)); | ||
} else { | ||
result.add(certs.toArray(new X509Certificate[0])[0]); | ||
} | ||
} catch (CertificateException e) { | ||
log.log(Level.SEVERE, String.format("Certificate for domain %s can't be parsed.", | ||
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> | ||
|
@@ -119,4 +280,34 @@ public String getPath() { | |
} | ||
} | ||
|
||
/** | ||
* Represents a Trust Bundle inspired by SPIFFE Bundle Format standard. Only trust domain's | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggested edit for clarity: Represents a SPIFFE trust bundle; that is, a map from trust domain to set of trusted certificates. |
||
* 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; | ||
|
||
public SpiffeBundle(Map<String, Long> sequenceNumbers, | ||
Map<String, List<X509Certificate>> trustDomainMap) { | ||
this.sequenceNumbers = ImmutableMap.copyOf(sequenceNumbers); | ||
matthewstevenson88 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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(); | ||
matthewstevenson88 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
public ImmutableMap<String, Long> getSequenceNumbers() { | ||
return sequenceNumbers; | ||
} | ||
|
||
public ImmutableMap<String, ImmutableList<X509Certificate>> getBundleMap() { | ||
return bundleMap; | ||
} | ||
} | ||
|
||
} |
Uh oh!
There was an error while loading. Please reload this page.