Refactor error handling: use Exceptions instead of Result wrapper type

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
This commit is contained in:
Max Lambrecht 2020-04-26 13:58:21 -03:00
parent 8e06cb12d7
commit 44cda6e809
56 changed files with 1134 additions and 1249 deletions

View File

@ -3,7 +3,7 @@ package spiffe.bundle.jwtbundle;
import lombok.NonNull;
import lombok.Value;
import org.apache.commons.lang3.NotImplementedException;
import spiffe.result.Result;
import spiffe.exception.BundleNotFoundException;
import spiffe.spiffeid.TrustDomain;
import java.nio.file.Path;
@ -29,9 +29,9 @@ public class JwtBundle implements JwtBundleSource {
/**
* Creates a new bundle from JWT public keys.
*
* @param trustDomain a TrustDomain to associate to the JwtBundle
* @param jwtKeys a Map of Public Keys
* @return a new JwtBundle.
* @param trustDomain a {@link TrustDomain} to associate to the JwtBundle
* @param jwtKeys a Map of public Keys
* @return a new {@link JwtBundle}.
*/
public static JwtBundle fromJWTKeys(@NonNull TrustDomain trustDomain, Map<String, PublicKey> jwtKeys) {
throw new NotImplementedException("Not implemented");
@ -40,11 +40,11 @@ public class JwtBundle implements JwtBundleSource {
/**
* Loads a bundle from a file on disk.
*
* @param trustDomain a TrustDomain to associate to the JwtBundle.
* @param bundlePath a path to a file containing the JwtBundle.
* @return a <code>Result.ok(jwtBundle)</code>, or a <code>Result.error(errorMessage)</code>
* @param trustDomain a {@link TrustDomain} to associate to the JWT bundle.
* @param bundlePath a path to a file containing the JWT bundle.
* @return a instance of a {@link JwtBundle}
*/
public static Result<JwtBundle, String > load(
public static JwtBundle load(
@NonNull final TrustDomain trustDomain,
@NonNull final Path bundlePath) {
throw new NotImplementedException("Not implemented");
@ -53,29 +53,30 @@ public class JwtBundle implements JwtBundleSource {
/**
* Parses a bundle from a byte array.
*
* @param trustDomain a TrustDomain
* @param bundleBytes an array of bytes representing the bundle.
* @return
* @param trustDomain a {@link TrustDomain}
* @param bundleBytes an array of bytes representing the JWT bundle.
* @return an instance of a {@link JwtBundle}
*/
public static Result<JwtBundle, String> parse(
public static JwtBundle parse(
@NonNull final TrustDomain trustDomain,
@NonNull final byte[] bundleBytes) {
throw new NotImplementedException("Not implemented");
}
/**
* Returns the JwtBundle for a TrustDomain.
* Returns the JWT bundle for a trust domain.
*
* @param trustDomain an instance of a TrustDomain
* @return a {@link spiffe.result.Ok} containing the JwtBundle for the TrustDomain, or
* an {@link spiffe.result.Error} if there is no bundle for the TrustDomain
* @param trustDomain a {@link TrustDomain}
* @return a {@link JwtBundle} for the trust domain
*
* @throws BundleNotFoundException if there is no bundle for the given trust domain
*/
@Override
public Result<JwtBundle, String> getJwtBundleForTrustDomain(TrustDomain trustDomain) {
public JwtBundle getJwtBundleForTrustDomain(TrustDomain trustDomain) throws BundleNotFoundException {
if (this.trustDomain.equals(trustDomain)) {
return Result.ok(this);
return this;
}
return Result.error("No JWT bundle for trust domain %s", trustDomain);
throw new BundleNotFoundException(String.format("No JWT bundle found for trust domain %s", trustDomain));
}
/**
@ -84,9 +85,9 @@ public class JwtBundle implements JwtBundleSource {
* it returns an Optional.empty().
*
* @param keyId the Key ID
* @return an {@link Optional} containing a PublicKey.
* @return an {@link Optional} containing a {@link PublicKey}.
*/
public Optional<PublicKey> findJwtKey(String keyId) {
public Optional<PublicKey> findJwtKey(String keyId) {
throw new NotImplementedException("Not implemented");
}

View File

@ -3,15 +3,14 @@ package spiffe.bundle.jwtbundle;
import lombok.NonNull;
import lombok.Value;
import org.apache.commons.lang3.NotImplementedException;
import spiffe.result.Result;
import spiffe.exception.BundleNotFoundException;
import spiffe.spiffeid.TrustDomain;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
/**
* A <code>JwtBundleSet</code> represents a set of X509Bundles keyed by TrustDomain.
* A <code>JwtBundleSet</code> represents a set of JWT bundles keyed by trust domain.
*/
@Value
public class JwtBundleSet implements JwtBundleSource {
@ -22,29 +21,33 @@ public class JwtBundleSet implements JwtBundleSource {
this.bundles = bundles;
}
/**
* Creates a JWT bundle set from the list of JWT bundles.
*
* @param bundles List of {@link JwtBundle}
* @return a {@link JwtBundleSet}
*/
public static JwtBundleSet of(@NonNull final List<JwtBundle> bundles) {
throw new NotImplementedException("Not implemented");
}
public static JwtBundleSet of(@NonNull final TrustDomain trustDomain,
@NonNull final JwtBundle jwtBundle) {
throw new NotImplementedException("Not implemented");
}
public List<JwtBundle> getJwtBundles() {
return new ArrayList<>(bundles.values());
}
/**
* Gets the JWT bundle associated to a trust domain.
*
* @param trustDomain an instance of a {@link TrustDomain}
* @return a {@link JwtBundle} associated to the given trust domain
* @throws BundleNotFoundException if no bundle could be found for the given trust domain
*/
@Override
public Result<JwtBundle, String> getJwtBundleForTrustDomain(final TrustDomain trustDomain) {
public JwtBundle getJwtBundleForTrustDomain(final TrustDomain trustDomain) throws BundleNotFoundException {
if (bundles.containsKey(trustDomain)) {
return Result.ok(bundles.get(trustDomain));
return bundles.get(trustDomain);
}
return Result.error("No JWT bundle for trust domain %s", trustDomain);
throw new BundleNotFoundException(String.format("No JWT bundle for trust domain %s", trustDomain));
}
/**
* Add bundle to set, if the trustDomain already exists
* Add JWT bundle to this set, if the trust domain already exists
* replace the bundle.
*
* @param jwtBundle an instance of a JwtBundle.

View File

@ -1,20 +1,21 @@
package spiffe.bundle.jwtbundle;
import lombok.NonNull;
import spiffe.result.Result;
import spiffe.exception.BundleNotFoundException;
import spiffe.spiffeid.TrustDomain;
/**
* A <code>JwtBundleSource</code> represents a source of JWT-Bundles.
* A <code>JwtBundleSource</code> represents a source of JWT bundles.
*/
public interface JwtBundleSource {
/**
* Returns the JWT bundle for a trustDomain.
* Returns the JWT bundle for a trust domain.
*
* @param trustDomain an instance of a TrustDomain
* @return a {@link spiffe.result.Ok} containing a {@link JwtBundle}, or a {@link spiffe.result.Error} if
* no bundle is found for the given trust domain.
* @param trustDomain an instance of a {@link TrustDomain}
* @return the {@link JwtBundle} for the given trust domain
*
* @throws BundleNotFoundException if no bundle is found for the given trust domain.
*/
Result<JwtBundle, String> getJwtBundleForTrustDomain(@NonNull final TrustDomain trustDomain);
JwtBundle getJwtBundleForTrustDomain(@NonNull final TrustDomain trustDomain) throws BundleNotFoundException;
}

View File

@ -3,13 +3,14 @@ package spiffe.bundle.x509bundle;
import lombok.NonNull;
import lombok.Value;
import lombok.val;
import spiffe.exception.BundleNotFoundException;
import spiffe.internal.CertificateUtils;
import spiffe.result.Result;
import spiffe.spiffeid.TrustDomain;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.HashSet;
import java.util.Set;
@ -29,54 +30,53 @@ public class X509Bundle implements X509BundleSource {
}
/**
* Load loads a Bundle from a file on disk.
* Loads a X509 bundle from a file on disk.
*
* @param trustDomain a TrustDomain to associate to the bundle
* @param bundlePath a Path to the file that has the X509 Authorities
* @return an instance of X509Bundle with the X509 Authorities
* associated to the TrustDomain.
* @param trustDomain a {@link TrustDomain} to associate to the bundle
* @param bundlePath a path to the file that has the X509 authorities
* @return an instance of {@link X509Bundle} with the X509 authorities
* associated to the trust domain.
*
* @throws IOException in case of failure accessing the given bundle path
* @throws CertificateException if the bundle cannot be parsed
*/
public static Result<X509Bundle, String> load(@NonNull final TrustDomain trustDomain, @NonNull final Path bundlePath) {
try {
val bundleBytes = Files.readAllBytes(bundlePath);
val x509Certificates = CertificateUtils.generateCertificates(bundleBytes);
if (x509Certificates.isError()) {
return Result.error(x509Certificates.getError());
}
val x509CertificateSet = new HashSet<>(x509Certificates.getValue());
val x509Bundle = new X509Bundle(trustDomain, x509CertificateSet);
return Result.ok(x509Bundle);
} catch (IOException e) {
return Result.error("Error loading X509Bundle from path %s: %s", bundlePath, e.getMessage());
}
public static X509Bundle load(@NonNull final TrustDomain trustDomain, @NonNull final Path bundlePath) throws IOException, CertificateException {
val bundleBytes = Files.readAllBytes(bundlePath);
val x509Certificates = CertificateUtils.generateCertificates(bundleBytes);
val x509CertificateSet = new HashSet<>(x509Certificates);
return new X509Bundle(trustDomain, x509CertificateSet);
}
/**
* Parses a bundle from a byte array.
* Parses a X095 bundle from an array of bytes.
*
* @param trustDomain a TrustDomain to associate to the bundle
* @param bundleBytes an array of bytes that represents the X509 Authorities
* @return an instance of X509Bundle with the X509 Authorities
* associated to the TrustDomain.
* @param trustDomain a {@link TrustDomain} to associate to the X509 bundle
* @param bundleBytes an array of bytes that represents the X509 authorities
*
* @return an instance of {@link X509Bundle} with the X509 authorities
* associated to the given trust domain
*
* @throws CertificateException if the bundle cannot be parsed
*/
public static Result<X509Bundle, String> parse(@NonNull final TrustDomain trustDomain, @NonNull final byte[] bundleBytes) {
public static X509Bundle parse(@NonNull final TrustDomain trustDomain, @NonNull final byte[] bundleBytes) throws CertificateException {
val x509Certificates = CertificateUtils.generateCertificates(bundleBytes);
if (x509Certificates.isError()) {
return Result.error(x509Certificates.getError());
}
val x509CertificateSet = new HashSet<>(x509Certificates.getValue());
val x509Bundle = new X509Bundle(trustDomain, x509CertificateSet);
return Result.ok(x509Bundle);
val x509CertificateSet = new HashSet<>(x509Certificates);
return new X509Bundle(trustDomain, x509CertificateSet);
}
/**
* Returns the X509 bundle associated to the trust domain.
*
* @param trustDomain an instance of a {@link TrustDomain}
* @return the {@link X509Bundle} associated to the given trust domain
*
* @throws BundleNotFoundException if no X509 bundle can be found for the given trust domain
*/
@Override
public Result<X509Bundle, String> getX509BundleForTrustDomain(TrustDomain trustDomain) {
public X509Bundle getX509BundleForTrustDomain(TrustDomain trustDomain) throws BundleNotFoundException {
if (this.trustDomain.equals(trustDomain)) {
return Result.ok(this);
return this;
}
return Result.error("No X509 bundle for trust domain %s", trustDomain);
throw new BundleNotFoundException(String.format("No X509 bundle found for trust domain %s", trustDomain));
}
}

View File

@ -2,15 +2,14 @@ package spiffe.bundle.x509bundle;
import lombok.NonNull;
import lombok.Value;
import spiffe.result.Result;
import spiffe.exception.BundleNotFoundException;
import spiffe.spiffeid.TrustDomain;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
/**
* A <code>X509BundleSet</code> represents a set of X509 Bundles keyed by TrustDomain.
* A <code>X509BundleSet</code> represents a set of X509 bundles keyed by trust domain.
*/
@Value
public class X509BundleSet implements X509BundleSource {
@ -22,10 +21,10 @@ public class X509BundleSet implements X509BundleSource {
}
/**
* Creates a new <code>X509BundleSet</code> initialized with the bundles
* Creates a new X509 bundle set from a list of X509 bundles.
*
* @param bundles list of bundles to put in the X509BundleSet
* @return a X509BundleSet initialized with the list of bundles
* @param bundles a list of {@link X509Bundle}
* @return a {@link X509BundleSet} initialized with the list of bundles
*/
public static X509BundleSet of(@NonNull final List<X509Bundle> bundles) {
ConcurrentHashMap<TrustDomain, X509Bundle> bundleMap = new ConcurrentHashMap<>();
@ -36,46 +35,27 @@ public class X509BundleSet implements X509BundleSource {
}
/**
* Creates a new <code>X509BundleSet</code> initialized with the x509Bundle.
*/
public static X509BundleSet of(@NonNull final TrustDomain trustDomain, @NonNull final X509Bundle x509Bundle) {
ConcurrentHashMap<TrustDomain, X509Bundle> bundleMap = new ConcurrentHashMap<>();
bundleMap.put(trustDomain, x509Bundle);
return new X509BundleSet(bundleMap);
}
/**
* Adds a bundle to set, if the trustDomain already exists
* replace the bundle.
* Adds a bundle to this Set, if the trust domain already exists,
* replaces the bundle.
*
* @param x509Bundle a X509Bundle.
* @param x509Bundle a {@link X509Bundle}
*/
public void add(@NonNull X509Bundle x509Bundle){
bundles.put(x509Bundle.getTrustDomain(), x509Bundle);
}
/**
* Returns all the bundles contained in the X509BundleSet.
* Returns the X509 bundle associated to the trust domain.
*
* @return a list with all the bundles for all the trustDomains
*/
public List<X509Bundle> getX509Bundles() {
return new ArrayList<>(bundles.values());
}
/**
* Returns a {@link spiffe.result.Ok} containing the X509Bundle for a trust domain,
* if the current set doesn't have bundle for the trust domain,
* it returns an {@link spiffe.result.Error}.
*
* @param trustDomain an instance of a TrustDomain
* @return
* @param trustDomain an instance of a {@link TrustDomain}
* @return the {@link X509Bundle} associated to the given trust domain
* @throws BundleNotFoundException if no bundle could be found for the given trust domain
*/
@Override
public Result<X509Bundle, String> getX509BundleForTrustDomain(final TrustDomain trustDomain) {
public X509Bundle getX509BundleForTrustDomain(final TrustDomain trustDomain) throws BundleNotFoundException {
if (bundles.containsKey(trustDomain)) {
return Result.ok(bundles.get(trustDomain));
return bundles.get(trustDomain);
}
return Result.error("No X509 bundle for trust domain %s", trustDomain);
throw new BundleNotFoundException(String.format("No X509 bundle for trust domain %s", trustDomain));
}
}

View File

@ -2,21 +2,20 @@ package spiffe.bundle.x509bundle;
import lombok.NonNull;
import spiffe.result.Result;
import spiffe.exception.BundleNotFoundException;
import spiffe.spiffeid.TrustDomain;
/**
* A <code>X509BundleSource</code> represents a source of X509-Bundles keyed by TrustDomain.
* A <code>X509BundleSource</code> represents a source of X509 bundles keyed by trust domain.
*/
public interface X509BundleSource {
/**
* Returns the bundle associated to a trustDomain.
* Returns the X509 bundle associated to the given trust domain.
*
* @param trustDomain an instance of a TrustDomain
* @return a {@link spiffe.result.Ok} containing a {@link X509Bundle}, or a {@link spiffe.result.Error} if
* no bundle is found for the given trust domain.
* @param trustDomain an instance of a {@link TrustDomain}
* @return the {@link X509Bundle} for the given trust domain
* @throws BundleNotFoundException if no bundle is found for the given trust domain
*/
Result<X509Bundle, String> getX509BundleForTrustDomain(@NonNull final TrustDomain trustDomain);
X509Bundle getX509BundleForTrustDomain(@NonNull final TrustDomain trustDomain) throws BundleNotFoundException;
}

View File

@ -0,0 +1,15 @@
package spiffe.exception;
/**
* Checked exception thrown to indicate that a bundle could not be
* found in the bundle source.
*/
public class BundleNotFoundException extends Exception {
public BundleNotFoundException(String message) {
super(message);
}
public BundleNotFoundException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,19 @@
package spiffe.exception;
/**
* Checked exception thrown to indicate that the socket endpoint address
* could not be parsed or is not valid.
*/
public class SocketEndpointAddressException extends Exception {
public SocketEndpointAddressException(String message) {
super(message);
}
public SocketEndpointAddressException(String message, Throwable cause) {
super(message, cause);
}
public SocketEndpointAddressException(Throwable cause) {
super(cause);
}
}

View File

@ -0,0 +1,19 @@
package spiffe.exception;
/**
* Unchecked exception thrown when a there was an error retrieving
* or processing a X509Context.
*/
public class X509ContextException extends RuntimeException {
public X509ContextException(String message) {
super(message);
}
public X509ContextException(String message, Throwable cause) {
super(message, cause);
}
public X509ContextException(Throwable cause) {
super(cause);
}
}

View File

@ -0,0 +1,18 @@
package spiffe.exception;
/**
* Unchecked thrown when there is an error creating or initializing a X509 source
*/
public class X509SourceException extends RuntimeException {
public X509SourceException(String message) {
super(message);
}
public X509SourceException(String message, Throwable cause) {
super(message, cause);
}
public X509SourceException(Throwable cause) {
super(cause);
}
}

View File

@ -0,0 +1,20 @@
package spiffe.exception;
/**
* Checked exception thrown when there is an error parsing
* the components of an X509 SVID.
*/
public class X509SvidException extends Exception {
public X509SvidException(String message) {
super(message);
}
public X509SvidException(String message, Throwable cause) {
super(message, cause);
}
public X509SvidException(Throwable cause) {
super(cause);
}
}

View File

@ -1,8 +1,7 @@
package spiffe.internal;
import lombok.val;
import org.apache.commons.lang3.exception.ExceptionUtils;
import spiffe.result.Result;
import lombok.var;
import spiffe.spiffeid.SpiffeId;
import spiffe.spiffeid.TrustDomain;
@ -14,7 +13,6 @@ import java.security.PrivateKey;
import java.security.cert.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.stream.Collectors;
@ -33,139 +31,108 @@ public class CertificateUtils {
private static final String X509_CERTIFICATE_TYPE = "X.509";
/**
* Generate a List of X509Certificates from a byte array.
* Generate a list of X509 certificates from a byte array.
*
* @param input as byte array representing a list of X509Certificates, as a DER or PEM
* @return a List of X509Certificate
* @param input as byte array representing a list of X509 certificates, as a DER or PEM
* @return a List of {@link X509Certificate}
*/
public static Result<List<X509Certificate>, String> generateCertificates(byte[] input) {
public static List<X509Certificate> generateCertificates(byte[] input) throws CertificateException {
val certificateFactory = getCertificateFactory();
if (certificateFactory.isError()) {
return Result.error("Error parsing certificates: could not create certificate factory %s", certificateFactory.getError());
}
try {
val certificates = certificateFactory
.getValue()
.generateCertificates(new ByteArrayInputStream(input));
val certificates = certificateFactory
.generateCertificates(new ByteArrayInputStream(input));
val x509CertificateList = certificates.stream()
.map(X509Certificate.class::cast)
.collect(Collectors.toList());
return Result.ok(x509CertificateList);
} catch (CertificateException e) {
return Result.error("Error parsing certificates: %s", e.getMessage());
}
return certificates.stream()
.map(X509Certificate.class::cast)
.collect(Collectors.toList());
}
/**
* Generates a PrivateKey from an array of bytes.
* Generates a private key from an array of bytes.
*
* @param privateKeyBytes is a PEM or DER PKCS#8 Private Key.
* @return a Result {@link spiffe.result.Ok} containing a {@link PrivateKey} or an {@link spiffe.result.Error}.
* @param privateKeyBytes is a PEM or DER PKCS#8 private key.
* @return a instance of {@link PrivateKey}
* @throws InvalidKeySpecException
* @throws NoSuchAlgorithmException
*/
public static Result<PrivateKey, String> generatePrivateKey(byte[] privateKeyBytes) {
public static PrivateKey generatePrivateKey(byte[] privateKeyBytes) throws InvalidKeySpecException, NoSuchAlgorithmException {
PKCS8EncodedKeySpec kspec = new PKCS8EncodedKeySpec(privateKeyBytes);
Result<PrivateKey, Throwable> privateKeyResult = generatePrivateKeyWithSpec(kspec);
if (privateKeyResult.isOk()) {
return Result.ok(privateKeyResult.getValue());
}
// PrivateKey is in PEM format, not supported, need to convert to DER and try again
if (privateKeyResult.getError() instanceof InvalidKeySpecException) {
PrivateKey privateKey = null;
try {
privateKey = generatePrivateKeyWithSpec(kspec);
} catch (InvalidKeySpecException e) {
byte[] keyDer = toDerFormat(privateKeyBytes);
kspec= new PKCS8EncodedKeySpec(keyDer);
privateKeyResult = generatePrivateKeyWithSpec(kspec);
}
return Result.ok(privateKeyResult.getValue());
}
private static Result<PrivateKey, Throwable> generatePrivateKeyWithSpec(PKCS8EncodedKeySpec kspec) {
try {
val keyFactory = KeyFactory.getInstance(PRIVATE_KEY_ALGORITHM);
val privateKey = keyFactory.generatePrivate(kspec);
return Result.ok(privateKey);
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
return Result.error(e);
kspec = new PKCS8EncodedKeySpec(keyDer);
privateKey = generatePrivateKeyWithSpec(kspec);
}
return privateKey;
}
/**
* Validate a certificate chain against a set of trusted certificates.
* Validate a certificate chain with a set of trusted certificates.
*
* @param chain the certificate chain
* @param chain the certificate chain
* @param trustedCerts to validate the certificate chain
* @return a Result {@link spiffe.result.Ok} if the chain can be chained to any of the trustedCerts, or
* an {@link spiffe.result.Error}.
*
* @throws CertificateException
* @throws InvalidAlgorithmParameterException
* @throws NoSuchAlgorithmException
* @throws CertPathValidatorException
*/
public static Result<Boolean, String> validate(List<X509Certificate> chain, List<X509Certificate> trustedCerts) {
public static void validate(List<X509Certificate> chain, List<X509Certificate> trustedCerts) throws CertificateException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, CertPathValidatorException {
val certificateFactory = getCertificateFactory();
if (certificateFactory.isError()) {
return Result.error(certificateFactory.getError());
}
try {
PKIXParameters pkixParameters = toPkixParameters(trustedCerts);
val certPath = certificateFactory.getValue().generateCertPath(chain);
getCertPathValidator().validate(certPath, pkixParameters);
} catch (CertificateException | NoSuchAlgorithmException | InvalidAlgorithmParameterException | CertPathValidatorException e) {
return Result.error("Error validating certificate chain: %s %n %s", e.getMessage(), ExceptionUtils.getStackTrace(e));
}
return Result.ok(true);
val pkixParameters = toPkixParameters(trustedCerts);
val certPath = certificateFactory.generateCertPath(chain);
getCertPathValidator().validate(certPath, pkixParameters);
}
/**
* Extracts the SpiffeID from a SVID - X509Certificate.
* Extracts the SPIFE ID from a X509 certificate.
* <p>
* It iterates over the list of SubjectAlternativesNames, read each entry, takes the value from the index
* defined in SAN_VALUE_INDEX and filters the entries that starts with the SPIFFE_PREFIX and returns the first.
*
* @param certificate a X509Certificate
* @return spiffe.result.Result with the SpiffeId
* @throws RuntimeException when the certificate subjectAlternatives names cannot be read
* @throws IllegalArgumentException when the certificate contains multiple SpiffeId.
* @param certificate a {@link X509Certificate}
* @return an instance of a {@link SpiffeId}
* @throws CertificateException if the certificate contains multiple SPIFFE IDs, or does not contain any, or
* the SAN extension cannot be decoded
*/
public static Result<SpiffeId, String> getSpiffeId(X509Certificate certificate) {
public static SpiffeId getSpiffeId(X509Certificate certificate) throws CertificateException {
val spiffeIds = getSpiffeIds(certificate);
if (spiffeIds.size() > 1) {
return Result.error("Certificate contains multiple SPIFFE IDs.");
throw new CertificateException("Certificate contains multiple SPIFFE IDs");
}
if (spiffeIds.size() < 1) {
return Result.error("No SPIFFE ID found in the certificate.");
throw new CertificateException("No SPIFFE ID found in the certificate");
}
val spiffeId = SpiffeId.parse(spiffeIds.get(0));
if (spiffeId.isError()) {
return Result.error(spiffeId.getError());
}
return spiffeId;
return SpiffeId.parse(spiffeIds.get(0));
}
// Extracts the trustDomain of a chain of certificates
public static Result<TrustDomain, String> getTrustDomain(List<X509Certificate> chain) {
/**
* Extracts the trust domain of a chain of certificates.
*
* @param chain a list of {@link X509Certificate}
* @return a {@link TrustDomain}
*
* @throws CertificateException
*/
public static TrustDomain getTrustDomain(List<X509Certificate> chain) throws CertificateException {
val spiffeId = getSpiffeId(chain.get(0));
if (spiffeId.isError()) {
return Result.error(spiffeId.getError());
}
return Result.ok(spiffeId.getValue().getTrustDomain());
return spiffeId.getTrustDomain();
}
private static List<String> getSpiffeIds(X509Certificate certificate) {
try {
return certificate.getSubjectAlternativeNames()
.stream()
.map(san -> (String) san.get(SAN_VALUE_INDEX))
.filter(uri -> startsWith(uri, SPIFFE_PREFIX))
.collect(Collectors.toList());
} catch (CertificateParsingException e) {
return new ArrayList<>();
}
private static List<String> getSpiffeIds(X509Certificate certificate) throws CertificateParsingException {
return certificate.getSubjectAlternativeNames()
.stream()
.map(san -> (String) san.get(SAN_VALUE_INDEX))
.filter(uri -> startsWith(uri, SPIFFE_PREFIX))
.collect(Collectors.toList());
}
private static PrivateKey generatePrivateKeyWithSpec(PKCS8EncodedKeySpec kspec) throws NoSuchAlgorithmException, InvalidKeySpecException {
return KeyFactory.getInstance(PRIVATE_KEY_ALGORITHM).generatePrivate(kspec);
}
// Create an instance of PKIXParameters used as input for the PKIX CertPathValidator
@ -187,17 +154,13 @@ public class CertificateUtils {
}
// Get the X509 Certificate Factory
private static Result<CertificateFactory, String> getCertificateFactory() {
try {
return Result.ok(CertificateFactory.getInstance(X509_CERTIFICATE_TYPE));
} catch (CertificateException e) {
return Result.error("Error creating certificate factory: %s", e.getMessage());
}
private static CertificateFactory getCertificateFactory() throws CertificateException {
return CertificateFactory.getInstance(X509_CERTIFICATE_TYPE);
}
// Given a private key in PEM format, encode it as DER
private static byte[] toDerFormat(byte[] privateKeyPem) {
String privateKey = new String(privateKeyPem);
var privateKey = new String(privateKeyPem);
privateKey = privateKey.replaceAll("(-+BEGIN PRIVATE KEY-+\\r?\\n|-+END PRIVATE KEY-+\\r?\\n?)", "");
privateKey = privateKey.replaceAll("\n", "");
val decoder = Base64.getDecoder();

View File

@ -1,39 +0,0 @@
package spiffe.result;
import lombok.Value;
import java.util.NoSuchElementException;
/**
* An {@link spiffe.result.Error} represents a Result that conveys an error of type E.
*
* @param <V> the type of the value conveyed by the Result
* @param <E> the type of the error wrapped in the Error
*/
@Value
public class Error<V, E> implements Result<V, E> {
E error;
Error(final E error) {
this.error = error;
}
/**
* @throws NoSuchElementException, Error does not contain any value.
*/
@Override
public V getValue() {
throw new NoSuchElementException("No value present in Error");
}
@Override
public boolean isOk() {
return false;
}
@Override
public boolean isError() {
return true;
}
}

View File

@ -1,44 +0,0 @@
package spiffe.result;
import lombok.Value;
import java.util.NoSuchElementException;
/**
* An {@link spiffe.result.Ok} represents a Result that conveys a value of type T.
*
* @param <V> the type the value wrapped in the Ok result
* @param <E> the type of the error
*/
@Value
public class Ok<V, E> implements Result<V, E> {
V value;
public Ok(final V value) {
this.value = value;
}
@Override
public V getValue() {
return value;
}
/**
* @throws NoSuchElementException, Ok does not contain any Error.
*/
@Override
public E getError() {
throw new NoSuchElementException("No error present in an Ok result");
}
@Override
public boolean isOk() {
return true;
}
@Override
public boolean isError() {
return false;
}
}

View File

@ -1,67 +0,0 @@
package spiffe.result;
import java.util.function.BiFunction;
import java.util.function.Function;
/**
* A <code>Result</code> represents the result of an operation, that can be {@link spiffe.result.Ok} or {@link spiffe.result.Error}.
*
* @param <V> type of the value conveyed by the Result
* @param <E> type of the error conveyed by the Result.
*
* @see Ok
* @see Error
*/
public interface Result<V, E> {
V getValue();
E getError();
boolean isOk();
boolean isError();
static <V, E> Ok<V, E> ok(final V value) {
return new Ok<>(value);
}
static <V, E> Error<V, E> error(final E error) {
return new Error<>(error);
}
static <V> Result<V, String> error(String format, Object ...args) {
return Result.error(String.format(format, args));
}
/**
* Applies the Function if the actual Result is an Ok.
*
* @param fn Function to apply, receives a superclass of U and returns a Result of T
* @param u Parameter of the Function
* @param <U> Type of the parameter of the Function
* @return A Result of type V.
*/
default <U> Result<V, E> thenApply(Function<? super U, Result<V, E>> fn, U u) {
if (this.isOk()) {
return fn.apply(u);
}
return this;
}
/**
* Applies the BiFunction if the actual Result is an Ok.
*
* @param fn Function to apply, receives a superclass of U and returns a Result of T
* @param u First parameter of the BiFunction
* @param s Second parameter of the BiFunction
* @param <U> Type of the parameter of the BiFunction
* @return A Result of type V.
*/
default <U, S> Result<V, E> thenApply(BiFunction<? super U, ? super S, Result<V, E>> fn, U u, S s) {
if (this.isOk()) {
return fn.apply(u, s);
}
return this;
}
}

View File

@ -1,9 +1,9 @@
package spiffe.spiffeid;
import lombok.NonNull;
import lombok.Value;
import lombok.val;
import org.apache.commons.lang3.StringUtils;
import spiffe.result.Result;
import java.net.URI;
import java.util.Arrays;
@ -29,70 +29,51 @@ public class SpiffeId {
}
/**
* Returns an instance of a SpiffeId, containing the TrustDomain and
* Returns an instance representing a SPIFFE ID, containing the trust domain and
* a path generated joining the segments (e.g. /path1/path2).
*
* @param trustDomain an instance of a TrustDomain
* @param segments a list of string path segments
* @param trustDomain an instance of a {@link TrustDomain}
* @param segments a list of string path segments
*
* @return a {@code Resul}, either an {@link spiffe.result.Ok} wrapping a {@link SpiffeId}
* or an {@link spiffe.result.Error} wrapping the error message.
* @return a {@link SpiffeId}
*/
public static Result<SpiffeId, String> of(final TrustDomain trustDomain, final String... segments) {
if (trustDomain == null) {
return Result.error("Trust Domain cannot be null");
}
public static SpiffeId of(@NonNull final TrustDomain trustDomain, final String... segments) {
val path = Arrays.stream(segments)
.filter(StringUtils::isNotBlank)
.map(SpiffeId::normalize)
.map(s -> "/" + s)
.collect(Collectors.joining());
return Result.ok(new SpiffeId(trustDomain, path));
return new SpiffeId(trustDomain, path);
}
/**
* Parses a SpiffeId from a string (e.g. spiffe://example.org/test).
* Parses a SPIFFE ID from a string (e.g. spiffe://example.org/test).
*
* @param spiffeIdAsString a String representing a spiffeId
* @return A {@link Result}, either an {@link spiffe.result.Ok} wrapping a {@link SpiffeId}
* or an {@link spiffe.result.Error} wrapping the error message.
* @param spiffeIdAsString a String representing a SPIFFE ID
* @return A {@link SpiffeId}
* @throws IllegalArgumentException if the given string cannot be parsed
*/
public static Result<SpiffeId, String> parse(final String spiffeIdAsString) {
public static SpiffeId parse(@NonNull final String spiffeIdAsString) {
if (StringUtils.isBlank(spiffeIdAsString)) {
return Result.error("SPIFFE ID cannot be empty");
throw new IllegalArgumentException("SPIFFE ID cannot be empty");
}
try {
val uri = URI.create(spiffeIdAsString);
val uri = URI.create(spiffeIdAsString);
if (!SPIFFE_SCHEMA.equals(uri.getScheme())) {
return Result.error("Invalid SPIFFE schema");
}
val trustDomainResult = TrustDomain.of(uri.getHost());
if (trustDomainResult.isError()) {
return Result.error(trustDomainResult.getError());
}
val path = uri.getPath();
return Result.ok(new SpiffeId(trustDomainResult.getValue(), path));
} catch (IllegalArgumentException e) {
return Result.error("Could not parse SPIFFE ID %s: %s", spiffeIdAsString, e.getMessage());
if (!SPIFFE_SCHEMA.equals(uri.getScheme())) {
throw new IllegalArgumentException("Invalid SPIFFE schema");
}
val trustDomain = TrustDomain.of(uri.getHost());
val path = uri.getPath();
return new SpiffeId(trustDomain, path);
}
/**
* Returns true if the trustDomain of the current object equals the
* trustDomain passed as parameter.
* Returns true if the trust domain of this SPIFFE ID is the same as the given trust domain.
*
* @param trustDomain instance of a TrustDomain
* @return true if the trustDomain given as a parameter is the same as the trustDomain
* of the current SpiffeId object.
* @param trustDomain instance of a {@link TrustDomain}
* @return <code>true</code> if the given trust domain equals the trust domain of this object, <code>false</code> otherwise
*/
public boolean memberOf(final TrustDomain trustDomain) {
return this.trustDomain.equals(trustDomain);

View File

@ -1,14 +1,15 @@
package spiffe.spiffeid;
import lombok.val;
import org.apache.commons.lang3.NotImplementedException;
import spiffe.result.Result;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.Security;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.apache.commons.lang3.StringUtils.isBlank;
@ -20,18 +21,17 @@ public class SpiffeIdUtils {
private static final char DEFAULT_CHAR_SEPARATOR = ',';
/**
* Reads the Accepted SPIFFE IDs from a System Property and parse them to SpiffeId instances.
* Reads the Accepted SPIFFE IDs from a system property and parses them to {@link SpiffeId} instances.
*
* @param systemProperty name of the System Property that should contain a list of
* SPIFFE IDs separated by a comma.
* @param systemProperty name of the system property that contains a list of SPIFFE IDs separated by a commas.
* @return a list of {@link SpiffeId} parsed from the values read from the security property
*
* @return a {@link Result}
* {@link spiffe.result.Ok} containing a List of SpiffeId instances. If no value is found, returns an empty list.
* {@link spiffe.result.Error} in case the param systemProperty is blank.
* @throws IllegalArgumentException if the given system property is empty or if any of the SPIFFE IDs
* cannot be parsed
*/
public static Result<List<SpiffeId>, String> getSpiffeIdsFromSystemProperty(final String systemProperty) {
public static List<SpiffeId> getSpiffeIdsFromSystemProperty(final String systemProperty) {
if (isBlank(systemProperty)) {
return Result.error("System property cannot be empty.");
throw new IllegalArgumentException("Argument systemProperty cannot be empty");
}
val spiffeIds = System.getProperty(systemProperty);
@ -39,54 +39,60 @@ public class SpiffeIdUtils {
}
/**
* Read the Accepted SPIFFE IDs from a Security Property (defined in java.security file) and parse
* them to SpiffeId instances.
* <p>
* @param securityProperty name of the Security Property that should contain a list of
* SPIFFE IDs separated by a comma.
* Reads the accepted SPIFFE IDs from a security Property (defined in java.security file) and parses
* them to {@link SpiffeId} instances.
*
* @return a Result:
* {@link spiffe.result.Ok} containing a List of SpiffeId instances. If no value is found, returns an empty list.
* {@link spiffe.result.Error} in case the param systemProperty is blank.
* @param securityProperty name of the security property that contains a list of SPIFFE IDs separated by commas.
* @return a List of {@link SpiffeId} parsed from the values read from the given security property
*
* @throws IllegalArgumentException if the security property is empty or if any of the SPIFFE IDs
* cannot be parsed
*/
public static Result<List<SpiffeId>, String> getSpiffeIdsFromSecurityProperty(final String securityProperty) {
public static List<SpiffeId> getSpiffeIdsFromSecurityProperty(final String securityProperty) {
if (isBlank(securityProperty)) {
return Result.error("Security property cannot be empty");
throw new IllegalArgumentException("Argument securityProperty cannot be empty");
}
val spiffeIds = Security.getProperty(securityProperty);
return toListOfSpiffeIds(spiffeIds, DEFAULT_CHAR_SEPARATOR);
}
/**
* Read a file containing a list of SPIFFE IDs and parse them to SpiffeId instances.
* Reads a file containing a list of SPIFFE IDs and parses them to {@link SpiffeId} instances.
* <p>
* The file should have one SPIFFE ID per line.
*
* @param spiffeIdFile
* @param separator
* @return
* @param spiffeIdsFile the path to the file containing a list of SPIFFE IDs
* @return a List of {@link SpiffeId} parsed from the file provided
*
* @throws IOException if the given spiffeIdsFile cannot be read
* @throws IllegalArgumentException if any of the SPIFFE IDs in the file cannot be parsed
*/
public static Result<List<SpiffeId>, String> getSpiffeIdListFromFile(final Path spiffeIdFile, final char separator) {
throw new NotImplementedException("Not implemented");
public static List<SpiffeId> getSpiffeIdListFromFile(final Path spiffeIdsFile) throws IOException {
Stream<String> lines = Files.lines(spiffeIdsFile);
return lines
.map(SpiffeId::parse)
.collect(Collectors.toList());
}
/**
* Parse a string representing a list of SPIFFE IDs and return a Result containing a List of
* instances of SpiffeId.
* Parses a string representing a list of SPIFFE IDs and returns a list of
* instances of {@link SpiffeId}.
*
* @param spiffeIds a list of SPIFFE IDs represented in a string
* @param separator used to separate the SPIFFE IDs in the string.
* @return a Result containing a List of SpiffeId instances or an Error.
*
* @return a list of {@link SpiffeId} instances.
*
* @throws IllegalArgumentException is the string provided is blank
*/
public static Result<List<SpiffeId>, String> toListOfSpiffeIds(final String spiffeIds, final char separator) {
public static List<SpiffeId> toListOfSpiffeIds(final String spiffeIds, final char separator) {
if (isBlank(spiffeIds)) {
return Result.error("SPIFFE IDs is empty");
throw new IllegalArgumentException("Argument spiffeIds cannot be emtpy");
}
val array = spiffeIds.split(String.valueOf(separator));
val spiffeIdList = Arrays.stream(array)
return Arrays.stream(array)
.map(SpiffeId::parse)
.map(Result::getValue)
.collect(Collectors.toList());
return Result.ok(spiffeIdList);
}
}

View File

@ -1,10 +1,10 @@
package spiffe.spiffeid;
import lombok.NonNull;
import lombok.Value;
import lombok.val;
import org.apache.commons.lang3.StringUtils;
import spiffe.result.Result;
import java.net.URI;
import java.net.URISyntaxException;
@ -24,27 +24,30 @@ public class TrustDomain {
}
/**
* Creates an instance of a TrustDomain.
* Creates a trust domain.
*
* @param trustDomain a TrustDomain represented as a String, must not be blank.
* @return an Ok result containing the parsed TrustDomain, or an Error if the trustDomain cannot be parsed
* @param trustDomain a trust domain represented as a string, must not be blank.
* @return an instance of a {@link TrustDomain}
*
* @throws IllegalArgumentException if the given string is blank or cannot be parsed
*/
public static Result<TrustDomain, String> of(String trustDomain) {
public static TrustDomain of(@NonNull String trustDomain) {
if (StringUtils.isBlank(trustDomain)) {
return Result.error("Trust Domain cannot be empty.");
throw new IllegalArgumentException("Trust Domain cannot be empty");
}
try {
val uri = new URI(normalize(trustDomain));
val result = new TrustDomain(getHost(uri));
return Result.ok(result);
val host = getHost(uri);
return new TrustDomain(host);
} catch (URISyntaxException e) {
return Result.error(format("Unable to parse: %s.", trustDomain));
throw new IllegalArgumentException(format("Unable to parse: %s", trustDomain), e);
}
}
/**
* Returns the trustDomain as String
* @return a String with the Trust Domain
* Returns the trust domain as a string.
*
* @return a String with the trust domain
*/
@Override
public String toString() {

View File

@ -4,7 +4,6 @@ import lombok.NonNull;
import lombok.Value;
import org.apache.commons.lang3.NotImplementedException;
import spiffe.bundle.jwtbundle.JwtBundleSource;
import spiffe.result.Result;
import spiffe.spiffeid.SpiffeId;
import java.time.LocalDateTime;
@ -47,20 +46,22 @@ public class JwtSvid {
* Parses and validates a JWT-SVID token and returns the
* JWT-SVID. The JWT-SVID signature is verified using the JWT bundle source.
*
* @param token a token as a String
* @param jwtBundleSource an implementation of a JwtBundleSource
* @param token a token as a string
* @param jwtBundleSource an implementation of a {@link JwtBundleSource}
* @param audience the audience as a String
* @return a JwtSvid or Error
* @return an instance of a {@link JwtSvid}
*
* @throws //TODO: declare thrown exceptions
*/
public Result<JwtSvid, String> parseAndValidate(@NonNull final String token, @NonNull final JwtBundleSource jwtBundleSource, String... audience) {
public JwtSvid parseAndValidate(@NonNull final String token, @NonNull final JwtBundleSource jwtBundleSource, String... audience) {
throw new NotImplementedException("Not implemented");
}
/**
* Returns the JWT-SVID marshaled to a string. The returned value is
* the same token value originally passed to ParseAndValidate.
* the same token value originally passed to parseAndValidate.
*
* @return
* @return the token
*/
public String marshall() {
return token;

View File

@ -1,6 +1,5 @@
package spiffe.svid.jwtsvid;
import spiffe.result.Result;
import spiffe.spiffeid.SpiffeId;
/**
@ -11,10 +10,12 @@ public interface JwtSvidSource {
/**
* Fetches a JWT-SVID from the source with the given parameters
*
* @param subject a SpiffeId
* @param subject a {@link SpiffeId}
* @param audience the audience
* @param extraAudiences an array of Strings
* @return a JwtSvid
* @return a {@link JwtSvid}
*
* @throws //TODO: declare thrown exceptions
*/
Result<JwtSvid, String> FetchJwtSvid(SpiffeId subject, String audience, String... extraAudiences);
JwtSvid FetchJwtSvid(SpiffeId subject, String audience, String... extraAudiences);
}

View File

@ -3,31 +3,35 @@ package spiffe.svid.x509svid;
import lombok.NonNull;
import lombok.Value;
import lombok.val;
import spiffe.exception.X509SvidException;
import spiffe.internal.CertificateUtils;
import spiffe.result.Result;
import spiffe.spiffeid.SpiffeId;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.util.List;
/**
* A <code>X509Svid</code> represents a SPIFFE X509-SVID.
* A <code>X509Svid</code> represents a SPIFFE X509 SVID.
* <p>
* Contains a SPIFFE ID, a PrivateKey and a chain of X509Certificate.
* Contains a SPIFFE ID, a private key and a chain of X509 certificates.
*/
@Value
public class X509Svid {
SpiffeId spiffeId;
// The X.509 certificates of the X509-SVID. The leaf certificate is
// the X509-SVID certificate. Any remaining certificates (if any)
// chain the X509-SVID certificate back to a X509 root
// for the trust domain.
/**
* The X.509 certificates of the X509-SVID. The leaf certificate is
* the X509-SVID certificate. Any remaining certificates (if any) chain
* the X509-SVID certificate back to a X509 root for the trust domain.
*/
List<X509Certificate> chain;
PrivateKey privateKey;
@ -42,63 +46,58 @@ public class X509Svid {
}
/**
* Loads the X509-SVID from PEM encoded files on disk.
* Loads the X509 SVID from PEM encoded files on disk.
*
* @param certsFile path to x509 certificate chain file
* @param privateKeyFile path to PrivateKey file
* @return an instance of X509Svid
* @param certsFilePath path to X509 certificate chain file
* @param privateKeyFilePath path to private key file
* @return an instance of {@link X509Svid}
*
* @throws X509SvidException if there is an error parsing the given certsFilePath or the privateKeyFilePath
*/
public static Result<X509Svid, String> load(@NonNull Path certsFile, @NonNull Path privateKeyFile) {
public static X509Svid load(@NonNull Path certsFilePath, @NonNull Path privateKeyFilePath) throws X509SvidException {
byte[] certsBytes;
byte[] privateKeyBytes;
try {
val certsBytes = Files.readAllBytes(certsFile);
byte[] privateKeyBytes = Files.readAllBytes(privateKeyFile);
return createX509Svid(certsBytes, privateKeyBytes);
certsBytes = Files.readAllBytes(certsFilePath);
privateKeyBytes = Files.readAllBytes(privateKeyFilePath);
} catch (IOException e) {
return Result.error("Error loading X509-SVID from certsFile %s and privateKeyFile %s: %s", certsFile, privateKeyFile, e.getMessage());
throw new X509SvidException(String.format("Could not load X509Svid from certsFilePath %s and privateKeyFilePath %s", certsFilePath, privateKeyFilePath), e);
}
}
/**
* Parses the X509-SVID from PEM or DER blocks containing certificate chain and key
* bytes. The key must be a PEM or DER block with PKCS#8.
*
* @param certsBytes chain of certificates as a byte array
* @param privateKeyBytes private key byte array
* @return a Result(Success) object containing the X509-SVID, or a Error containing the Exception cause
*/
public static Result<X509Svid, String> parse(@NonNull byte[] certsBytes, @NonNull byte[] privateKeyBytes) {
return createX509Svid(certsBytes, privateKeyBytes);
}
/** Return the chain of certificates as an array. */
/**
* Parses the X509 SVID from PEM or DER blocks containing certificate chain and key
* bytes. The key must be a PEM or DER block with PKCS#8.
*
* @param certsBytes chain of certificates as a byte array
* @param privateKeyBytes private key as byte array
* @return a {@link X509Svid} parsed from the given certBytes and privateKeyBytes
*
* @throws X509SvidException if the given certsBytes or privateKeyBytes cannot be parsed
*/
public static X509Svid parse(@NonNull byte[] certsBytes, @NonNull byte[] privateKeyBytes) throws X509SvidException {
return createX509Svid(certsBytes, privateKeyBytes);
}
/**
* Return the chain of certificates as an array.
*/
public X509Certificate[] getChainArray() {
return chain.toArray(new X509Certificate[0]);
}
private static Result<X509Svid, String> createX509Svid(byte[] certsBytes, byte[] privateKeyBytes) {
val x509Certificates = CertificateUtils.generateCertificates(certsBytes);
if (x509Certificates.isError()) {
return Result.error(x509Certificates.getError());
private static X509Svid createX509Svid(byte[] certsBytes, byte[] privateKeyBytes) throws X509SvidException {
List<X509Certificate> x509Certificates = null;
try {
x509Certificates = CertificateUtils.generateCertificates(certsBytes);
val privateKey = CertificateUtils.generatePrivateKey(privateKeyBytes);
val spiffeId = CertificateUtils.getSpiffeId(x509Certificates.get(0));
return new X509Svid(spiffeId, x509Certificates, privateKey);
} catch (CertificateException e) {
throw new X509SvidException("X509 SVID could not be parsed from cert bytes", e);
} catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
throw new X509SvidException("X509 SVID Private Key could not be parsed from privateKeyBytes", e);
}
val privateKey = CertificateUtils.generatePrivateKey(privateKeyBytes);
if (privateKey.isError()) {
return Result.error(privateKey.getError());
}
val spiffeId =
CertificateUtils
.getSpiffeId(x509Certificates.getValue().get(0));
if (spiffeId.isError()) {
return Result.error("Error creating X509-SVID: %s", spiffeId.getError());
}
val x509Svid = new X509Svid(
spiffeId.getValue(),
x509Certificates.getValue(),
privateKey.getValue());
return Result.ok(x509Svid);
}
}

View File

@ -1,10 +1,14 @@
package spiffe.svid.x509svid;
import spiffe.result.Result;
/**
* A <code>X509SvidSource</code> represents a source of X509-SVIDs.
* A <code>X509SvidSource</code> represents a source of X509 SVIDs.
*/
public interface X509SvidSource {
Result<X509Svid, String> getX509Svid();
/**
* Returns the X509 SVID in the source.
*
* @return an instance of a {@link X509Svid}
*/
X509Svid getX509Svid();
}

View File

@ -3,10 +3,14 @@ package spiffe.svid.x509svid;
import lombok.NonNull;
import lombok.val;
import spiffe.bundle.x509bundle.X509BundleSource;
import spiffe.exception.BundleNotFoundException;
import spiffe.internal.CertificateUtils;
import spiffe.result.Result;
import spiffe.spiffeid.SpiffeId;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertPathValidatorException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
@ -14,60 +18,48 @@ import java.util.function.Supplier;
/**
* A <code>X509SvidValidator</code> provides methods to validate
* a chain of X509 Certificates using an X509BundleSource.
* a chain of X509 certificates using an X509 bundle source.
*/
public class X509SvidValidator {
/**
* Verifies that a chain of certificate can be chained to one trustedCert in the x509BundleSource.
* Verifies that a chain of certificates can be chained to one authority in the given X509 bundle source.
*
* @param chain a chain of X509 Certificates to be validated
* @param x509BundleSource a {@link X509BundleSource }to provide the trusted bundle certs
* @param chain a list representing the chain of X509 certificates to be validated
* @param x509BundleSource a {@link X509BundleSource } to provide the authorities
*
* @return a Result object conveying the result of the verification. If the chain can be verified with
* a trusted bundle, it returns an Ok(true), otherwise returns an Error with a String message.
* @throws CertificateException is the chain cannot be verified with an authority from the X509 bundle source
* @throws NullPointerException if the given chain or 509BundleSource are null
*/
public static Result<Boolean, String> verifyChain(
public static void verifyChain(
@NonNull List<X509Certificate> chain,
@NonNull X509BundleSource x509BundleSource) {
val trustDomain = CertificateUtils.getTrustDomain(chain);
if (trustDomain.isError()) {
return Result.error(trustDomain.getError());
@NonNull X509BundleSource x509BundleSource) throws CertificateException {
try {
val trustDomain = CertificateUtils.getTrustDomain(chain);
val x509Bundle = x509BundleSource.getX509BundleForTrustDomain(trustDomain);
CertificateUtils.validate(chain, new ArrayList<>(x509Bundle.getX509Authorities()));
} catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException | CertPathValidatorException | BundleNotFoundException e) {
throw new CertificateException(e);
}
val x509Bundle = x509BundleSource.getX509BundleForTrustDomain(trustDomain.getValue());
if (x509Bundle.isError()) {
return Result.error("No X509 Bundle found for the Trust Domain %s", trustDomain.getValue());
}
return CertificateUtils.validate(chain, new ArrayList<>(x509Bundle.getValue().getX509Authorities()));
}
/**
* Checks that the Certificate provided has a SPIFFE ID that is in the list of acceptedSpiffeIds supplied.
* Checks that the X509 SVID provided has a SPIFFE ID that is in the list of accepted SPIFFE IDs supplied.
*
* @param spiffeId a SPIFFE ID to be verified
* @param acceptedSpiffedIdsSupplier a Supplier of a List os SPIFFE IDs that are accepted
* @param x509Certificate a {@link X509Svid} with a SPIFFE ID to be verified
* @param acceptedSpiffedIdsSupplier a {@link Supplier} of a list os SPIFFE IDs that are accepted
*
* @return an {@link spiffe.result.Ok} with true if the SPIFFE ID is in the list,
* an {@link spiffe.result.Error} containing en error message if the SPIFFE ID is not in the list
* or if there's en error getting the list.
* @throws CertificateException is the SPIFFE ID in x509Certificate is not in the list supplied by acceptedSpiffedIdsSupplier,
* or if the SPIFFE ID cannot be parsed from the x509Certificate
* @throws NullPointerException if the given x509Certificate or acceptedSpiffedIdsSupplier are null
*/
public static Result<Boolean, String> verifySpiffeId(SpiffeId spiffeId, Supplier<Result<List<SpiffeId>, String>> acceptedSpiffedIdsSupplier) {
if (acceptedSpiffedIdsSupplier.get().isError()) {
return Result.error("Error getting list of accepted SPIFFE IDs");
}
public static void verifySpiffeId(@NonNull X509Certificate x509Certificate,
@NonNull Supplier<List<SpiffeId>> acceptedSpiffedIdsSupplier)
throws CertificateException {
val spiffeIdList = acceptedSpiffedIdsSupplier.get();
if (spiffeIdList.isError()) {
return Result.error(spiffeIdList.getError());
val spiffeId = CertificateUtils.getSpiffeId(x509Certificate);
if (!spiffeIdList.contains(spiffeId)) {
throw new CertificateException(String.format("SPIFFE ID %s in x509Certificate is not accepted", spiffeId));
}
if (spiffeIdList.getValue().contains(spiffeId)) {
return Result.ok(true);
}
return Result.error("SPIFFE ID '%s' is not accepted", spiffeId);
}
}

View File

@ -1,15 +1,18 @@
package spiffe.workloadapi;
import lombok.val;
import org.apache.commons.lang3.StringUtils;
import spiffe.result.Result;
import spiffe.exception.SocketEndpointAddressException;
import java.net.InetAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.List;
/**
* Utility class to get the default Workload api address and parse string addresses.
* Utility class to get the default Workload API address and parse string addresses.
*/
public class Address {
@ -18,88 +21,128 @@ public class Address {
*/
public static final String SOCKET_ENV_VARIABLE = "SPIFFE_ENDPOINT_SOCKET";
private static final List<String> VALID_SCHEMES = Arrays.asList("unix", "tcp");
/**
* Returns the default Workload API address hold by the system environment variable
* defined by SOCKET_ENV_VARIABLE
*/
public static String getDefaultAddress() {
return System.getenv(Address.SOCKET_ENV_VARIABLE);
}
public static Result<URI, String> parseAddress(String addr) {
/**
* Parses and validates a Workload API socket address.
* <p>
* The given address should either have a tcp, or a unix scheme.
* <p>
* The given address should contain a path.
* <p>
* The given address cannot be opaque, cannot have fragments, query values or user info.
* <p>
* If the given address is tcp, it should contain an IP and a port.
*
* @param address the Workload API socket address as a string
* @return an instance of a {@link URI}
*
* @throws SocketEndpointAddressException if the address could not be parsed or if it is not valid
*/
public static URI parseAddress(String address) throws SocketEndpointAddressException {
URI parsedAddress;
try {
parsedAddress = new URI(addr);
parsedAddress = new URI(address);
} catch (URISyntaxException e) {
return Result.error("Workload endpoint socket is not a valid URI: %s", e.getMessage());
throw new SocketEndpointAddressException(String.format("Workload endpoint socket is not a valid URI: %s", address), e);
}
if (parsedAddress.getScheme() == null) {
return Result.error("Workload endpoint socket URI must have a tcp:// or unix:// scheme");
val scheme = parsedAddress.getScheme();
if (!isValid(scheme)) {
throw new SocketEndpointAddressException(String.format("Workload endpoint socket URI must have a tcp:// or unix:// scheme: %s", address));
}
switch (parsedAddress.getScheme()) {
String error = null;
switch (scheme) {
case "unix": {
if (parsedAddress.isOpaque() && parsedAddress.isAbsolute()) {
return Result.error("Workload endpoint unix socket URI must not be opaque");
error = "Workload endpoint unix socket URI must not be opaque: %s";
break;
}
if (StringUtils.isNotBlank(parsedAddress.getUserInfo())) {
return Result.error("Workload endpoint unix socket URI must not include user info");
error = "Workload endpoint unix socket URI must not include user info: %s";
break;
}
if (StringUtils.isBlank(parsedAddress.getHost()) && StringUtils.isBlank(parsedAddress.getPath())) {
return Result.error("Workload endpoint unix socket URI must include a path");
error = "Workload endpoint unix socket URI must include a path: %s";
break;
}
if (StringUtils.isNotBlank(parsedAddress.getRawQuery())) {
return Result.error("Workload endpoint unix socket URI must not include query values");
error = "Workload endpoint unix socket URI must not include query values: %s";
break;
}
if (StringUtils.isNotBlank(parsedAddress.getFragment())) {
return Result.error("Workload endpoint unix socket URI must not include a fragment");
error = "Workload endpoint unix socket URI must not include a fragment: %s";
}
return Result.ok(parsedAddress);
break;
}
case "tcp": {
if (parsedAddress.isOpaque() && parsedAddress.isAbsolute()) {
return Result.error("Workload endpoint tcp socket URI must not be opaque");
error = "Workload endpoint tcp socket URI must not be opaque: %s";
break;
}
if (StringUtils.isNotBlank(parsedAddress.getUserInfo())) {
return Result.error("Workload endpoint tcp socket URI must not include user info");
error = "Workload endpoint tcp socket URI must not include user info: %s";
break;
}
if (StringUtils.isBlank(parsedAddress.getHost())) {
return Result.error("Workload endpoint tcp socket URI must include a host");
error = "Workload endpoint tcp socket URI must include a host: %s";
break;
}
if (StringUtils.isNotBlank(parsedAddress.getPath())) {
return Result.error("Workload endpoint tcp socket URI must not include a path");
error = "Workload endpoint tcp socket URI must not include a path: %s";
break;
}
if (StringUtils.isNotBlank(parsedAddress.getRawQuery())) {
return Result.error("Workload endpoint tcp socket URI must not include query values");
error = "Workload endpoint tcp socket URI must not include query values: %s";
break;
}
if (StringUtils.isNotBlank(parsedAddress.getFragment())) {
return Result.error("Workload endpoint tcp socket URI must not include a fragment");
error = "Workload endpoint tcp socket URI must not include a fragment: %s";
break;
}
String ip = parseIp(parsedAddress.getHost());
if (ip == null) {
return Result.error("Workload endpoint tcp socket URI host component must be an IP:port");
if (StringUtils.isBlank(ip)) {
error = "Workload endpoint tcp socket URI host component must be an IP:port: %s";
break;
}
int port = parsedAddress.getPort();
if (port == -1) {
return Result.error("Workload endpoint tcp socket URI host component must include a port");
error = "Workload endpoint tcp socket URI host component must include a port: %s";
}
return Result.ok(parsedAddress);
break;
}
}
return Result.error("Workload endpoint socket URI must have a tcp:// or unix:// scheme");
if (StringUtils.isNotBlank(error)) {
throw new SocketEndpointAddressException(String.format(error, address));
}
return parsedAddress;
}
private static boolean isValid(String scheme) {
return (StringUtils.isNotBlank(scheme) && VALID_SCHEMES.contains(scheme));
}
private static String parseIp(String host) {

View File

@ -3,37 +3,35 @@ package spiffe.workloadapi;
import org.apache.commons.lang3.NotImplementedException;
import spiffe.bundle.jwtbundle.JwtBundle;
import spiffe.bundle.jwtbundle.JwtBundleSource;
import spiffe.result.Result;
import spiffe.spiffeid.SpiffeId;
import spiffe.spiffeid.TrustDomain;
import spiffe.svid.jwtsvid.JwtSvid;
import spiffe.svid.jwtsvid.JwtSvidSource;
/**
* A <code>JwtSource</code> represents a source of SPIFFE JWT-SVID and JWT bundles
* A <code>JwtSource</code> represents a source of SPIFFE JWT SVID and JWT bundles
* maintained via the Workload API.
*/
public class JwtSource implements JwtSvidSource, JwtBundleSource {
/**
* Creates a new JWTSource. It blocks until the initial update
* Creates a new JWT source. It blocks until the initial update
* has been received from the Workload API.
*
* @param spiffeSocketPath a Path to the Workload API endpoint
* @return a Result containing an instance of a JwtSource, or an Error with an
* Exception.
* @param spiffeSocketPath a path to the Workload API endpoint
* @return an instance of a {@link JwtSource}
*/
public static Result<JwtSource, String> newSource(String spiffeSocketPath) {
public static JwtSource newSource(String spiffeSocketPath) {
throw new NotImplementedException("Not implemented");
}
@Override
public Result<JwtBundle, String> getJwtBundleForTrustDomain(TrustDomain trustDomain) {
public JwtBundle getJwtBundleForTrustDomain(TrustDomain trustDomain) {
throw new NotImplementedException("Not implemented");
}
@Override
public Result<JwtSvid, String> FetchJwtSvid(SpiffeId subject, String audience, String... extraAudiences) {
public JwtSvid FetchJwtSvid(SpiffeId subject, String audience, String... extraAudiences) {
throw new NotImplementedException("Not implemented");
}
}

View File

@ -1,7 +1,5 @@
package spiffe.workloadapi;
import spiffe.result.Error;
/**
* a <code>Watcher</code> handles updates of type T.
*
@ -11,5 +9,5 @@ public interface Watcher<T> {
void OnUpdate(final T update);
void OnError(final Error<T, String> t);
void OnError(final Throwable e);
}

View File

@ -10,9 +10,10 @@ import lombok.extern.java.Log;
import lombok.val;
import org.apache.commons.lang3.NotImplementedException;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import spiffe.bundle.jwtbundle.JwtBundleSet;
import spiffe.result.Result;
import spiffe.exception.SocketEndpointAddressException;
import spiffe.exception.X509ContextException;
import spiffe.exception.X509SvidException;
import spiffe.spiffeid.SpiffeId;
import spiffe.svid.jwtsvid.JwtSvid;
import spiffe.workloadapi.internal.GrpcConversionUtils;
@ -23,7 +24,7 @@ import spiffe.workloadapi.internal.SpiffeWorkloadAPIGrpc.SpiffeWorkloadAPIBlocki
import spiffe.workloadapi.internal.SpiffeWorkloadAPIGrpc.SpiffeWorkloadAPIStub;
import java.io.Closeable;
import java.net.URI;
import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
@ -34,10 +35,7 @@ import static spiffe.workloadapi.internal.Workload.X509SVIDResponse;
/**
* A <code>WorkloadApiClient</code> represents a client to interact with the Workload API.
* Supports one-shot calls and watch updates for X509 and JWT SVIDS and Bundles.
* <p>
* Multiple WorkloadApiClients can be created for the same SPIFFE Socket Path,
* they will share a common ManagedChannel.
* Supports one-shot calls and watch updates for X509 and JWT SVIDS and bundles.
*/
@Log
public class WorkloadApiClient implements Closeable {
@ -55,26 +53,25 @@ public class WorkloadApiClient implements Closeable {
}
/**
* Creates a new WorkloadAPI client using the Default Address from the Environment Variable
* Creates a new Workload API client using the default socket endpoint address.
* @see Address#getDefaultAddress()
*
* @return a Result containing a WorklaodAPI client
* @return a {@link WorkloadApiClient}
*/
public static Result<WorkloadApiClient, String> newClient() {
public static WorkloadApiClient newClient() throws SocketEndpointAddressException {
val options = ClientOptions.builder().build();
return newClient(options);
}
/**
* Creates a new Workload API Client.
* Creates a new Workload API client.
* <p>
* If the SPIFFE Socket Path is not provided, it uses the Default Address from
* the Environment variable for creating the client.
* If the SPIFFE socket endpoint address is not provided in the options, it uses the default address.
*
* @param options {@link ClientOptions}
* @return a Result containing a WorklaodAPI client
* @return a {@link WorkloadApiClient}
*/
public static Result<WorkloadApiClient, String> newClient(@NonNull ClientOptions options) {
public static WorkloadApiClient newClient(@NonNull ClientOptions options) throws SocketEndpointAddressException {
String spiffeSocketPath;
if (StringUtils.isNotBlank(options.spiffeSocketPath)) {
spiffeSocketPath = options.spiffeSocketPath;
@ -82,13 +79,8 @@ public class WorkloadApiClient implements Closeable {
spiffeSocketPath = Address.getDefaultAddress();
}
val parseResult = Address.parseAddress(spiffeSocketPath);
if (parseResult.isError()) {
return Result.error(parseResult.getError());
}
URI parsedAddress = parseResult.getValue();
val managedChannel = GrpcManagedChannelFactory.newChannel(parsedAddress);
val socketEndpointAddress = Address.parseAddress(spiffeSocketPath);
val managedChannel = GrpcManagedChannelFactory.newChannel(socketEndpointAddress);
val workloadAPIAsyncStub = SpiffeWorkloadAPIGrpc
.newStub(managedChannel)
@ -98,64 +90,54 @@ public class WorkloadApiClient implements Closeable {
.newBlockingStub(managedChannel)
.withInterceptors(new SecurityHeaderInterceptor());
val workloadApiClient = new WorkloadApiClient(workloadAPIAsyncStub, workloadAPIBlockingStub, managedChannel);
return Result.ok(workloadApiClient);
return new WorkloadApiClient(workloadAPIAsyncStub, workloadAPIBlockingStub, managedChannel);
}
/**
* One-shot fetch call to get an X509 Context (SPIFFE X509-SVID and Bundles).
* One-shot blocking fetch call to get an X509 context.
*
* @throws X509ContextException if there is an error fetching or processing the X509 context
*/
public Result<X509Context, String> fetchX509Context() {
public X509Context fetchX509Context() {
Context.CancellableContext cancellableContext;
cancellableContext = Context.current().withCancellation();
Result<X509Context, String> result;
X509Context result;
try {
result = cancellableContext.call(this::processX509Context);
} catch (Exception e) {
return Result.error("Error fetching X509Context: %s %n %s", e.getMessage(), ExceptionUtils.getStackTrace(e));
throw new X509ContextException("Error fetching X509Context", e);
}
// close connection
cancellableContext.close();
return result;
}
private Result<X509Context, String> processX509Context() {
try {
Iterator<X509SVIDResponse> x509SVIDResponse = workloadApiBlockingStub.fetchX509SVID(newX509SvidRequest());
if (x509SVIDResponse.hasNext()) {
return GrpcConversionUtils.toX509Context(x509SVIDResponse.next());
}
} catch (Exception e) {
return Result.error("Error processing X509Context: %s %n %s", e.getMessage(), ExceptionUtils.getStackTrace(e));
}
return Result.error("Could not get X509Context");
}
/**
* Watches for updates to the X509 Context.
* Watches for X509 context updates.
*
* @param watcher receives the update X509 context.
* @param watcher an instance that implements a {@link Watcher}.
*/
public void watchX509Context(Watcher<X509Context> watcher) {
StreamObserver<X509SVIDResponse> streamObserver = new StreamObserver<X509SVIDResponse>() {
@Override
public void onNext(X509SVIDResponse value) {
Result<X509Context, String> x509Context = GrpcConversionUtils.toX509Context(value);
if (x509Context.isError()) {
watcher.OnError(Result.error(x509Context.getError()));
X509Context x509Context = null;
try {
x509Context = GrpcConversionUtils.toX509Context(value);
} catch (CertificateException | X509SvidException e) {
watcher.OnError(new X509ContextException("Error processing X509 Context update", e));
}
watcher.OnUpdate(x509Context.getValue());
watcher.OnUpdate(x509Context);
}
@Override
public void onError(Throwable t) {
String error = String.format("Error getting X509Context update: %s %n %s", t.getMessage(), ExceptionUtils.getStackTrace(t));
watcher.OnError(Result.error(error));
watcher.OnError(new X509ContextException("Error getting X509Context", t));
}
@Override
public void onCompleted() {
watcher.OnError(Result.error("Unexpected completed stream."));
watcher.OnError(new X509ContextException("Unexpected completed stream"));
}
};
Context.CancellableContext cancellableContext;
@ -170,20 +152,22 @@ public class WorkloadApiClient implements Closeable {
* @param subject a SPIFFE ID
* @param audience the audience of the JWT-SVID
* @param extraAudience the extra audience for the JWT_SVID
* @return an {@link spiffe.result.Ok} containing the JWT SVID, or an {@link spiffe.result.Error}
* if the JwtSvid could not be fetched.
*
* @return an instance of a {@link JwtSvid}
*
* @throws //TODO: declare thrown exceptions
*/
public Result<JwtSvid, String> fetchJwtSvid(SpiffeId subject, String audience, String... extraAudience) {
public JwtSvid fetchJwtSvid(SpiffeId subject, String audience, String... extraAudience) {
throw new NotImplementedException("Not implemented");
}
/**
* Fetches the JWT bundles for JWT-SVID validation, keyed
* by a SPIFFE ID of the trust domain to which they belong.
* Fetches the JWT bundles for JWT-SVID validation, keyed by trust domain.
*
* @return an {@link spiffe.result.Ok} containing the JwtBundleSet.
* @return an instance of a {@link JwtBundleSet}
* @throws //TODO: declare thrown exceptions
*/
public Result<JwtBundleSet, String> fetchJwtBundles() {
public JwtBundleSet fetchJwtBundles() {
throw new NotImplementedException("Not implemented");
}
@ -193,14 +177,16 @@ public class WorkloadApiClient implements Closeable {
*
* @param token JWT token
* @param audience audience of the JWT
* @return the JwtSvid if the token and audience could be validated.
* @return the {@link JwtSvid} if the token and audience could be validated.
*
* @throws //TODO: declare thrown exceptions
*/
public Result<JwtSvid, String> validateJwtSvid(String token, String audience) {
public JwtSvid validateJwtSvid(String token, String audience) {
throw new NotImplementedException("Not implemented");
}
/**
* Watches for updates to the JWT Bundles.
* Watches for JWT bundles updates.
*
* @param jwtBundlesWatcher receives the update for JwtBundles.
*/
@ -208,10 +194,9 @@ public class WorkloadApiClient implements Closeable {
throw new NotImplementedException("Not implemented");
}
private X509SVIDRequest newX509SvidRequest() {
return X509SVIDRequest.newBuilder().build();
}
/**
* Closes this Workload API closing the underlying channel and cancelling the contexts.
*/
@Override
public void close() {
log.info("Closing WorkloadAPI client");
@ -224,6 +209,22 @@ public class WorkloadApiClient implements Closeable {
}
}
private X509SVIDRequest newX509SvidRequest() {
return X509SVIDRequest.newBuilder().build();
}
private X509Context processX509Context() {
try {
Iterator<X509SVIDResponse> x509SVIDResponse = workloadApiBlockingStub.fetchX509SVID(newX509SvidRequest());
if (x509SVIDResponse.hasNext()) {
return GrpcConversionUtils.toX509Context(x509SVIDResponse.next());
}
} catch (Exception e) {
throw new X509ContextException("Error processing X509Context", e);
}
throw new X509ContextException("Error processing X509Context: x509SVIDResponse is empty");
}
/**
* Options for creating a new {@link WorkloadApiClient}.
*/

View File

@ -10,7 +10,7 @@ import java.util.List;
/**
* A <code>X509Context</code> represents the X509 materials that are fetched from the Workload API.
* <p>
* Contains a List of {@link X509Svid} and a {@link X509BundleSet}.
* Contains a list of {@link X509Svid} and a {@link X509BundleSet}.
*/
@Value
public class X509Context {

View File

@ -5,11 +5,13 @@ import lombok.Data;
import lombok.NonNull;
import lombok.extern.java.Log;
import lombok.val;
import org.apache.commons.lang3.exception.ExceptionUtils;
import spiffe.bundle.x509bundle.X509Bundle;
import spiffe.bundle.x509bundle.X509BundleSet;
import spiffe.bundle.x509bundle.X509BundleSource;
import spiffe.result.Error;
import spiffe.result.Result;
import spiffe.exception.BundleNotFoundException;
import spiffe.exception.SocketEndpointAddressException;
import spiffe.exception.X509SourceException;
import spiffe.spiffeid.TrustDomain;
import spiffe.svid.x509svid.X509Svid;
import spiffe.svid.x509svid.X509SvidSource;
@ -20,13 +22,15 @@ import java.util.function.Function;
import java.util.logging.Level;
/**
* A <code>X509Source</code> represents a source of X509-SVID and X509 Bundles maintained via the
* A <code>X509Source</code> represents a source of X509 SVIDs and X509 bundles maintained via the
* Workload API.
* <p>
* It handles a {@link X509Svid} and a {@link X509BundleSet} that are updated automatically
* whenever there is an update from the Workload API.
* <p>
* It implements the Closeable interface. The {@link #close()} method closes the source,
* Implements {@link X509SvidSource} and {@link X509BundleSource}.
* <p>
* Implements the {@link Closeable} interface. The {@link #close()} method closes the source,
* dropping the connection to the Workload API. Other source methods will return an error
* after close has been called.
*/
@ -41,94 +45,81 @@ public class X509Source implements X509SvidSource, X509BundleSource, Closeable {
private volatile boolean closed;
/**
* Creates a new X509Source. It blocks until the initial update
* Creates a new X509 source. It blocks until the initial update
* has been received from the Workload API.
* <p>
* It uses the Default Address from the Environment variable to get the Workload API endpoint address.
* It uses the default address socket endpoint from the environment variable to get the Workload API address.
* <p>
* It uses the default X509-SVID.
* It uses the default X509 SVID.
*
* @return an initialized an {@link spiffe.result.Ok} with X509Source, or an {@link Error} in
* case the X509Source could not be initialized.
* @return an instance of {@link X509Source}, with the svid and bundles initialized
*
* @throws SocketEndpointAddressException if the address to the Workload API is not valid
* @throws X509SourceException if the source could not be initialized
*/
public static Result<X509Source, String> newSource() {
public static X509Source newSource() throws SocketEndpointAddressException {
X509SourceOptions x509SourceOptions = X509SourceOptions.builder().build();
return newSource(x509SourceOptions);
}
/**
* Creates a new X509Source. It blocks until the initial update
* Creates a new X509 source. It blocks until the initial update
* has been received from the Workload API.
* <p>
* The {@link WorkloadApiClient} can be provided in the options, if it is not,
* a new client is created.
*
* @param options {@link X509SourceOptions}
* @return an initialized an {@link spiffe.result.Ok} with X509Source, or an {@link Error} in
* case the X509Source could not be initialized.
* @return an instance of {@link X509Source}, with the svid and bundles initialized
*
* @throws SocketEndpointAddressException if the address to the Workload API is not valid
* @throws X509SourceException if the source could not be initialized
*/
public static Result<X509Source, String> newSource(@NonNull X509SourceOptions options) {
public static X509Source newSource(@NonNull X509SourceOptions options) throws SocketEndpointAddressException {
if (options.workloadApiClient == null) {
Result<WorkloadApiClient, String> workloadApiClient = createClient(options);
if (workloadApiClient.isError()) {
return Result.error(workloadApiClient.getError());
}
options.workloadApiClient = workloadApiClient.getValue();
options.workloadApiClient = createClient(options);
}
val x509Source = new X509Source();
x509Source.picker = options.picker;
x509Source.workloadApiClient = options.workloadApiClient;
Result<Boolean, String> init = x509Source.init();
if (init.isError()) {
try {
x509Source.init();
} catch (Exception e) {
x509Source.close();
return Result.error("Error creating X509 Source: %s", init.getError());
throw new X509SourceException("Error creating X509 source", e);
}
return Result.ok(x509Source);
}
private static Result<WorkloadApiClient, String> createClient(@NonNull X509Source.@NonNull X509SourceOptions options) {
Result<WorkloadApiClient, String> workloadApiClient;
val clientOptions= WorkloadApiClient.ClientOptions
.builder()
.spiffeSocketPath(options.spiffeSocketPath)
.build();
workloadApiClient = WorkloadApiClient.newClient(clientOptions);
return workloadApiClient;
}
private X509Source() {
return x509Source;
}
/**
* Returns the X509-SVID handled by this source, returns an Error in case
* the source is already closed.
* Returns the X509 SVID handled by this source.
*
* @return an {@link spiffe.result.Ok} containing the {@link X509Svid}
* @return a {@link X509Svid}
* @throws IllegalStateException if the source is closed
*/
@Override
public Result<X509Svid, String> getX509Svid() {
val checkClosed = checkClosed();
if (checkClosed.isError()) {
return Result.error(checkClosed.getError());
public X509Svid getX509Svid() {
if (isClosed()) {
throw new IllegalStateException("X509 SVID source is closed");
}
return Result.ok(svid);
return svid;
}
/**
* Returns the X509-Bundle for a given trust domain, returns an Error in case
* there is no bundle for the trust domain, or the source is already closed.
* Returns the X509 bundle for a given trust domain.
*
* @return an {@link spiffe.result.Ok} containing the {@link X509Bundle}.
* @return an instance of a {@link X509Bundle}
*
* @throws BundleNotFoundException is there is no bundle for the trust domain provided
* @throws IllegalStateException if the source is closed
*/
@Override
public Result<X509Bundle, String> getX509BundleForTrustDomain(@NonNull final TrustDomain trustDomain) {
val checkClosed = checkClosed();
if (checkClosed.isError()) {
return Result.error(checkClosed.getError());
public X509Bundle getX509BundleForTrustDomain(@NonNull final TrustDomain trustDomain) throws BundleNotFoundException {
if (isClosed()) {
throw new IllegalStateException("X509 bundle source is closed");
}
return bundles.getX509BundleForTrustDomain(trustDomain);
}
@ -150,14 +141,18 @@ public class X509Source implements X509SvidSource, X509BundleSource, Closeable {
}
private Result<Boolean, String> init() {
Result<X509Context, String> x509Context = workloadApiClient.fetchX509Context();
if (x509Context.isError()) {
return Result.error(x509Context.getError());
}
setX509Context(x509Context.getValue());
private static WorkloadApiClient createClient(@NonNull X509Source.@NonNull X509SourceOptions options) throws SocketEndpointAddressException {
val clientOptions= WorkloadApiClient.ClientOptions
.builder()
.spiffeSocketPath(options.spiffeSocketPath)
.build();
return WorkloadApiClient.newClient(clientOptions);
}
private void init() {
X509Context x509Context = workloadApiClient.fetchX509Context();
setX509Context(x509Context);
setX509ContextWatcher();
return Result.ok(true);
}
private void setX509ContextWatcher() {
@ -169,8 +164,8 @@ public class X509Source implements X509SvidSource, X509BundleSource, Closeable {
}
@Override
public void OnError(Error<X509Context, String> error) {
log.log(Level.SEVERE, String.format("Error in X509Context watcher: %s", error.getError()));
public void OnError(Throwable error) {
log.log(Level.SEVERE, String.format("Error in X509Context watcher: %s %n %s", error.getMessage(), ExceptionUtils.getStackTrace(error)));
}
});
}
@ -188,12 +183,9 @@ public class X509Source implements X509SvidSource, X509BundleSource, Closeable {
}
}
private Result<Boolean, String> checkClosed() {
private boolean isClosed() {
synchronized (this) {
if (closed) {
return Result.error("source is closed");
}
return Result.ok(true);
return closed;
}
}
@ -202,8 +194,21 @@ public class X509Source implements X509SvidSource, X509BundleSource, Closeable {
*/
@Data
public static class X509SourceOptions {
/**
* Address to the Workload API, if it is not set, the default address will be used.
*/
String spiffeSocketPath;
/**
* Function to choose the X509 SVID from the list returned by the Workload API
* If it is not set, the default svid is picked.
*/
Function<List<X509Svid>, X509Svid> picker;
/**
* A custom instance of a {@link WorkloadApiClient}, if it is not set, a new instance will be created
*/
WorkloadApiClient workloadApiClient;
@Builder

View File

@ -1,63 +1,50 @@
package spiffe.workloadapi.internal;
import lombok.val;
import spiffe.bundle.x509bundle.X509Bundle;
import spiffe.bundle.x509bundle.X509BundleSet;
import spiffe.result.Result;
import spiffe.exception.X509SvidException;
import spiffe.spiffeid.SpiffeId;
import spiffe.svid.x509svid.X509Svid;
import spiffe.workloadapi.X509Context;
import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.List;
/**
* Utility methods for converting GRPC objects to JAVA-SPIFFE domain objects.
*/
public class GrpcConversionUtils {
public static Result<X509Context, String> toX509Context(Workload.X509SVIDResponse x509SVIDResponse) {
Result<List<X509Svid>, String> x509SvidListResult = getListOfX509Svid(x509SVIDResponse);
if (x509SvidListResult.isError()) {
return Result.error(x509SvidListResult.getError());
}
Result<List<X509Bundle>, String> x509BundleListResult = getListOfX509Bundles(x509SVIDResponse);
if (x509BundleListResult.isError()) {
return Result.error(x509BundleListResult.getError());
}
X509BundleSet bundleSet = X509BundleSet.of(x509BundleListResult.getValue());
X509Context result = new X509Context(x509SvidListResult.getValue(), bundleSet);
return Result.ok(result);
public static X509Context toX509Context(Workload.X509SVIDResponse x509SVIDResponse) throws CertificateException, X509SvidException {
List<X509Svid> x509SvidList = getListOfX509Svid(x509SVIDResponse);
List<X509Bundle> x509BundleList = getListOfX509Bundles(x509SVIDResponse);
X509BundleSet bundleSet = X509BundleSet.of(x509BundleList);
return new X509Context(x509SvidList, bundleSet);
}
private static Result<List<X509Bundle>, String> getListOfX509Bundles(Workload.X509SVIDResponse x509SVIDResponse) {
private static List<X509Bundle> getListOfX509Bundles(Workload.X509SVIDResponse x509SVIDResponse) throws CertificateException {
List<X509Bundle> x509BundleList = new ArrayList<>();
for (Workload.X509SVID x509SVID : x509SVIDResponse.getSvidsList()) {
Result<SpiffeId, String> spiffeId = SpiffeId.parse(x509SVID.getSpiffeId());
if (spiffeId.isError()) {
return Result.error(spiffeId.getError());
}
SpiffeId spiffeId = SpiffeId.parse(x509SVID.getSpiffeId());
Result<X509Bundle, String> bundle = X509Bundle.parse(
spiffeId.getValue().getTrustDomain(),
X509Bundle bundle = X509Bundle.parse(
spiffeId.getTrustDomain(),
x509SVID.getBundle().toByteArray());
if (bundle.isError()) {
return Result.error(bundle.getError());
}
x509BundleList.add(bundle.getValue());
x509BundleList.add(bundle);
}
return Result.ok(x509BundleList);
return x509BundleList;
}
private static Result<List<X509Svid>, String> getListOfX509Svid(Workload.X509SVIDResponse x509SVIDResponse) {
private static List<X509Svid> getListOfX509Svid(Workload.X509SVIDResponse x509SVIDResponse) throws X509SvidException {
List<X509Svid> x509SvidList = new ArrayList<>();
for (Workload.X509SVID x509SVID : x509SVIDResponse.getSvidsList()) {
Result<X509Svid, String> svid = X509Svid.parse(
val svid = X509Svid.parse(
x509SVID.getX509Svid().toByteArray(),
x509SVID.getX509SvidKey().toByteArray());
if (svid.isError()){
return Result.error(svid.getError());
}
x509SvidList.add(svid.getValue());
x509SvidList.add(svid);
}
return Result.ok(x509SvidList);
return x509SvidList;
}
}

View File

@ -23,7 +23,7 @@ public class GrpcManagedChannelFactory {
* Return a ManagedChannel to the Spiffe Socket Endpoint provided.
*
* @param address URI representing the Workload API endpoint.
* @return a instance of a ManagedChannel.
* @return a instance of a {@link ManagedChannel}
*/
public static ManagedChannel newChannel(@NonNull URI address) {
if ("unix".equals(address.getScheme())) {

View File

@ -1,42 +1,48 @@
package spiffe.bundle.x509bundle;
import org.junit.jupiter.api.Test;
import spiffe.result.Result;
import spiffe.spiffeid.TrustDomain;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.cert.CertificateException;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
public class X509BundleTest {
@Test
void parse_bundleByteArrayInPEMFormatAndTrustDomain_returnX509Bundle() throws IOException {
byte[] bundlePem = Files.readAllBytes(Paths.get("../testdata/bundle.pem"));
TrustDomain trustDomain = TrustDomain.of("example.org").getValue();
TrustDomain trustDomain = TrustDomain.of("example.org");
Result<X509Bundle, String> x509Bundle = X509Bundle.parse(trustDomain, bundlePem);
X509Bundle x509Bundle = null;
try {
x509Bundle = X509Bundle.parse(trustDomain, bundlePem);
} catch (CertificateException e) {
fail("Not expected exception", e);
}
assertAll(
() -> assertEquals(1, x509Bundle.getValue().getX509Authorities().size()),
() -> assertEquals("example.org", x509Bundle.getValue().getTrustDomain().toString())
);
assertEquals(1, x509Bundle.getX509Authorities().size());
assertEquals("example.org", x509Bundle.getTrustDomain().toString());
}
@Test
void load_bundleByteArrayInPEMFormatAndTrustDomain_returnX509Bundle() {
Path bundlePath = Paths.get("../testdata/bundle.pem");
TrustDomain trustDomain = TrustDomain.of("example.org").getValue();
TrustDomain trustDomain = TrustDomain.of("example.org");
Result<X509Bundle, String> x509Bundle = X509Bundle.load(trustDomain, bundlePath);
X509Bundle x509Bundle = null;
try {
x509Bundle = X509Bundle.load(trustDomain, bundlePath);
} catch (IOException | CertificateException e) {
fail("Not expected exception", e);
}
assertAll(
() -> assertEquals(1, x509Bundle.getValue().getX509Authorities().size()),
() -> assertEquals("example.org", x509Bundle.getValue().getTrustDomain().toString())
);
assertEquals(1, x509Bundle.getX509Authorities().size());
assertEquals("example.org", x509Bundle.getTrustDomain().toString());
}
}

View File

@ -2,13 +2,20 @@ package spiffe.internal;
import lombok.val;
import org.junit.jupiter.api.Test;
import spiffe.spiffeid.SpiffeId;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertPathValidatorException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
public class CertificateUtilsTest {
@ -16,23 +23,31 @@ public class CertificateUtilsTest {
void generateCertificates_ofPEMByteArray_returnsListWithOneX509Certificate() throws IOException {
val certBytes = Files.readAllBytes(Paths.get("../testdata/x509cert.pem"));
val x509CertificateList = CertificateUtils.generateCertificates(certBytes);
List<X509Certificate> x509CertificateList = null;
SpiffeId spiffeId = null;
try {
x509CertificateList = CertificateUtils.generateCertificates(certBytes);
spiffeId = CertificateUtils.getSpiffeId(x509CertificateList.get(0));
} catch (CertificateException e) {
fail("Not expected exception. Should have generated the certificates", e);
}
val spiffeId = CertificateUtils.getSpiffeId(x509CertificateList.getValue().get(0));
assertEquals("spiffe://example.org/test", spiffeId.getValue().toString());
assertEquals("spiffe://example.org/test", spiffeId.toString());
}
@Test
void validate_certificateThatIsExpired_ReturnsError() throws IOException {
void validate_certificateThatIsExpired_throwsCertificateException() throws IOException, CertificateException {
val certBytes = Files.readAllBytes(Paths.get("../testdata/x509cert_other.pem"));
val bundleBytes = Files.readAllBytes(Paths.get("../testdata/bundle_other.pem"));
val chain = CertificateUtils.generateCertificates(certBytes);
val trustedCert = CertificateUtils.generateCertificates(bundleBytes);
val result = CertificateUtils.validate(chain.getValue(), trustedCert.getValue());
assertTrue(result.isError());
assertTrue(result.getError().contains("Error validating certificate chain: validity check failed"));
try {
CertificateUtils.validate(chain, trustedCert);
fail("Expected exception");
} catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException | CertPathValidatorException e) {
assertEquals("validity check failed", e.getMessage());
}
}
}

View File

@ -2,8 +2,6 @@ package spiffe.spiffeid;
import lombok.val;
import org.junit.jupiter.api.Test;
import spiffe.result.Error;
import spiffe.result.Ok;
import static org.junit.jupiter.api.Assertions.*;
@ -11,56 +9,56 @@ public class SpiffeIdTest {
@Test
void of_TrustDomainAndPathSegments_ReturnsSpiffeIdWithTrustDomainAndPathWithSegments() {
val trustDomain = TrustDomain.of("trust-domain.org").getValue();
val trustDomain = TrustDomain.of("trust-domain.org");
val spiffeIdResult = SpiffeId.of(trustDomain, "path1", "path2");
val spiffeId = SpiffeId.of(trustDomain, "path1", "path2");
assertAll("spiffeId",
() -> assertEquals("trust-domain.org", spiffeIdResult.getValue().getTrustDomain().toString()),
() -> assertEquals("/path1/path2", spiffeIdResult.getValue().getPath())
() -> assertEquals("trust-domain.org", spiffeId.getTrustDomain().toString()),
() -> assertEquals("/path1/path2", spiffeId.getPath())
);
}
@Test
void of_BlankPaths_ReturnsSpiffeIdWithTrustDomainAndPathWithSegments() {
val trustDomain = TrustDomain.of("trust-domain.org").getValue();
void of_TrustDomainAndNoPaths_ReturnsSpiffeIdWithTrustDomain() {
val trustDomain = TrustDomain.of("trust-domain.org");
val spiffeIdResult = SpiffeId.of(trustDomain, "", "");
val spiffeId = SpiffeId.of(trustDomain);
assertAll("spiffeId",
() -> assertEquals("trust-domain.org", spiffeIdResult.getValue().getTrustDomain().toString()),
() -> assertEquals("", spiffeIdResult.getValue().getPath())
() -> assertEquals("trust-domain.org", spiffeId.getTrustDomain().toString()),
() -> assertEquals("", spiffeId.getPath())
);
}
@Test
void of_TrustDomainAndPathsWithCaps_ReturnsSpiffeIdNormalized() {
val trustDomain = TrustDomain.of("TRuST-DoMAIN.Org").getValue();
val trustDomain = TrustDomain.of("TRuST-DoMAIN.Org");
val spiffeIdResult = SpiffeId.of(trustDomain, "PATH1", "paTH2");
val spiffeId = SpiffeId.of(trustDomain, "PATH1", "paTH2");
assertAll("normalized spiffeId",
() -> assertEquals("trust-domain.org", spiffeIdResult.getValue().getTrustDomain().toString()),
() -> assertEquals("/path1/path2", spiffeIdResult.getValue().getPath())
() -> assertEquals("trust-domain.org", spiffeId.getTrustDomain().toString()),
() -> assertEquals("/path1/path2", spiffeId.getPath())
);
}
@Test
void of_TrustDomainAndPathWithLeadingAndTrailingBlanks_ReturnsSpiffeIdNormalized() {
val trustDomain = TrustDomain.of(" trust-domain.org ").getValue();
val trustDomain = TrustDomain.of(" trust-domain.org ");
val spiffeIdResult = SpiffeId.of(trustDomain, " path1 ", " path2 ");
val spiffeId = SpiffeId.of(trustDomain, " path1 ", " path2 ");
assertAll("normalized spiffeId",
() -> assertEquals("trust-domain.org", spiffeIdResult.getValue().getTrustDomain().toString()),
() -> assertEquals("/path1/path2", spiffeIdResult.getValue().getPath())
() -> assertEquals("trust-domain.org", spiffeId.getTrustDomain().toString()),
() -> assertEquals("/path1/path2", spiffeId.getPath())
);
}
@Test
void toString_SpiffeId_ReturnsTheSpiffeIdInAStringFormatIncludingTheSchema() {
val trustDomain = TrustDomain.of("trust-domain.org").getValue();
val spiffeId = SpiffeId.of(trustDomain, "path1", "path2", "path3").getValue();
val trustDomain = TrustDomain.of("trust-domain.org");
val spiffeId = SpiffeId.of(trustDomain, "path1", "path2", "path3");
val spiffeIdToString = spiffeId.toString();
@ -69,20 +67,20 @@ public class SpiffeIdTest {
@Test
void memberOf_aTrustDomainAndASpiffeIdWithSameTrustDomain_ReturnTrue() {
val trustDomain = TrustDomain.of("trust-domain.org").getValue();
val spiffeId = SpiffeId.of(trustDomain, "path1", "path2").getValue();
val trustDomain = TrustDomain.of("trust-domain.org");
val spiffeId = SpiffeId.of(trustDomain, "path1", "path2");
val isMemberOf = spiffeId.memberOf(TrustDomain.of("trust-domain.org").getValue());
val isMemberOf = spiffeId.memberOf(TrustDomain.of("trust-domain.org"));
assertTrue(isMemberOf);
}
@Test
void memberOf_aTrustDomainAndASpiffeIdWithDifferentTrustDomain_ReturnFalse() {
val trustDomain = TrustDomain.of("trust-domain.org").getValue();
val spiffeId = SpiffeId.of(trustDomain, "path1", "path2").getValue();
val trustDomain = TrustDomain.of("trust-domain.org");
val spiffeId = SpiffeId.of(trustDomain, "path1", "path2");
val isMemberOf = spiffeId.memberOf(TrustDomain.of("other-domain.org").getValue());
val isMemberOf = spiffeId.memberOf(TrustDomain.of("other-domain.org"));
assertFalse(isMemberOf);
}
@ -91,69 +89,67 @@ public class SpiffeIdTest {
void parse_aString_ReturnsASpiffeIdThatHasTrustDomainAndPathSegments() {
val spiffeIdAsString = "spiffe://trust-domain.org/path1/path2";
val spiffeIdResult = SpiffeId.parse(spiffeIdAsString);
val spiffeId = SpiffeId.parse(spiffeIdAsString);
assertAll("SpiffeId",
() -> assertEquals(Ok.class, spiffeIdResult.getClass()),
() -> assertEquals("trust-domain.org", spiffeIdResult.getValue().getTrustDomain().toString()),
() -> assertEquals("/path1/path2", spiffeIdResult.getValue().getPath())
() -> assertEquals("trust-domain.org", spiffeId.getTrustDomain().toString()),
() -> assertEquals("/path1/path2", spiffeId.getPath())
);
}
@Test
void parse_aStringContainingInvalidSchema_ReturnsError() {
void parse_aStringContainingInvalidSchema_throwsIllegalArgumentException() {
val invalidadSpiffeId = "siffe://trust-domain.org/path1/path2";
val spiffeIdResult = SpiffeId.parse(invalidadSpiffeId);
assertAll("Error",
() -> assertEquals(Error.class, spiffeIdResult.getClass()),
() -> assertEquals("Invalid SPIFFE schema", spiffeIdResult.getError())
);
try {
SpiffeId.parse(invalidadSpiffeId);
fail("Should have thrown IllegalArgumentException");
} catch (IllegalArgumentException e) {
assertEquals("Invalid SPIFFE schema", e.getMessage());
}
}
@Test
void parse_aBlankString_ReturnsAError() {
val spiffeIdAsString = "";
val spiffeIdResult = SpiffeId.parse(spiffeIdAsString);
assertAll("Error",
() -> assertEquals(Error.class, spiffeIdResult.getClass()),
() -> assertEquals("SPIFFE ID cannot be empty", spiffeIdResult.getError())
);
void parse_aBlankString_throwsIllegalArgumentException() {
try {
SpiffeId.parse("");
fail("Should have thrown IllegalArgumentException");
} catch (IllegalArgumentException e) {
assertEquals("SPIFFE ID cannot be empty", e.getMessage());
}
}
@Test
void of_nullTrustDomain_returnsAError() {
val spiffeIdResult = SpiffeId.of(null, "path");
assertEquals(Error.class, spiffeIdResult.getClass());
assertEquals("Trust Domain cannot be null", spiffeIdResult.getError());
void of_nullTrustDomain_throwsIllegalArgumentException() {
try {
SpiffeId.of(null, "path");
fail("Should have thrown IllegalArgumentException");
} catch (NullPointerException e) {
assertEquals("trustDomain is marked non-null but is null", e.getMessage());
}
}
@Test
void equals_twoSpiffeIdsWithSameTrustDomainAndPath_returnsTrue() {
val spiffeId1 = SpiffeId.of(TrustDomain.of("example.org").getValue(), "path1").getValue();
val spiffeId2 = SpiffeId.of(TrustDomain.of("example.org").getValue(), "path1").getValue();
val spiffeId1 = SpiffeId.of(TrustDomain.of("example.org"), "path1");
val spiffeId2 = SpiffeId.of(TrustDomain.of("example.org"), "path1");
assertEquals(spiffeId1, spiffeId2);
}
@Test
void equals_twoSpiffeIdsWithSameTrustDomainAndDifferentPath_returnsFalse() {
val spiffeId1 = SpiffeId.of(TrustDomain.of("example.org").getValue(), "path1").getValue();
val spiffeId2 = SpiffeId.of(TrustDomain.of("example.org").getValue(), "other").getValue();
val spiffeId1 = SpiffeId.of(TrustDomain.of("example.org"), "path1");
val spiffeId2 = SpiffeId.of(TrustDomain.of("example.org"), "other");
assertNotEquals(spiffeId1, spiffeId2);
}
@Test
void equals_twoSpiffeIdsWithDifferentTrustDomainAndSamePath_returnsFalse() {
val spiffeId1 = SpiffeId.of(TrustDomain.of("example.org").getValue(), "path1").getValue();
val spiffeId2 = SpiffeId.of(TrustDomain.of("other.org").getValue(), "path1").getValue();
val spiffeId1 = SpiffeId.of(TrustDomain.of("example.org"), "path1");
val spiffeId2 = SpiffeId.of(TrustDomain.of("other.org"), "path1");
assertNotEquals(spiffeId1, spiffeId2);
}

View File

@ -2,86 +2,85 @@ package spiffe.spiffeid;
import lombok.val;
import org.junit.jupiter.api.Test;
import spiffe.result.Error;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
public class TrustDomainTest {
@Test
void of_givenAString_returnTrustDomain() {
val trustDomainResult = TrustDomain.of("domain.test");
assertEquals("domain.test", trustDomainResult.getValue().toString());
val trustDomain = TrustDomain.of("domain.test");
assertEquals("domain.test", trustDomain.toString());
}
@Test
void of_givenASpiffeIdString_returnTrustDomainWithHostPart() {
val trustDomainResult = TrustDomain.of("spiffe://domain.test");
assertEquals("domain.test", trustDomainResult.getValue().toString());
val trustDomain = TrustDomain.of("spiffe://domain.test");
assertEquals("domain.test", trustDomain.toString());
}
@Test
void of_givenASpiffeIdStringWithPath_returnTrustDomainWithHostPart() {
val trustDomainResult = TrustDomain.of("spiffe://domain.test/workload");
assertEquals("domain.test", trustDomainResult.getValue().toString());
val trustDomain = TrustDomain.of("spiffe://domain.test/workload");
assertEquals("domain.test", trustDomain.toString());
}
@Test
void of_givenAStringWithCaps_returnNormalizedTrustDomain() {
val trustDomainResult = TrustDomain.of("DoMAin.TesT");
val trustDomain = TrustDomain.of("DoMAin.TesT");
assertEquals("domain.test", trustDomainResult.getValue().toString());
assertEquals("domain.test", trustDomain.toString());
}
@Test
void of_givenAStringWithTrailingAndLeadingBlanks_returnNormalizedTrustDomain() {
val trustDomainResult = TrustDomain.of(" domain.test ");
val trustDomain = TrustDomain.of(" domain.test ");
assertEquals("domain.test", trustDomainResult.getValue().toString());
assertEquals("domain.test", trustDomain.toString());
}
@Test
void of_nullString_ThrowsIllegalArgumentException() {
val trustDomainResult = TrustDomain.of(null);
assertAll(
() -> assertEquals(Error.class, trustDomainResult.getClass()),
() -> assertEquals("Trust Domain cannot be empty.", trustDomainResult.getError())
);
try {
TrustDomain.of(null);
} catch (NullPointerException e) {
assertEquals("trustDomain is marked non-null but is null", e.getMessage());
}
}
@Test
void of_emptyString_ThrowsIllegalArgumentException() {
val trustDomainResult = TrustDomain.of("");
assertAll(
() -> assertEquals(Error.class, trustDomainResult.getClass()),
() -> assertEquals("Trust Domain cannot be empty.", trustDomainResult.getError())
);
try {
TrustDomain.of("");
} catch (IllegalArgumentException e) {
assertEquals("Trust Domain cannot be empty", e.getMessage());
}
}
@Test
void of_blankString_ThrowsIllegalArgumentException() {
val trustDomainResult = TrustDomain.of(" ");
assertAll(
() -> assertEquals(Error.class, trustDomainResult.getClass()),
() -> assertEquals("Trust Domain cannot be empty.", trustDomainResult.getError())
);
try {
TrustDomain.of(" ");
} catch (IllegalArgumentException e) {
assertEquals("Trust Domain cannot be empty", e.getMessage());
}
}
@Test
void equals_twoTrustDomainObjectsWithTheSameString_returnsTrue() {
val trustDomainResult1 = TrustDomain.of("example.org");
val trustDomainResult2 = TrustDomain.of("example.org");
val trustDomain1 = TrustDomain.of("example.org");
val trustDomain2 = TrustDomain.of("example.org");
assertEquals(trustDomainResult1.getValue(), trustDomainResult2.getValue());
assertEquals(trustDomain1, trustDomain2);
}
@Test
void equals_twoTrustDomainObjectsWithDifferentStrings_returnsFalse() {
val trustDomainResult1 = TrustDomain.of("example.org");
val trustDomainResult2 = TrustDomain.of("other.org");
val trustDomain1 = TrustDomain.of("example.org");
val trustDomain2 = TrustDomain.of("other.org");
assertNotEquals(trustDomainResult1.getValue(), trustDomainResult2.getValue());
assertNotEquals(trustDomain1, trustDomain2);
}
}

View File

@ -2,6 +2,7 @@ package spiffe.svid.x509svid;
import lombok.val;
import org.junit.jupiter.api.Test;
import spiffe.exception.X509SvidException;
import java.io.IOException;
import java.nio.file.Files;
@ -12,45 +13,50 @@ import static org.junit.jupiter.api.Assertions.*;
public class X509SvidTest {
@Test
void parse_GivenCertAndPrivateKeyPEMsInByteArrays_ReturnsX509Svid() throws IOException {
void parse_GivenCertAndPrivateKeyPEMsInByteArrays_ReturnsX509Svid() throws X509SvidException, IOException {
val certPem = Files.readAllBytes(Paths.get("../testdata/x509cert.pem"));
val keyPem = Files.readAllBytes(Paths.get("../testdata/pkcs8key.pem"));
val result = X509Svid.parse(certPem, keyPem);
val x509Svid = X509Svid.parse(certPem, keyPem);
assertAll("X509-SVID",
() -> assertTrue(result.isOk()),
() -> assertEquals("spiffe://example.org/test", result.getValue().getSpiffeId().toString()),
() -> assertEquals(1, result.getValue().getChain().size()),
() -> assertNotNull(result.getValue().getPrivateKey())
() -> assertEquals("spiffe://example.org/test", x509Svid.getSpiffeId().toString()),
() -> assertEquals(1, x509Svid.getChain().size()),
() -> assertNotNull(x509Svid.getPrivateKey())
);
}
@Test
void parse_GivenChainOfCertsAndPrivateKeyPEMsInByteArrays_ReturnsX509Svid() throws IOException {
void parse_GivenChainOfCertsAndPrivateKeyPEMsInByteArrays_ReturnsX509Svid() throws IOException, X509SvidException {
val certPem = Files.readAllBytes(Paths.get("../testdata/x509chain.pem"));
val keyPem = Files.readAllBytes(Paths.get("../testdata/pkcs8key.pem"));
val result = X509Svid.parse(certPem, keyPem);
assertAll("X509-SVID",
() -> assertEquals("spiffe://example.org/test", result.getValue().getSpiffeId().toString()),
() -> assertEquals(4, result.getValue().getChain().size()),
() -> assertNotNull(result.getValue().getPrivateKey())
() -> assertEquals("spiffe://example.org/test", result.getSpiffeId().toString()),
() -> assertEquals(4, result.getChain().size()),
() -> assertNotNull(result.getPrivateKey())
);
}
@Test
void load_GivenCertAndPrivateKeyPaths_ReturnsX509Svid() {
void load_GivenCertAndPrivateKeyPaths_ReturnsX509Svid() throws X509SvidException {
val certsFile = Paths.get("../testdata/x509cert.pem");
val privateKeyFile = Paths.get("../testdata/pkcs8key.pem");
val result = X509Svid.load(certsFile, privateKeyFile);
X509Svid result;
try {
result = X509Svid.load(certsFile, privateKeyFile);
} catch (X509SvidException e) {
fail("Not expected exception", e);
throw e;
}
assertAll("X509-SVID",
() -> assertEquals("spiffe://example.org/test", result.getValue().getSpiffeId().toString()),
() -> assertEquals(1, result.getValue().getChain().size()),
() -> assertNotNull(result.getValue().getPrivateKey())
() -> assertEquals("spiffe://example.org/test", result.getSpiffeId().toString()),
() -> assertEquals(1, result.getChain().size()),
() -> assertNotNull(result.getPrivateKey())
);
}
}

View File

@ -7,14 +7,15 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import spiffe.bundle.x509bundle.X509Bundle;
import spiffe.bundle.x509bundle.X509BundleSource;
import spiffe.exception.BundleNotFoundException;
import spiffe.internal.CertificateUtils;
import spiffe.result.Result;
import spiffe.spiffeid.SpiffeId;
import spiffe.spiffeid.TrustDomain;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.cert.CertificateException;
import java.util.Arrays;
import java.util.List;
@ -32,50 +33,55 @@ public class X509SvidValidatorTest {
}
@Test
void verifyChain_certificateExpired_returnsError() throws IOException {
void verifyChain_certificateExpired_throwsCertificateException() throws IOException, CertificateException, BundleNotFoundException {
val certBytes = Files.readAllBytes(Paths.get("../testdata/x509cert.pem"));
val chain = CertificateUtils.generateCertificates(certBytes).getValue();
Result<X509Bundle, String> x509Bundle=
val chain = CertificateUtils.generateCertificates(certBytes);
X509Bundle x509Bundle=
X509Bundle.load(
TrustDomain.of("example.org").getValue(),
TrustDomain.of("example.org"),
Paths.get("../testdata/bundle.pem")
);
when(bundleSourceMock
.getX509BundleForTrustDomain(
TrustDomain.of("example.org").getValue()))
TrustDomain.of("example.org")))
.thenReturn(x509Bundle);
val result = X509SvidValidator.verifyChain(chain, bundleSourceMock);
assertTrue(result.isError());
assertTrue(result.getError().contains("CertificateExpiredException: NotAfter"));
try {
X509SvidValidator.verifyChain(chain, bundleSourceMock);
fail("Verify chain should have thrown validation exception");
} catch (CertificateException e) {
assertEquals("java.security.cert.CertPathValidatorException: validity check failed", e.getMessage());
}
}
@Test
void checkSpiffeId_givenASpiffeIdInTheListOfAcceptedIds_returnsValid() {
val spiffeId1 = SpiffeId.parse("spiffe://example.org/test").getValue();
val spiffeId2 = SpiffeId.parse("spiffe://example.org/test2").getValue();
void checkSpiffeId_givenASpiffeIdInTheListOfAcceptedIds_doesntThrowException() throws IOException, CertificateException {
val spiffeId1 = SpiffeId.parse("spiffe://example.org/test");
val spiffeId2 = SpiffeId.parse("spiffe://example.org/test2");
Result<List<SpiffeId>, String> spiffeIdList = Result.ok(Arrays.asList(spiffeId1, spiffeId2));
val certBytes = Files.readAllBytes(Paths.get("../testdata/x509cert.pem"));
val x509Certificate = CertificateUtils.generateCertificates(certBytes);
val result = X509SvidValidator
.verifySpiffeId(SpiffeId.parse("spiffe://example.org/test").getValue(), () -> spiffeIdList);
val spiffeIdList = Arrays.asList(spiffeId1, spiffeId2);
assertTrue(result.isOk());
X509SvidValidator.verifySpiffeId(x509Certificate.get(0), () -> spiffeIdList);
}
@Test
void checkSpiffeId_givenASpiffeIdNotInTheListOfAcceptedIds_returnsValid() {
val spiffeId1 = SpiffeId.parse("spiffe://example.org/other1").getValue();
val spiffeId2 = SpiffeId.parse("spiffe://example.org/other2").getValue();
Result<List<SpiffeId>, String> spiffeIdList = Result.ok(Arrays.asList(spiffeId1, spiffeId2));
void checkSpiffeId_givenASpiffeIdNotInTheListOfAcceptedIds_throwsCertificateException() throws IOException, CertificateException {
val spiffeId1 = SpiffeId.parse("spiffe://example.org/other1");
val spiffeId2 = SpiffeId.parse("spiffe://example.org/other2");
List<SpiffeId> spiffeIdList = Arrays.asList(spiffeId1, spiffeId2);
val result = X509SvidValidator.verifySpiffeId(SpiffeId.parse("spiffe://example.org/test").getValue(), () -> spiffeIdList);
val certBytes = Files.readAllBytes(Paths.get("../testdata/x509cert.pem"));
val x509Certificate = CertificateUtils.generateCertificates(certBytes);
assertAll(
() -> assertTrue(result.isError()),
() -> assertEquals("SPIFFE ID 'spiffe://example.org/test' is not accepted",result.getError())
);
try {
X509SvidValidator.verifySpiffeId(x509Certificate.get(0), () -> spiffeIdList);
fail("Should have thrown CertificateException");
} catch (CertificateException e) {
assertEquals("SPIFFE ID spiffe://example.org/test in x509Certificate is not accepted", e.getMessage());
}
}
}

View File

@ -3,7 +3,7 @@ package spiffe.workloadapi;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import spiffe.result.Result;
import spiffe.exception.SocketEndpointAddressException;
import java.net.URI;
import java.util.stream.Stream;
@ -14,33 +14,38 @@ public class AddressTest {
@ParameterizedTest
@MethodSource("provideTestAddress")
void parseAddressInvalid(String input, Result expected) {
Result<URI, String> result = Address.parseAddress(input);
assertEquals(expected, result);
void parseAddressInvalid(String input, Object expected) {
URI result = null;
try {
result = Address.parseAddress(input);
assertEquals(expected, result);
} catch (SocketEndpointAddressException e) {
assertEquals(expected, e.getMessage());
}
}
static Stream<Arguments> provideTestAddress() {
return Stream.of(
Arguments.of("unix://foo", Result.ok(URI.create("unix://foo"))),
Arguments.of("\\t", Result.error("Workload endpoint socket is not a valid URI: Illegal character in path at index 0: \\t")),
Arguments.of("blah", Result.error("Workload endpoint socket URI must have a tcp:// or unix:// scheme")),
Arguments.of("unix:opaque", Result.error("Workload endpoint unix socket URI must not be opaque")),
Arguments.of("unix://", Result.error("Workload endpoint socket is not a valid URI: Expected authority at index 7: unix://")),
Arguments.of("unix://foo?whatever", Result.error("Workload endpoint unix socket URI must not include query values")),
Arguments.of("unix://foo#whatever", Result.error("Workload endpoint unix socket URI must not include a fragment")),
Arguments.of("unix://john:doe@foo/path", Result.error("Workload endpoint unix socket URI must not include user info")),
Arguments.of("unix://foo", URI.create("unix://foo")),
Arguments.of("\\t", "Workload endpoint socket is not a valid URI: \\t"),
Arguments.of("blah", "Workload endpoint socket URI must have a tcp:// or unix:// scheme: blah"),
Arguments.of("unix:opaque", "Workload endpoint unix socket URI must not be opaque: unix:opaque"),
Arguments.of("unix://", "Workload endpoint socket is not a valid URI: unix://"),
Arguments.of("unix://foo?whatever", "Workload endpoint unix socket URI must not include query values: unix://foo?whatever"),
Arguments.of("unix://foo#whatever", "Workload endpoint unix socket URI must not include a fragment: unix://foo#whatever"),
Arguments.of("unix://john:doe@foo/path", "Workload endpoint unix socket URI must not include user info: unix://john:doe@foo/path"),
Arguments.of("tcp://1.2.3.4:5", Result.ok(URI.create("tcp://1.2.3.4:5"))),
Arguments.of("tcp:opaque", Result.error("Workload endpoint tcp socket URI must not be opaque")),
Arguments.of("tcp://", Result.error("Workload endpoint socket is not a valid URI: Expected authority at index 6: tcp://")),
Arguments.of("tcp://1.2.3.4:5?whatever", Result.error("Workload endpoint tcp socket URI must not include query values")),
Arguments.of("tcp://1.2.3.4:5#whatever", Result.error("Workload endpoint tcp socket URI must not include a fragment")),
Arguments.of("tcp://john:doe@1.2.3.4:5/path", Result.error("Workload endpoint tcp socket URI must not include user info")),
Arguments.of("tcp://1.2.3.4:5/path", Result.error("Workload endpoint tcp socket URI must not include a path")),
Arguments.of("tcp://foo", Result.error("Workload endpoint tcp socket URI host component must be an IP:port")),
Arguments.of("tcp://1.2.3.4", Result.error("Workload endpoint tcp socket URI host component must include a port")),
Arguments.of("tcp://1.2.3.4:5", URI.create("tcp://1.2.3.4:5")),
Arguments.of("tcp:opaque", "Workload endpoint tcp socket URI must not be opaque: tcp:opaque"),
Arguments.of("tcp://", "Workload endpoint socket is not a valid URI: tcp://"),
Arguments.of("tcp://1.2.3.4:5?whatever", "Workload endpoint tcp socket URI must not include query values: tcp://1.2.3.4:5?whatever"),
Arguments.of("tcp://1.2.3.4:5#whatever", "Workload endpoint tcp socket URI must not include a fragment: tcp://1.2.3.4:5#whatever"),
Arguments.of("tcp://john:doe@1.2.3.4:5/path", "Workload endpoint tcp socket URI must not include user info: tcp://john:doe@1.2.3.4:5/path"),
Arguments.of("tcp://1.2.3.4:5/path", "Workload endpoint tcp socket URI must not include a path: tcp://1.2.3.4:5/path"),
Arguments.of("tcp://foo", "Workload endpoint tcp socket URI host component must be an IP:port: tcp://foo"),
Arguments.of("tcp://1.2.3.4", "Workload endpoint tcp socket URI host component must include a port: tcp://1.2.3.4"),
Arguments.of("blah://foo", Result.error("Workload endpoint socket URI must have a tcp:// or unix:// scheme"))
Arguments.of("blah://foo", "Workload endpoint socket URI must have a tcp:// or unix:// scheme: blah://foo")
);
}
}

View File

@ -3,17 +3,17 @@ package spiffe.helper;
import lombok.Builder;
import lombok.Value;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
@Value
class BundleEntry {
String alias;
Certificate certificate;
X509Certificate certificate;
@Builder
BundleEntry(
final String alias,
final Certificate certificate) {
final X509Certificate certificate) {
this.alias = alias;
this.certificate = certificate;
}

View File

@ -3,7 +3,6 @@ package spiffe.helper;
import lombok.Builder;
import lombok.NonNull;
import lombok.val;
import spiffe.result.Result;
import java.io.File;
import java.io.FileInputStream;
@ -17,7 +16,7 @@ import java.security.cert.CertificateException;
/**
* Represents a Java KeyStore, provides some functions
* to store a PrivateKey, Certificate chain and Bundles.
* to store a private key, a X509 certificate chain, and X509 bundles.
* Package private, to be used by the KeyStoreHelper.
*/
class KeyStore {
@ -33,25 +32,20 @@ class KeyStore {
KeyStore(
@NonNull final Path keyStoreFilePath,
@NonNull final KeyStoreType keyStoreType,
@NonNull final char[] keyStorePassword) {
@NonNull final char[] keyStorePassword) throws KeyStoreException {
this.keyStoreFilePath = keyStoreFilePath;
this.keyStoreType = keyStoreType;
this.keyStorePassword = keyStorePassword;
setupKeyStore();
}
private void setupKeyStore() {
private void setupKeyStore() throws KeyStoreException {
this.keyStoreFile = new File(keyStoreFilePath.toUri());
val keyStore = loadKeyStore(keyStoreFile);
if (keyStore.isError()) {
throw new RuntimeException(keyStore.getError());
}
this.keyStore = keyStore.getValue();
this.keyStore = loadKeyStore(keyStoreFile);
}
private Result<java.security.KeyStore, Throwable> loadKeyStore(final File keyStoreFile) {
private java.security.KeyStore loadKeyStore(final File keyStoreFile) throws KeyStoreException {
try {
val keyStore = java.security.KeyStore.getInstance(keyStoreType.value());
@ -62,58 +56,48 @@ class KeyStore {
//create new keyStore
keyStore.load(null, keyStorePassword);
}
return Result.ok(keyStore);
} catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException e) {
return Result.error(e);
return keyStore;
} catch (IOException | NoSuchAlgorithmException | CertificateException e) {
throw new KeyStoreException(e);
}
}
/**
* Store a PrivateKey and Certificate chain in a Java KeyStore
* Store a private key and X509 certificate chain in a Java KeyStore
*
* @param privateKeyEntry contains the alias, privateKey, chain, privateKey password
* @return Result of Boolean indicating if it was successful or an Error wrapping an Exception
*/
Result<Boolean, Throwable> storePrivateKey(final PrivateKeyEntry privateKeyEntry) {
try {
// Store PrivateKey Entry in KeyStore
keyStore.setKeyEntry(
privateKeyEntry.getAlias(),
privateKeyEntry.getPrivateKey(),
privateKeyEntry.getPassword(),
privateKeyEntry.getCertificateChain()
);
void storePrivateKey(final PrivateKeyEntry privateKeyEntry) throws KeyStoreException {
// Store PrivateKey Entry in KeyStore
keyStore.setKeyEntry(
privateKeyEntry.getAlias(),
privateKeyEntry.getPrivateKey(),
privateKeyEntry.getPassword(),
privateKeyEntry.getCertificateChain()
);
return this.flush();
} catch (KeyStoreException e) {
return Result.error(e);
}
this.flush();
}
/**
* Store a Bundle Entry in the KeyStore
*/
Result<Boolean, Throwable> storeBundleEntry(BundleEntry bundleEntry) {
try {
// Store Bundle Entry in KeyStore
this.keyStore.setCertificateEntry(
bundleEntry.getAlias(),
bundleEntry.getCertificate()
);
return this.flush();
} catch (KeyStoreException e) {
return Result.error(e);
}
void storeBundleEntry(BundleEntry bundleEntry) throws KeyStoreException {
// Store Bundle Entry in KeyStore
this.keyStore.setCertificateEntry(
bundleEntry.getAlias(),
bundleEntry.getCertificate()
);
this.flush();
}
// Flush KeyStore to disk, to the configured (@see keyStoreFilePath)
private Result<Boolean, Throwable> flush() {
private void flush() throws KeyStoreException {
try {
keyStore.store(new FileOutputStream(keyStoreFile), keyStorePassword);
return Result.ok(true);
} catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException e) {
return Result.error(e);
} catch (IOException | NoSuchAlgorithmException | CertificateException e) {
throw new KeyStoreException(e);
}
}
}

View File

@ -2,25 +2,24 @@ package spiffe.helper;
import lombok.Builder;
import lombok.NonNull;
import lombok.SneakyThrows;
import lombok.extern.java.Log;
import lombok.val;
import org.apache.commons.lang3.NotImplementedException;
import org.apache.commons.lang3.StringUtils;
import spiffe.result.Error;
import spiffe.result.Result;
import spiffe.exception.SocketEndpointAddressException;
import spiffe.workloadapi.Watcher;
import spiffe.workloadapi.WorkloadApiClient;
import spiffe.workloadapi.WorkloadApiClient.ClientOptions;
import spiffe.workloadapi.X509Context;
import java.nio.file.Path;
import java.security.KeyStoreException;
import java.util.concurrent.CountDownLatch;
import java.util.logging.Level;
/**
* A <code>KeyStoreHelper</code> represents a helper for storing X095-SVIDs and Bundles,
* that are automatically rotated via the Worklaod API, in a Java KeyStore in a file in disk.
* A <code>KeyStoreHelper</code> represents a helper for storing X509 SVIDs and bundles,
* that are automatically rotated via the Workload API, in a Java KeyStore in a file in disk.
*/
@Log
public class KeyStoreHelper {
@ -33,23 +32,25 @@ public class KeyStoreHelper {
private final String spiffeSocketPath;
/**
* Create an instance of a KeyStoreHelper for fetching X509-SVIDs and Bundles
* from a Workload API and store them in a Java binary KeyStore in disk.
* Create an instance of a KeyStoreHelper for fetching X509 SVIDs and bundles
* from a Workload API and store them in a binary Java KeyStore in disk.
* <p>
* It blocks until the initial update has been received from the Workload API.
*
* @param keyStoreFilePath path to File storing the KeyStore.
* @param keyStoreType the type of keystore. Only JKS and PKCS12 are supported. If it's not provided, PKCS12 is used
* See the KeyStore section in the <a href=
* "https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#KeyStore">
* Java Cryptography Architecture Standard Algorithm Name Documentation</a>
* for information about standard keystore types.
* See the KeyStore section in the <a href=
* "https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#KeyStore">
* Java Cryptography Architecture Standard Algorithm Name Documentation</a>
* for information about standard keystore types.
* @param keyStorePassword the password to generate the keystore integrity check
* @param privateKeyPassword the password to protect the key
* @param privateKeyAlias the alias name
* @param spiffeSocketPath optional spiffeSocketPath, if absent uses SPIFFE_ENDPOINT_SOCKET env variable
* @throws RuntimeException if this first update cannot be fetched.
* @throws RuntimeException if the KeyStore cannot be setup.
*
* @throws SocketEndpointAddressException is the socket endpoint address is not valid
* @throws KeyStoreException is the entry cannot be stored in the KeyStore
* @throws RuntimeException if there is an error fetching the certificates from the Workload API
*/
@Builder
public KeyStoreHelper(
@ -58,7 +59,8 @@ public class KeyStoreHelper {
@NonNull final char[] keyStorePassword,
@NonNull final char[] privateKeyPassword,
@NonNull final String privateKeyAlias,
@NonNull String spiffeSocketPath) {
@NonNull String spiffeSocketPath)
throws SocketEndpointAddressException, KeyStoreException {
this.privateKeyPassword = privateKeyPassword.clone();
@ -66,7 +68,7 @@ public class KeyStoreHelper {
this.spiffeSocketPath = spiffeSocketPath;
this.keyStore =
spiffe.helper.KeyStore
KeyStore
.builder()
.keyStoreFilePath(keyStoreFilePath)
.keyStoreType(keyStoreType)
@ -76,9 +78,8 @@ public class KeyStoreHelper {
setupX509ContextFetcher();
}
@SneakyThrows
private void setupX509ContextFetcher() {
Result<WorkloadApiClient, String> workloadApiClient;
private void setupX509ContextFetcher() throws SocketEndpointAddressException {
WorkloadApiClient workloadApiClient;
if (StringUtils.isNotBlank(spiffeSocketPath)) {
ClientOptions clientOptions = ClientOptions.builder().spiffeSocketPath(spiffeSocketPath).build();
@ -88,8 +89,8 @@ public class KeyStoreHelper {
}
CountDownLatch countDownLatch = new CountDownLatch(1);
setX509ContextWatcher(workloadApiClient.getValue(), countDownLatch);
countDownLatch.await();
setX509ContextWatcher(workloadApiClient, countDownLatch);
await(countDownLatch);
}
private void setX509ContextWatcher(WorkloadApiClient workloadApiClient, CountDownLatch countDownLatch) {
@ -97,18 +98,22 @@ public class KeyStoreHelper {
@Override
public void OnUpdate(X509Context update) {
log.log(Level.INFO, "Received X509Context update");
storeX509ContextUpdate(update);
try {
storeX509ContextUpdate(update);
} catch (KeyStoreException e) {
this.OnError(e);
}
countDownLatch.countDown();
}
@Override
public void OnError(Error<X509Context, String> error) {
throw new RuntimeException(error.getError());
public void OnError(Throwable t) {
throw new RuntimeException(t);
}
});
}
private void storeX509ContextUpdate(final X509Context update) {
private void storeX509ContextUpdate(final X509Context update) throws KeyStoreException {
val privateKeyEntry = PrivateKeyEntry.builder()
.alias(privateKeyAlias)
.password(privateKeyPassword)
@ -116,14 +121,19 @@ public class KeyStoreHelper {
.certificateChain(update.getDefaultSvid().getChainArray())
.build();
val storeKeyResult = keyStore.storePrivateKey(privateKeyEntry);
if (storeKeyResult.isError()) {
throw new RuntimeException(storeKeyResult.getError());
}
keyStore.storePrivateKey(privateKeyEntry);
log.log(Level.INFO, "Stored X509Context update");
// TODO: Store all the Bundles
throw new NotImplementedException("Bundle Storing is not implemented");
}
private void await(CountDownLatch countDownLatch) {
try {
countDownLatch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -4,21 +4,21 @@ import lombok.Builder;
import lombok.Value;
import java.security.Key;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
@Value
class PrivateKeyEntry {
String alias;
Key privateKey;
char[] password;
Certificate[] certificateChain;
X509Certificate[] certificateChain;
@Builder
PrivateKeyEntry(
final String alias,
final Key privateKey,
final char[] password,
final Certificate[] certificateChain) {
final X509Certificate[] certificateChain) {
this.alias = alias;
this.privateKey = privateKey;
this.password = password;

View File

@ -4,6 +4,7 @@ import lombok.val;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import spiffe.exception.X509SvidException;
import spiffe.internal.CertificateUtils;
import spiffe.svid.x509svid.X509Svid;
@ -20,7 +21,8 @@ import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
public class KeyStoreTest {
@ -31,12 +33,12 @@ public class KeyStoreTest {
@BeforeEach
void setup() {
void setup() throws X509SvidException {
x509Svid = X509Svid
.load(
Paths.get("../testdata/x509cert.pem"),
Paths.get("../testdata/pkcs8key.pem")
).getValue();
);
}
@Test
@ -60,9 +62,8 @@ public class KeyStoreTest {
.build();
val result = keyStore.storePrivateKey(privateKeyEntry);
keyStore.storePrivateKey(privateKeyEntry);
assertTrue(result.isOk());
checkEntryWasStored(keyStoreFilePath, keyStorePassword, privateKeyPassword, keyStoreType, DEFAULT_ALIAS);
}
@ -81,7 +82,7 @@ public class KeyStoreTest {
val privateKey = (PrivateKey) keyStore.getKey(alias, privateKeyPassword);
assertEquals(1, chain.length);
assertEquals("spiffe://example.org/test", spiffeId.getValue().toString());
assertEquals("spiffe://example.org/test", spiffeId.toString());
assertNotNull(privateKey);
}

View File

@ -13,13 +13,9 @@ will trust for TLS connections.
```
val sslContextOptions = SslContextOptions
.builder()
.x509Source(x509Source.newSource().getValue())
.x509Source(x509Source.newSource()())
.build();
Result<SSLContext, String> sslContext = SpiffeSslContextFactory.getSslContext(sslContextOptions);
if (sslContext.isError()) {
// handle sslContext.getError();
}
SSLContext sslContext = SpiffeSslContextFactory.getSslContext(sslContextOptions);
```
See [HttpsServer example](src/main/java/spiffe/provider/examples/HttpsServer.java).
@ -33,21 +29,13 @@ Supplier of accepted SPIFFE IDs list can be provided as part of the `SslContextO
.spiffeSocketPath(spiffeSocket)
.build();
val x509Source = X509Source.newSource(sourceOptions);
if (x509Source.isError()) {
// handle x509source.getError()
}
SslContextOptions sslContextOptions = SslContextOptions
.builder()
.acceptedSpiffeIdsSupplier(acceptedSpiffeIdsListSupplier)
.x509Source(x509Source.getValue())
.x509Source(x509Source())
.build();
Result<SSLContext, String> sslContext = SpiffeSslContextFactory
.getSslContext(sslContextOptions);
if (sslContext.isError()) {
// handle sslContext.getError()
}
SSLContext sslContext = SpiffeSslContextFactory.getSslContext(sslContextOptions);
```
See [HttpsClient example](src/main/java/spiffe/provider/examples/HttpsClient.java) that defines a Supplier for providing

View File

@ -1,7 +1,6 @@
package spiffe.provider;
import lombok.val;
import spiffe.result.Result;
import spiffe.svid.x509svid.X509Svid;
import spiffe.svid.x509svid.X509SvidSource;
@ -17,9 +16,9 @@ import java.util.Objects;
import static spiffe.provider.SpiffeProviderConstants.DEFAULT_ALIAS;
/**
* A <code>SpiffeKeyManager</code> represents a X509 KeyManager for the SPIFFE Provider.
* A <code>SpiffeKeyManager</code> represents a X509 key manager for the SPIFFE provider.
* <p>
* Provides the chain of X509 Certificates and the Private Key.
* Provides the chain of X509 certificates and the private key.
*/
public final class SpiffeKeyManager extends X509ExtendedKeyManager {
@ -32,27 +31,24 @@ public final class SpiffeKeyManager extends X509ExtendedKeyManager {
/**
* Returns the certificate chain associated with the given alias.
*
* @return the X.509 SVID Certificates
* @return the certificate chain as an array of {@link X509Certificate}
*/
@Override
public X509Certificate[] getCertificateChain(String alias) {
if (!Objects.equals(alias, DEFAULT_ALIAS)) {
return null;
}
Result<X509Svid, String> x509Svid = x509SvidSource.getX509Svid();
if (x509Svid.isError()) {
throw new IllegalStateException(x509Svid.getError());
}
return x509Svid.getValue().getChainArray();
X509Svid x509Svid = x509SvidSource.getX509Svid();
return x509Svid.getChainArray();
}
/**
* Returns the key associated with the given alias.
* Returns the private key handled by this key manager.
*
* @param alias a key entry, as this KeyManager only handles one identity, i.e. one SVID,
* it will return the PrivateKey if the alias asked for is 'Spiffe'.
* it will return the PrivateKey if the given alias is 'Spiffe'.
*
* @return the Private Key
* @return the {@link PrivateKey} handled by this key manager
*/
@Override
public PrivateKey getPrivateKey(String alias) {
@ -61,12 +57,8 @@ public final class SpiffeKeyManager extends X509ExtendedKeyManager {
return null;
}
Result<X509Svid, String> x509Svid = x509SvidSource.getX509Svid();
if (x509Svid.isError()) {
throw new IllegalStateException(x509Svid.getError());
}
return x509Svid.getValue().getPrivateKey();
X509Svid x509Svid = x509SvidSource.getX509Svid();
return x509Svid.getPrivateKey();
}
@ -104,11 +96,8 @@ public final class SpiffeKeyManager extends X509ExtendedKeyManager {
// the ALIAS handled by the current KeyManager, if it's not supported returns null
private String getAlias(String... keyTypes) {
val x509Svid = x509SvidSource.getX509Svid();
if (x509Svid.isError()) {
return null;
}
val privateKeyAlgorithm = x509Svid.getValue().getPrivateKey().getAlgorithm();
val privateKeyAlgorithm = x509Svid.getPrivateKey().getAlgorithm();
if (Arrays.asList(keyTypes).contains(privateKeyAlgorithm)) {
return DEFAULT_ALIAS;
}

View File

@ -33,10 +33,10 @@ public final class SpiffeKeyManagerFactory extends KeyManagerFactorySpi {
}
/**
* This method creates a KeyManager and initializes with a x509SvidSource passed as parameter.
* This method creates a KeyManager and initializes with the given X509 SVID source.
*
* @param x509SvidSource implementation of a {@link spiffe.bundle.x509bundle.X509BundleSource}
* @return a {@link KeyManager}
* @param x509SvidSource an instance of a {@link X509SvidSource}
* @return an array with an instance of a {@link KeyManager}
*/
public KeyManager[] engineGetKeyManagers(X509SvidSource x509SvidSource) {
val spiffeKeyManager = new SpiffeKeyManager(x509SvidSource);

View File

@ -4,8 +4,6 @@ import lombok.Builder;
import lombok.Data;
import lombok.NonNull;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import spiffe.result.Result;
import spiffe.spiffeid.SpiffeId;
import spiffe.workloadapi.X509Source;
@ -32,31 +30,29 @@ public final class SpiffeSslContextFactory {
* If the option acceptedSpiffeIdsSupplier is not provided, the list of accepted SPIFFE IDs
* is read from the Security Property ssl.spiffe.accept.
* If the sslProcotol is not provided, the default TLSv1.2 is used.
*
* @return a Result containing a SSLContext
* @return a {@link SSLContext}
* @throws IllegalArgumentException if the X509Source is not provided in the options
* @throws NoSuchAlgorithmException at initializing the SSL context
* @throws KeyManagementException at initializing the SSL context
*/
public static Result<SSLContext, String> getSslContext(@NonNull SslContextOptions options) {
try {
SSLContext sslContext;
if (StringUtils.isNotBlank(options.sslProtocol)) {
sslContext = SSLContext.getInstance(options.sslProtocol);
} else {
sslContext = SSLContext.getInstance(DEFAULT_SSL_PROTOCOL);
}
if (options.x509Source == null) {
return Result.error("x509Source option cannot be null, a X509 Source must be provided");
}
sslContext.init(
new SpiffeKeyManagerFactory().engineGetKeyManagers(options.x509Source),
new SpiffeTrustManagerFactory().engineGetTrustManagers(options.x509Source, options.acceptedSpiffeIdsSupplier),
null);
return Result.ok(sslContext);
} catch (NoSuchAlgorithmException | KeyManagementException e) {
return Result.error("Error creating SSL Context: %s %n %s", e.getMessage(), ExceptionUtils.getStackTrace(e));
public static SSLContext getSslContext(@NonNull SslContextOptions options) throws NoSuchAlgorithmException, KeyManagementException {
SSLContext sslContext;
if (StringUtils.isNotBlank(options.sslProtocol)) {
sslContext = SSLContext.getInstance(options.sslProtocol);
} else {
sslContext = SSLContext.getInstance(DEFAULT_SSL_PROTOCOL);
}
if (options.x509Source == null) {
throw new IllegalArgumentException("x509Source option cannot be null, a X509 Source must be provided");
}
sslContext.init(
new SpiffeKeyManagerFactory().engineGetKeyManagers(options.x509Source),
new SpiffeTrustManagerFactory().engineGetTrustManagers(options.x509Source, options.acceptedSpiffeIdsSupplier),
null);
return sslContext;
}
/**
@ -66,13 +62,13 @@ public final class SpiffeSslContextFactory {
public static class SslContextOptions {
String sslProtocol;
X509Source x509Source;
Supplier<Result<List<SpiffeId>, String>> acceptedSpiffeIdsSupplier;
Supplier<List<SpiffeId>> acceptedSpiffeIdsSupplier;
@Builder
public SslContextOptions(
String sslProtocol,
X509Source x509Source,
Supplier<Result<List<SpiffeId>, String>> acceptedSpiffeIdsSupplier) {
Supplier<List<SpiffeId>> acceptedSpiffeIdsSupplier) {
this.x509Source = x509Source;
this.acceptedSpiffeIdsSupplier = acceptedSpiffeIdsSupplier;
this.sslProtocol = sslProtocol;

View File

@ -7,6 +7,8 @@ import javax.net.ssl.SSLSocketFactory;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
/**
* A <code>SpiffeSslSocketFactory</code> is an implementation of SSLSocketFactory
@ -17,12 +19,9 @@ public class SpiffeSslSocketFactory extends SSLSocketFactory {
private final SSLSocketFactory delegate;
public SpiffeSslSocketFactory(SslContextOptions contextOptions) {
public SpiffeSslSocketFactory(SslContextOptions contextOptions) throws KeyManagementException, NoSuchAlgorithmException {
val sslContext = SpiffeSslContextFactory.getSslContext(contextOptions);
if (sslContext.isError()) {
throw new RuntimeException(sslContext.getError());
}
delegate = sslContext.getValue().getSocketFactory();
delegate = sslContext.getSocketFactory();
}
@Override
@ -42,7 +41,7 @@ public class SpiffeSslSocketFactory extends SSLSocketFactory {
@Override
public Socket createSocket(String s, int i) throws IOException {
return delegate.createSocket(s, i );
return delegate.createSocket(s, i);
}
@Override

View File

@ -1,9 +1,6 @@
package spiffe.provider;
import lombok.val;
import spiffe.bundle.x509bundle.X509BundleSource;
import spiffe.internal.CertificateUtils;
import spiffe.result.Result;
import spiffe.spiffeid.SpiffeId;
import spiffe.svid.x509svid.X509SvidValidator;
@ -25,17 +22,17 @@ import java.util.function.Supplier;
public final class SpiffeTrustManager extends X509ExtendedTrustManager {
private final X509BundleSource x509BundleSource;
private final Supplier<Result<List<SpiffeId>, String>> acceptedSpiffeIdsSupplier;
private final Supplier<List<SpiffeId>> acceptedSpiffeIdsSupplier;
/**
* Creates a SpiffeTrustManager with a X509BundleSource used to provide the trusted
* bundles, and a Supplier of a List of accepted SpiffeIds to be used during peer SVID validation.
*
* @param X509BundleSource an implementation of a {@link X509BundleSource}
* @param X509BundleSource an implementation of a {@link X509BundleSource}
* @param acceptedSpiffeIdsSupplier a Supplier of a list of accepted SPIFFE IDs.
*/
public SpiffeTrustManager(X509BundleSource X509BundleSource,
Supplier<Result<List<SpiffeId>, String>> acceptedSpiffeIdsSupplier) {
Supplier<List<SpiffeId>> acceptedSpiffeIdsSupplier) {
this.x509BundleSource = X509BundleSource;
this.acceptedSpiffeIdsSupplier = acceptedSpiffeIdsSupplier;
}
@ -54,10 +51,7 @@ public final class SpiffeTrustManager extends X509ExtendedTrustManager {
*/
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
val result = validatePeerChain(chain);
if (result.isError()) {
throw new CertificateException(result.getError());
}
validatePeerChain(chain);
}
/**
@ -74,10 +68,7 @@ public final class SpiffeTrustManager extends X509ExtendedTrustManager {
*/
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
val result = validatePeerChain(chain);
if (result.isError()) {
throw new CertificateException(result.getError());
}
validatePeerChain(chain);
}
@Override
@ -85,7 +76,9 @@ public final class SpiffeTrustManager extends X509ExtendedTrustManager {
return new X509Certificate[0];
}
/** {@link #checkClientTrusted(X509Certificate[], String)} */
/**
* {@link #checkClientTrusted(X509Certificate[], String)}
*/
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException {
checkClientTrusted(chain, authType);
@ -96,33 +89,25 @@ public final class SpiffeTrustManager extends X509ExtendedTrustManager {
checkServerTrusted(chain, authType);
}
/** {@link #checkClientTrusted(X509Certificate[], String)} */
/**
* {@link #checkClientTrusted(X509Certificate[], String)}
*/
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine sslEngine) throws CertificateException {
checkClientTrusted(chain, authType);
}
/** {@link #checkServerTrusted(X509Certificate[], String)} */
/**
* {@link #checkServerTrusted(X509Certificate[], String)}
*/
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine sslEngine) throws CertificateException {
checkServerTrusted(chain, authType);
}
// Check the spiffeId using the checkSpiffeId function and the chain using the bundleSource and a Validator
private Result<Boolean, String> validatePeerChain(X509Certificate[] chain) {
val spiffeId = CertificateUtils.getSpiffeId(chain[0]);
if (spiffeId.isError()) {
return Result.error(spiffeId.getError());
}
return X509SvidValidator
.verifySpiffeId(
spiffeId.getValue(),
acceptedSpiffeIdsSupplier)
.thenApply(
X509SvidValidator::verifyChain,
Arrays.asList(chain),
x509BundleSource
);
private void validatePeerChain(X509Certificate[] chain) throws CertificateException {
X509SvidValidator.verifySpiffeId(chain[0], acceptedSpiffeIdsSupplier);
X509SvidValidator.verifyChain(Arrays.asList(chain), x509BundleSource);
}
}

View File

@ -2,7 +2,6 @@ package spiffe.provider;
import lombok.val;
import spiffe.bundle.x509bundle.X509BundleSource;
import spiffe.result.Result;
import spiffe.spiffeid.SpiffeId;
import spiffe.spiffeid.SpiffeIdUtils;
@ -77,9 +76,9 @@ public class SpiffeTrustManagerFactory extends TrustManagerFactorySpi {
*/
public TrustManager[] engineGetTrustManagers(
X509BundleSource x509BundleSource,
Supplier<Result<List<SpiffeId>, String>> acceptedSpiffeIdsSupplier) {
Supplier<List<SpiffeId>> acceptedSpiffeIdsSupplier) {
Supplier<Result<List<SpiffeId>, String>> spiffeIdsSupplier;
Supplier<List<SpiffeId>> spiffeIdsSupplier;
if (acceptedSpiffeIdsSupplier != null) {
spiffeIdsSupplier = acceptedSpiffeIdsSupplier;
} else {
@ -104,7 +103,7 @@ public class SpiffeTrustManagerFactory extends TrustManagerFactorySpi {
}
private Result<List<SpiffeId>, String> getAcceptedSpiffeIds() {
private List<SpiffeId> getAcceptedSpiffeIds() {
return SpiffeIdUtils.getSpiffeIdsFromSecurityProperty(SSL_SPIFFE_ACCEPT_PROPERTY);
}
}

View File

@ -1,19 +1,20 @@
package spiffe.provider;
import lombok.val;
import spiffe.exception.SocketEndpointAddressException;
import spiffe.exception.X509SourceException;
import spiffe.workloadapi.X509Source;
/**
* A <code>X509SourceManager</code> is a Singleton that handles an instance of a X509Source.
* Uses the environment variable 'SPIFFE_ENDPOINT_SOCKET' to create a X509Source backed by the
* <p>
* The default SPIFFE socket enpoint address is used to create a X509Source backed by the
* Workload API.
* If the environment variable is not defined, it will throw an <code>IllegalStateException</code>.
* If the X509Source cannot be initialized, it will throw a <code>RuntimeException</code>.
* <p>
* @implNote The reason to have this Singleton is because we need to have
* a single X509Source instance to be used by the {@link SpiffeKeyManagerFactory}
* and {@link SpiffeTrustManagerFactory} to inject it in the {@link SpiffeKeyManager} and {@link SpiffeTrustManager}
* instances.
* @implNote This Singleton needed to be able to handle a single {@link X509Source} instance
* to be used by the {@link SpiffeKeyManagerFactory} and {@link SpiffeTrustManagerFactory} to inject it
* in the {@link SpiffeKeyManager} and {@link SpiffeTrustManager} instances.
*/
public enum X509SourceManager {
@ -22,15 +23,11 @@ public enum X509SourceManager {
private final X509Source x509Source;
X509SourceManager() {
val x509SourceResult =
X509Source.newSource();
if (x509SourceResult.isError()) {
// panic in case of error creating the X509Source
throw new RuntimeException(x509SourceResult.getError());
try {
x509Source = X509Source.newSource();
} catch (SocketEndpointAddressException e) {
throw new X509SourceException("Could not create X509 Source. Socket endpoint address is not valid", e);
}
// set the singleton instance
x509Source = x509SourceResult.getValue();
}
public X509Source getX509Source() {

View File

@ -1,9 +1,9 @@
package spiffe.provider.examples;
import lombok.val;
import spiffe.exception.SocketEndpointAddressException;
import spiffe.provider.SpiffeSslContextFactory;
import spiffe.provider.SpiffeSslContextFactory.SslContextOptions;
import spiffe.result.Result;
import spiffe.spiffeid.SpiffeId;
import spiffe.workloadapi.X509Source;
import spiffe.workloadapi.X509Source.X509SourceOptions;
@ -15,6 +15,8 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@ -32,62 +34,55 @@ import java.util.stream.Stream;
public class HttpsClient {
String spiffeSocket;
Supplier<Result<List<SpiffeId>, String>> acceptedSpiffeIdsListSupplier;
Supplier<List<SpiffeId>> acceptedSpiffeIdsListSupplier;
int serverPort;
public static void main(String[] args) throws IOException {
public static void main(String[] args) {
String spiffeSocket = "unix:/tmp/agent.sock";
HttpsClient httpsClient =
new HttpsClient(4000, spiffeSocket, HttpsClient::listOfSpiffeIds);
httpsClient.run();
HttpsClient httpsClient = new HttpsClient(4000, spiffeSocket, HttpsClient::listOfSpiffeIds);
try {
httpsClient.run();
} catch (KeyManagementException | NoSuchAlgorithmException | IOException | SocketEndpointAddressException e) {
throw new RuntimeException("Error starting Https Client", e);
}
}
HttpsClient(int serverPort, String spiffeSocket, Supplier<Result<List<SpiffeId>, String>> acceptedSpiffeIdsListSupplier) {
HttpsClient(int serverPort, String spiffeSocket, Supplier<List<SpiffeId>> acceptedSpiffeIdsListSupplier) {
this.serverPort = serverPort;
this.spiffeSocket = spiffeSocket;
this.acceptedSpiffeIdsListSupplier = acceptedSpiffeIdsListSupplier;
}
void run() throws IOException {
void run() throws IOException, SocketEndpointAddressException, KeyManagementException, NoSuchAlgorithmException {
val sourceOptions = X509SourceOptions
.builder()
.spiffeSocketPath(spiffeSocket)
.build();
val x509Source = X509Source.newSource(sourceOptions);
if (x509Source.isError()) {
throw new RuntimeException(x509Source.getError());
}
SslContextOptions sslContextOptions = SslContextOptions
.builder()
.acceptedSpiffeIdsSupplier(acceptedSpiffeIdsListSupplier)
.x509Source(x509Source.getValue())
.x509Source(x509Source)
.build();
Result<SSLContext, String> sslContext = SpiffeSslContextFactory
.getSslContext(sslContextOptions);
SSLContext sslContext = SpiffeSslContextFactory.getSslContext(sslContextOptions);
if (sslContext.isError()) {
throw new RuntimeException(sslContext.getError());
}
SSLSocketFactory sslSocketFactory = sslContext.getValue().getSocketFactory();
SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket("localhost", serverPort);
new WorkloadThread(sslSocket, x509Source.getValue()).start();
new WorkloadThread(sslSocket, x509Source).start();
}
static Result<List<SpiffeId>, String> listOfSpiffeIds() {
static List<SpiffeId> listOfSpiffeIds() {
try {
Path path = Paths.get("java-spiffe-provider/src/main/java/spiffe/provider/examples/spiffeIds.txt");
Stream<String> lines = Files.lines(path);
List<SpiffeId> list = lines
return lines
.map(SpiffeId::parse)
.map(Result::getValue)
.collect(Collectors.toList());
return Result.ok(list);
} catch (Exception e) {
return Result.error("Error getting list of accepted SPIFFE IDs: %s", e.getMessage());
throw new RuntimeException("Error getting list of spiffeIds", e);
}
}
}

View File

@ -1,9 +1,9 @@
package spiffe.provider.examples;
import lombok.val;
import spiffe.exception.SocketEndpointAddressException;
import spiffe.provider.SpiffeSslContextFactory;
import spiffe.provider.SpiffeSslContextFactory.SslContextOptions;
import spiffe.result.Result;
import spiffe.workloadapi.X509Source;
import javax.net.ssl.SSLContext;
@ -11,10 +11,12 @@ import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLServerSocketFactory;
import javax.net.ssl.SSLSocket;
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
/**
* Example of a simple HTTPS Server backed by the Workload API to get the X509 Certificates
* and trusted cert bundles.
* Example of a simple HTTPS Server backed by the Workload API to get the X509 certificates
* and trusted bundles.
* <p>
* The purpose of this class is to show the use of the {@link SpiffeSslContextFactory} to create
* a {@link SSLContext} that uses X509-SVID provided by a Workload API. The SSLContext uses the
@ -28,38 +30,41 @@ public class HttpsServer {
int port;
public static void main(String[] args) throws IOException {
public static void main(String[] args) {
HttpsServer httpsServer = new HttpsServer(4000);
httpsServer.run();
try {
httpsServer.run();
} catch (IOException | KeyManagementException | NoSuchAlgorithmException e) {
throw new RuntimeException("Error starting HttpsServer");
}
}
HttpsServer(int port ) {
this.port = port;
}
void run() throws IOException {
val x509Source = X509Source.newSource();
if (x509Source.isError()) {
throw new RuntimeException(x509Source.getError());
void run() throws IOException, KeyManagementException, NoSuchAlgorithmException {
X509Source x509Source = null;
try {
x509Source = X509Source.newSource();
} catch (SocketEndpointAddressException e) {
throw new RuntimeException(e);
}
val sslContextOptions = SslContextOptions
.builder()
.x509Source(x509Source.getValue())
.x509Source(x509Source)
.build();
Result<SSLContext, String> sslContext = SpiffeSslContextFactory.getSslContext(sslContextOptions);
if (sslContext.isError()) {
throw new RuntimeException(sslContext.getError());
}
SSLContext sslContext = SpiffeSslContextFactory.getSslContext(sslContextOptions);
SSLServerSocketFactory sslServerSocketFactory = sslContext.getValue().getServerSocketFactory();
SSLServerSocketFactory sslServerSocketFactory = sslContext.getServerSocketFactory();
SSLServerSocket sslServerSocket = (SSLServerSocket) sslServerSocketFactory.createServerSocket(port);
// Server will validate Client chain and SPIFFE ID
sslServerSocket.setNeedClientAuth(true);
SSLSocket sslSocket = (SSLSocket) sslServerSocket.accept();
new WorkloadThread(sslSocket, x509Source.getValue()).start();
new WorkloadThread(sslSocket, x509Source).start();
}
}

View File

@ -37,10 +37,10 @@ class WorkloadThread extends Thread {
PrintWriter printWriter = new PrintWriter(new OutputStreamWriter(outputStream));
SpiffeId peerSpiffeId = CertificateUtils
.getSpiffeId((X509Certificate) sslSession.getPeerCertificates()[0]).getValue();
.getSpiffeId((X509Certificate) sslSession.getPeerCertificates()[0]);
SpiffeId mySpiffeId = CertificateUtils
.getSpiffeId((X509Certificate) sslSession.getLocalCertificates()[0]).getValue();
.getSpiffeId((X509Certificate) sslSession.getLocalCertificates()[0]);
// Send message to peer
printWriter.printf("Hello %s, I'm %s", peerSpiffeId, mySpiffeId);

View File

@ -5,13 +5,14 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import spiffe.exception.X509SvidException;
import spiffe.internal.CertificateUtils;
import spiffe.result.Result;
import spiffe.svid.x509svid.X509Svid;
import spiffe.svid.x509svid.X509SvidSource;
import javax.net.ssl.X509KeyManager;
import java.nio.file.Paths;
import java.security.cert.CertificateException;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.when;
@ -26,32 +27,31 @@ public class SpiffeKeyManagerTest {
X509Svid x509Svid;
@BeforeEach
void setup() {
void setup() throws X509SvidException {
MockitoAnnotations.initMocks(this);
keyManager = (X509KeyManager) new SpiffeKeyManagerFactory().engineGetKeyManagers(x509SvidSource)[0];
x509Svid = X509Svid
.load(
Paths.get("../testdata/x509cert.pem"),
Paths.get("../testdata/pkcs8key.pem"))
.getValue();
Paths.get("../testdata/pkcs8key.pem"));
}
@Test
void getCertificateChain_returnsAnArrayOfX509Certificates() {
when(x509SvidSource.getX509Svid()).thenReturn(Result.ok(x509Svid));
void getCertificateChain_returnsAnArrayOfX509Certificates() throws CertificateException {
when(x509SvidSource.getX509Svid()).thenReturn(x509Svid);
val certificateChain = keyManager.getCertificateChain(DEFAULT_ALIAS);
val spiffeId = CertificateUtils.getSpiffeId(certificateChain[0]);
assertAll(
() -> assertEquals(1, certificateChain.length),
() -> assertEquals("spiffe://example.org/test", spiffeId.getValue().toString())
() -> assertEquals("spiffe://example.org/test", spiffeId.toString())
);
}
@Test
void getPrivateKey_aliasIsSpiffe_returnAPrivateKey() {
when(x509SvidSource.getX509Svid()).thenReturn(Result.ok(x509Svid));
when(x509SvidSource.getX509Svid()).thenReturn(x509Svid);
val privateKey = keyManager.getPrivateKey(DEFAULT_ALIAS);

View File

@ -8,12 +8,14 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import spiffe.bundle.x509bundle.X509Bundle;
import spiffe.bundle.x509bundle.X509BundleSource;
import spiffe.result.Result;
import spiffe.exception.BundleNotFoundException;
import spiffe.exception.X509SvidException;
import spiffe.spiffeid.SpiffeId;
import spiffe.spiffeid.TrustDomain;
import spiffe.svid.x509svid.X509Svid;
import javax.net.ssl.X509TrustManager;
import java.io.IOException;
import java.nio.file.Paths;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
@ -35,22 +37,19 @@ public class SpiffeTrustManagerTest {
X509TrustManager trustManager;
@BeforeAll
static void setupClass() {
static void setupClass() throws IOException, CertificateException, X509SvidException {
x509Svid = X509Svid
.load(
Paths.get("../testdata/x509cert.pem"),
Paths.get("../testdata/pkcs8key.pem"))
.getValue();
Paths.get("../testdata/pkcs8key.pem"));
otherX509Svid = X509Svid
.load(
Paths.get("../testdata/x509cert_other.pem"),
Paths.get("../testdata/key_other.pem"))
.getValue();
Paths.get("../testdata/key_other.pem"));
x509Bundle = X509Bundle
.load(
TrustDomain.of("example.org").getValue(),
Paths.get("../testdata/bundle.pem"))
.getValue();
TrustDomain.of("example.org"),
Paths.get("../testdata/bundle.pem"));
}
@BeforeEach
@ -60,63 +59,61 @@ public class SpiffeTrustManagerTest {
new SpiffeTrustManagerFactory()
.engineGetTrustManagers(
bundleSource,
() -> Result.ok(acceptedSpiffeIds))[0];
() -> acceptedSpiffeIds)[0];
}
@Test
void checkClientTrusted_passAExpiredCertificate_throwsException() {
void checkClientTrusted_passAExpiredCertificate_throwsException() throws BundleNotFoundException {
acceptedSpiffeIds =
Collections
.singletonList(
SpiffeId.parse("spiffe://example.org/test").getValue()
SpiffeId.parse("spiffe://example.org/test")
);
val chain = x509Svid.getChainArray();
when(bundleSource.getX509BundleForTrustDomain(TrustDomain.of("example.org").getValue())).thenReturn(Result.ok(x509Bundle));
when(bundleSource.getX509BundleForTrustDomain(TrustDomain.of("example.org"))).thenReturn(x509Bundle);
try {
trustManager.checkClientTrusted(chain, "");
fail("CertificateException was expected");
} catch (CertificateException e) {
assertTrue(e.getMessage().contains("CertificateExpiredException: NotAfter"));
assertEquals("java.security.cert.CertPathValidatorException: validity check failed", e.getMessage());
}
}
@Test
void checkClientTrusted_passCertificateWithNonAcceptedSpiffeId_ThrowCertificateException() {
void checkClientTrusted_passCertificateWithNonAcceptedSpiffeId_ThrowCertificateException() throws BundleNotFoundException {
acceptedSpiffeIds =
Collections
.singletonList(
SpiffeId.parse("spiffe://example.org/other").getValue()
SpiffeId.parse("spiffe://example.org/other")
);
X509Certificate[] chain = x509Svid.getChainArray();
when(bundleSource
.getX509BundleForTrustDomain(
TrustDomain.of("example.org").getValue()))
.thenReturn(Result.ok(x509Bundle));
when(bundleSource.getX509BundleForTrustDomain(TrustDomain.of("example.org")))
.thenReturn(x509Bundle);
try {
trustManager.checkClientTrusted(chain, "");
fail("CertificateException was expected");
} catch (CertificateException e) {
assertEquals("SPIFFE ID 'spiffe://example.org/test' is not accepted", e.getMessage());
assertEquals("SPIFFE ID spiffe://example.org/test in x509Certificate is not accepted", e.getMessage());
}
}
@Test
void checkClientTrusted_passCertificateThatDoesntChainToBundle_ThrowCertificateException() {
void checkClientTrusted_passCertificateThatDoesntChainToBundle_ThrowCertificateException() throws BundleNotFoundException {
acceptedSpiffeIds =
Collections
.singletonList(
SpiffeId.parse("spiffe://other.org/test").getValue()
SpiffeId.parse("spiffe://other.org/test")
);
val chain = otherX509Svid.getChainArray();
when(bundleSource.getX509BundleForTrustDomain(TrustDomain.of("other.org").getValue())).thenReturn(Result.ok(x509Bundle));
when(bundleSource.getX509BundleForTrustDomain(TrustDomain.of("other.org"))).thenReturn(x509Bundle);
try {
trustManager.checkClientTrusted(chain, "");
@ -127,56 +124,56 @@ public class SpiffeTrustManagerTest {
}
@Test
void checkServerTrusted_passAnExpiredCertificate_ThrowsException() {
void checkServerTrusted_passAnExpiredCertificate_ThrowsException() throws BundleNotFoundException {
acceptedSpiffeIds =
Collections
.singletonList(
SpiffeId.parse("spiffe://example.org/test").getValue()
SpiffeId.parse("spiffe://example.org/test")
);
val chain = x509Svid.getChainArray();
when(bundleSource.getX509BundleForTrustDomain(TrustDomain.of("example.org").getValue())).thenReturn(Result.ok(x509Bundle));
when(bundleSource.getX509BundleForTrustDomain(TrustDomain.of("example.org"))).thenReturn(x509Bundle);
try {
trustManager.checkServerTrusted(chain, "");
fail("CertificateException was expected");
} catch (CertificateException e) {
assertTrue(e.getMessage().contains("CertificateExpiredException: NotAfter"));
assertEquals("java.security.cert.CertPathValidatorException: validity check failed", e.getMessage());
}
}
@Test
void checkServerTrusted_passCertificateWithNonAcceptedSpiffeId_ThrowCertificateException() {
void checkServerTrusted_passCertificateWithNonAcceptedSpiffeId_ThrowCertificateException() throws BundleNotFoundException {
acceptedSpiffeIds =
Collections
.singletonList(
SpiffeId.parse("spiffe://example.org/other").getValue()
SpiffeId.parse("spiffe://example.org/other")
);
val chain = x509Svid.getChainArray();
when(bundleSource.getX509BundleForTrustDomain(TrustDomain.of("example.org").getValue())).thenReturn(Result.ok(x509Bundle));
when(bundleSource.getX509BundleForTrustDomain(TrustDomain.of("example.org"))).thenReturn(x509Bundle);
try {
trustManager.checkServerTrusted(chain, "");
fail("CertificateException was expected");
} catch (CertificateException e) {
assertEquals("SPIFFE ID 'spiffe://example.org/test' is not accepted", e.getMessage());
assertEquals("SPIFFE ID spiffe://example.org/test in x509Certificate is not accepted", e.getMessage());
}
}
@Test
void checkServerTrusted_passCertificateThatDoesntChainToBundle_ThrowCertificateException() {
void checkServerTrusted_passCertificateThatDoesntChainToBundle_ThrowCertificateException() throws BundleNotFoundException {
acceptedSpiffeIds =
Collections
.singletonList(
SpiffeId.parse("spiffe://other.org/test").getValue()
SpiffeId.parse("spiffe://other.org/test")
);
val chain = otherX509Svid.getChainArray();
when(bundleSource.getX509BundleForTrustDomain(TrustDomain.of("other.org").getValue())).thenReturn(Result.ok(x509Bundle));
when(bundleSource.getX509BundleForTrustDomain(TrustDomain.of("other.org"))).thenReturn(x509Bundle);
try {
trustManager.checkServerTrusted(chain, "");