Merge pull request #11 from maxlambrecht/federation-support

Adding Federation support
This commit is contained in:
Max 2018-09-26 10:12:24 -03:00 committed by GitHub
commit adbda7b957
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 127 additions and 42 deletions

View File

@ -10,6 +10,9 @@ The JAVA-SPIFFE library provides two components:
and receives the updates asynchronously from the Workload API. Using the terminology of the Java Security API,
this library provides a custom Security Provider that can be installed in the JVM.
It supports Federation. The TrustStore validates the peer's SVID using a set of Trusted CAs that includes the
Federated TrustDomains CAs bundles. These Federated CAs bundles come from the Workload API in the X509SVIDResponse.
## SPIFFE Workload API Client Example
The `X509SVIDFetcher` provides the `registerListener` method that allows a consumer to register a listener
@ -20,16 +23,18 @@ The gRPC channel is configured based on the Address (tcp or unix socket) and the
### Use
```
Fetcher<List<X509SVID>> svidFetcher = new X509SVIDFetcher("/tmp/agent.sock");
Consumer<List<X509SVID>> certificateUpdater;
certificateUpdater = certs -> {
certs.forEach(svid -> {
System.out.println("Spiffe ID fetched: " + svid.getSpiffeId());
});
};
Fetcher<X509SVIDResponse> svidFetcher = new X509SVIDFetcher("/tmp/agent.sock");
Consumer<X509SVIDResponse> xvidConsumer = x509SVIDResponse -> {
x509SVIDResponse.getSvidsList().forEach(svid -> {
System.out.println("Spiffe ID fetched: " + svid.getSpiffeId());
System.out.println("Federated with: " + svid.getFederatesWithList());
});
System.out.println(x509SVIDResponse.getFederatedBundlesMap());
};
//Registering the callback to receive the SVIDs from the Workload API
svidFetcher.registerListener(certificateUpdater);
svidFetcher.registerListener(xvidConsumer);
```
The `X509SVIDFetcher` can be configured with a custom `RetryPolicy`.
@ -84,7 +89,7 @@ ssl.KeyManagerFactory.algorithm=Spiffe
ssl.TrustManagerFactory.algorithm=Spiffe
# The list of spiffeIDs that will be authorized
ssl.spiffe.accept=spiffe://example.org/workload, spiffe://example.org/workload2
ssl.spiffe.accept=spiffe://example.org/workload, spiffe://example.org/workload2, spiffe://example2.org/workload
```
In your `java.security` file:
@ -92,6 +97,7 @@ In your `java.security` file:
* replace `<n>` following the order of the `# List of Providers` in the master file.
* replace the value of the custom property `ssl.spiffe.accept` with the Spiffe IDs of the workloads that are allowed to connect.
If the property is not present or if it's empty, any spiffe id will be authorized.
To pass your custom security properties file through the command line via system property when starting the JVM:

View File

@ -1,5 +1,5 @@
group 'spiffe'
version '0.1.0'
version '0.2.0'
buildscript {

View File

@ -17,7 +17,7 @@ import java.util.logging.Logger;
* Provides functionality to interact with a Workload API
*
*/
public final class X509SVIDFetcher implements Fetcher<List<X509SVID>> {
public final class X509SVIDFetcher implements Fetcher<X509SVIDResponse> {
private static final Logger LOGGER = Logger.getLogger(X509SVIDFetcher.class.getName());
@ -58,19 +58,19 @@ public final class X509SVIDFetcher implements Fetcher<List<X509SVID>> {
}
/**
* Register the listener to receive the X509 SVIDS from the Workload API
* Register the listener to receive the X509SVIDResponse from the Workload API
* In case there's an error in the connection with the Workload API,
* it retries using a RetryHandler that implements a backoff policy
*
*/
@Override
public void registerListener(Consumer<List<X509SVID>> listener) {
public void registerListener(Consumer<X509SVIDResponse> listener) {
StreamObserver<X509SVIDResponse> observer = new StreamObserver<X509SVIDResponse>() {
@Override
public void onNext(X509SVIDResponse value) {
LOGGER.log(Level.FINE, "New SVID received ");
listener.accept(value.getSvidsList());
listener.accept(value);
retryHandler.reset();
}

View File

@ -11,6 +11,8 @@ import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import static java.util.Collections.EMPTY_LIST;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.startsWith;
/**
@ -90,6 +92,8 @@ class CertificateUtils {
* Validates that the SPIFFE ID is present and matches the SPIFFE ID configured in
* the java.security property ssl.spiffe.accept
*
* If the authorized spiffe ids list is empty any spiffe id is authorized
*
* @param chain an array of X509Certificate that contains the Peer's SVID to be validated
* @throws CertificateException when either the certificates doesn't have a SPIFFE ID or the SPIFFE ID is not authorized
*/
@ -99,9 +103,9 @@ class CertificateUtils {
throw new CertificateException("SPIFFE ID not found in the certificate");
}
List<String> acceptedSpiffeIds = getAcceptedSpiffeIds();
List<String> acceptedSpiffeIds = getAuthorizedSpiffeIDs();
if (!acceptedSpiffeIds.contains(spiffeId.get())) {
if (!acceptedSpiffeIds.isEmpty() && !acceptedSpiffeIds.contains(spiffeId.get())) {
String errorMessage = String.format("SPIFFE ID %s is not authorized", spiffeId.get());
LOGGER.log(Level.WARNING, errorMessage);
throw new CertificateException(errorMessage);
@ -109,12 +113,15 @@ class CertificateUtils {
}
/**
* Returns the list of accepted spiffe id configured in the SSL_SPIFFE_ACCEPT_PROPERTY property in
* Returns the list of authorized spiffe ids configured in the SSL_SPIFFE_ACCEPT_PROPERTY property in
* the java security properties file
*
*/
private static List<String> getAcceptedSpiffeIds() {
private static List<String> getAuthorizedSpiffeIDs() {
String commaSeparatedSpiffeIds = Security.getProperty(SSL_SPIFFE_ACCEPT_PROPERTY);
if (isBlank(commaSeparatedSpiffeIds)) {
return EMPTY_LIST;
}
String [] array = commaSeparatedSpiffeIds.split(",");
return normalize(Arrays.asList(array));
}

View File

@ -1,13 +1,15 @@
package spiffe.provider;
import spiffe.api.svid.Fetcher;
import spiffe.api.svid.Workload.X509SVID;
import spiffe.api.svid.Workload;
import spiffe.api.svid.X509SVIDFetcher;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.logging.Level;
import java.util.logging.Logger;
import static java.util.Collections.EMPTY_SET;
@ -20,6 +22,7 @@ import static java.util.Collections.EMPTY_SET;
public class SpiffeIdManager {
private static final SpiffeIdManager INSTANCE = new SpiffeIdManager();
private static final Logger LOGGER = Logger.getLogger(SpiffeIdManager.class.getName());
static SpiffeIdManager getInstance() {
return INSTANCE;
@ -35,6 +38,11 @@ public class SpiffeIdManager {
*/
private final FunctionalReadWriteLock guard;
/**
* Used to make the getters wait until there's a spiffeSVID initialized
*/
private final CountDownLatch completedSpiffeSVIDUpdate = new CountDownLatch(1);
/**
* Private Constructor
*
@ -43,11 +51,12 @@ public class SpiffeIdManager {
*/
private SpiffeIdManager() {
guard = new FunctionalReadWriteLock();
Fetcher<List<X509SVID>> svidFetcher = new X509SVIDFetcher();
Fetcher<Workload.X509SVIDResponse> svidFetcher = new X509SVIDFetcher();
svidFetcher.registerListener(this::updateSVID);
}
public SpiffeSVID getSpiffeSVID() {
awaitSpiffeSVID();
return guard.read(() -> spiffeSVID);
}
@ -55,21 +64,34 @@ public class SpiffeIdManager {
* Method used as callback that gets executed whenever an SVID update is pushed by the Workload API
* Uses a write lock to synchronize access to spiffeSVID
*/
private void updateSVID(List<X509SVID> certs) {
X509SVID svid = certs.get(0);
guard.write(() -> spiffeSVID = new SpiffeSVID(svid));
private void updateSVID(Workload.X509SVIDResponse x509SVIDResponse) {
guard.write(() -> spiffeSVID = new SpiffeSVID(x509SVIDResponse));
completedSpiffeSVIDUpdate.countDown();
LOGGER.log(Level.FINE, "Spiffe SVID has been updated ");
}
X509Certificate getCertificate() {
public X509Certificate getCertificate() {
awaitSpiffeSVID();
return guard.read(() -> spiffeSVID != null ? spiffeSVID.getCertificate() : null);
}
PrivateKey getPrivateKey() {
public PrivateKey getPrivateKey() {
awaitSpiffeSVID();
return guard.read(() -> spiffeSVID != null ? spiffeSVID.getPrivateKey() : null);
}
@SuppressWarnings("unchecked")
Set<X509Certificate> getTrustedCerts() {
return guard.read(() -> spiffeSVID != null ? spiffeSVID.getBundle() : EMPTY_SET);
public Set<X509Certificate> getTrustedCerts() {
awaitSpiffeSVID();
return guard.read(() -> spiffeSVID != null ? spiffeSVID.getTrustedCerts() : EMPTY_SET);
}
private void awaitSpiffeSVID() {
try {
completedSpiffeSVIDUpdate.await();
} catch (InterruptedException e) {
LOGGER.info("Interrupted " + e.getMessage());
Thread.currentThread().interrupt();
}
}
}

View File

@ -1,9 +1,14 @@
package spiffe.provider;
import com.google.protobuf.ByteString;
import spiffe.api.svid.Workload;
import java.security.PrivateKey;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
@ -20,37 +25,71 @@ public class SpiffeSVID {
* The SPIFFE Identity String
*/
private String spiffeID;
/**
* The SPIFFE Verifiable Identity Document
*/
private X509Certificate certificate;
/**
* The Private Key associated to the Public Key of the certificate
*/
private PrivateKey privateKey;
/**
* The trust chain used as the set of CAs trusted certificates
*/
private Set<X509Certificate> bundle;
/**
* The map of Federated Trust Domains and their bundles
*/
private Map<String, Set<X509Certificate>> federatedBundles;
/**
* All trusted CAs certs, including the Federated CAs
*/
private Set<X509Certificate> trustedCerts;
/**
* Constructor
*
* @param x509SVID: Workload.X509SVID
* @param x509SVIDResponse: Workload.X509SVIDResponse
*
*/
SpiffeSVID(Workload.X509SVID x509SVID) {
SpiffeSVID(Workload.X509SVIDResponse x509SVIDResponse) {
try {
certificate = CertificateUtils.generateCertificate(x509SVID.getX509Svid().toByteArray());
bundle = CertificateUtils.generateCertificates(x509SVID.getBundle().toByteArray());
privateKey = CertificateUtils.generatePrivateKey(x509SVID.getX509SvidKey().toByteArray());
spiffeID = x509SVID.getSpiffeId();
Workload.X509SVID svid = x509SVIDResponse.getSvidsList().get(0);
certificate = CertificateUtils.generateCertificate(svid.getX509Svid().toByteArray());
bundle = CertificateUtils.generateCertificates(svid.getBundle().toByteArray());
privateKey = CertificateUtils.generatePrivateKey(svid.getX509SvidKey().toByteArray());
spiffeID = svid.getSpiffeId();
federatedBundles = buildFederatedX509CertificatesMap(x509SVIDResponse.getFederatedBundlesMap());
trustedCerts = new HashSet<>();
trustedCerts.addAll(bundle);
federatedBundles.values().forEach(set -> trustedCerts.addAll(set));
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "SVID message could not be processed", e);
throw new RuntimeException(e);
}
}
private Map<String, Set<X509Certificate>> buildFederatedX509CertificatesMap(Map<String, ByteString> federatedBundlesMap) {
Map<String, Set<X509Certificate>> federatedCertificates = new HashMap<>();
federatedBundlesMap.forEach((trustDomain, cert) -> {
try {
federatedCertificates.put(trustDomain, CertificateUtils.generateCertificates(cert.toByteArray()));
} catch (CertificateException e) {
LOGGER.log(Level.SEVERE, "Federated Bundles couldn't be processed ", e);
throw new RuntimeException(e);
}
});
return federatedCertificates;
}
public String getSpiffeID() {
return spiffeID;
}
@ -66,4 +105,12 @@ public class SpiffeSVID {
public Set<X509Certificate> getBundle() {
return bundle;
}
public Map<String, Set<X509Certificate>> getFederatedBundles() {
return federatedBundles;
}
public Set<X509Certificate> getTrustedCerts() {
return trustedCerts;
}
}

View File

@ -7,7 +7,7 @@ message X509SVIDRequest { }
// The X509SVIDResponse message carries a set of X.509 SVIDs and their
// associated information. It also carries a set of global CRLs, and a
// TTL to inform the svid when it should check back next.
// TTL to inform the workload when it should check back next.
message X509SVIDResponse {
// A list of X509SVID messages, each of which includes a single
// SPIFFE Verifiable Identity Document, along with its private key
@ -18,19 +18,19 @@ message X509SVIDResponse {
repeated bytes crl = 2;
// CA certificate bundles belonging to foreign Trust Domains that the
// svid should trust, keyed by the SPIFFE ID of the foreign
// workload should trust, keyed by the SPIFFE ID of the foreign
// domain. Bundles are ASN.1 DER encoded.
map<string, bytes> federated_bundles = 3;
}
// The X509SVID message carries a single WorkloadAPIClient and all associated
// The X509SVID message carries a single SVID and all associated
// information, including CA bundles.
message X509SVID {
// The SPIFFE ID of the WorkloadAPIClient in this entry
// The SPIFFE ID of the SVID in this entry
string spiffe_id = 1;
// ASN.1 DER encoded certificate chain. MAY include intermediates,
// the leaf certificate (or WorkloadAPIClient itself) MUST come first.
// the leaf certificate (or SVID itself) MUST come first.
bytes x509_svid = 2;
// ASN.1 DER encoded PKCS#8 private key. MUST be unencrypted.
@ -40,12 +40,15 @@ message X509SVID {
// ASN.1 DER encoded
bytes bundle = 4;
// List of trust domains the SVID federates with, which corresponds to
// keys in the federated_bundles map in the X509SVIDResponse message.
repeated string federates_with = 5;
}
service SpiffeWorkloadAPI {
// X.509-WorkloadAPIClient Profile
// Fetch all SPIFFE identities the svid is entitled to, as
// X.509-SVID Profile
// Fetch all SPIFFE identities the workload is entitled to, as
// well as related information like trust bundles and CRLs. As
// this information changes, subsequent messages will be sent.
rpc FetchX509SVID(X509SVIDRequest) returns (stream X509SVIDResponse);
}
}