Refactor error handling: use Exceptions instead of Result wrapper type
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
This commit is contained in:
parent
8e06cb12d7
commit
44cda6e809
|
|
@ -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");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())) {
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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, "");
|
||||
|
|
|
|||
Loading…
Reference in New Issue