Repository: nifi-registry
Updated Branches:
  refs/heads/master a87d42ee9 -> 785cb81ff


http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/ProxiedEntitiesUtils.java
----------------------------------------------------------------------
diff --git 
a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/ProxiedEntitiesUtils.java
 
b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/ProxiedEntitiesUtils.java
new file mode 100644
index 0000000..33015fc
--- /dev/null
+++ 
b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/ProxiedEntitiesUtils.java
@@ -0,0 +1,163 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.registry.web.security;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.authorization.user.NiFiUser;
+import org.apache.nifi.registry.authorization.user.NiFiUserUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ *
+ */
+public class ProxiedEntitiesUtils {
+    private static final Logger logger = 
LoggerFactory.getLogger(ProxiedEntitiesUtils.class);
+
+    public static final String PROXY_ENTITIES_CHAIN = "X-ProxiedEntitiesChain";
+    public static final String PROXY_ENTITIES_ACCEPTED = 
"X-ProxiedEntitiesAccepted";
+    public static final String PROXY_ENTITIES_DETAILS = 
"X-ProxiedEntitiesDetails";
+
+    private static final String GT = ">";
+    private static final String ESCAPED_GT = "\\\\>";
+    private static final String LT = "<";
+    private static final String ESCAPED_LT = "\\\\<";
+
+    private static final String ANONYMOUS_CHAIN = "<>";
+
+    /**
+     * Formats the specified DN to be set as a HTTP header using well known 
conventions.
+     *
+     * @param dn raw dn
+     * @return the dn formatted as an HTTP header
+     */
+    public static String formatProxyDn(String dn) {
+        return LT + sanitizeDn(dn) + GT;
+    }
+
+    /**
+     * If a user provides a DN with the sequence '><', they could escape the 
tokenization process and impersonate another user.
+     * <p>
+     * Example:
+     * <p>
+     * Provided DN: {@code jdoe><alopresto} -> {@code 
<jdoe><alopresto><proxy...>} would allow the user to impersonate jdoe
+     *
+     * @param rawDn the unsanitized DN
+     * @return the sanitized DN
+     */
+    private static String sanitizeDn(String rawDn) {
+        if (StringUtils.isEmpty(rawDn)) {
+            return rawDn;
+        } else {
+            String sanitizedDn = rawDn.replaceAll(GT, 
ESCAPED_GT).replaceAll(LT, ESCAPED_LT);
+            if (!sanitizedDn.equals(rawDn)) {
+                logger.warn("The provided DN [" + rawDn + "] contained 
dangerous characters that were escaped to [" + sanitizedDn + "]");
+            }
+            return sanitizedDn;
+        }
+    }
+
+    /**
+     * Reconstitutes the original DN from the sanitized version passed in the 
proxy chain.
+     * <p>
+     * Example:
+     * <p>
+     * {@code alopresto\>\<proxy1} -> {@code alopresto><proxy1}
+     *
+     * @param sanitizedDn the sanitized DN
+     * @return the original DN
+     */
+    private static String unsanitizeDn(String sanitizedDn) {
+        if (StringUtils.isEmpty(sanitizedDn)) {
+            return sanitizedDn;
+        } else {
+            String unsanitizedDn = sanitizedDn.replaceAll(ESCAPED_GT, 
GT).replaceAll(ESCAPED_LT, LT);
+            if (!unsanitizedDn.equals(sanitizedDn)) {
+                logger.warn("The provided DN [" + sanitizedDn + "] had been 
escaped, and was reconstituted to the dangerous DN [" + unsanitizedDn + "]");
+            }
+            return unsanitizedDn;
+        }
+    }
+
+    /**
+     * Tokenizes the specified proxy chain.
+     *
+     * @param rawProxyChain raw chain
+     * @return tokenized proxy chain
+     */
+    public static List<String> tokenizeProxiedEntitiesChain(String 
rawProxyChain) {
+        final List<String> proxyChain = new ArrayList<>();
+        if (!StringUtils.isEmpty(rawProxyChain)) {
+            // Split the String on the >< token
+            List<String> elements = 
Arrays.asList(StringUtils.splitByWholeSeparatorPreserveAllTokens(rawProxyChain, 
"><"));
+
+            // Unsanitize each DN and collect back
+            elements = 
elements.stream().map(ProxiedEntitiesUtils::unsanitizeDn).collect(Collectors.toList());
+
+            // Remove the leading < from the first element
+            elements.set(0, elements.get(0).replaceFirst(LT, ""));
+
+            // Remove the trailing > from the last element
+            int last = elements.size() - 1;
+            String lastElement = elements.get(last);
+            if (lastElement.endsWith(GT)) {
+                elements.set(last, lastElement.substring(0, 
lastElement.length() - 1));
+            }
+
+            proxyChain.addAll(elements);
+        }
+
+        return proxyChain;
+    }
+
+    /**
+     * Builds the proxy chain for the specified user.
+     *
+     * @param user The current user
+     * @return The proxy chain for that user in String form
+     */
+    public static String buildProxiedEntitiesChainString(final NiFiUser user) {
+        // calculate the dn chain
+        List<String> proxyChain = 
NiFiUserUtils.buildProxiedEntitiesChain(user);
+        if (proxyChain.isEmpty()) {
+            return ANONYMOUS_CHAIN;
+        }
+        proxyChain = 
proxyChain.stream().map(ProxiedEntitiesUtils::formatProxyDn).collect(Collectors.toList());
+        return StringUtils.join(proxyChain, "");
+    }
+
+    public static void successfulAuthorization(HttpServletRequest request, 
HttpServletResponse response, Authentication authResult) {
+        if (StringUtils.isNotBlank(request.getHeader(PROXY_ENTITIES_CHAIN))) {
+            response.setHeader(PROXY_ENTITIES_ACCEPTED, 
Boolean.TRUE.toString());
+        }
+    }
+
+    public static void unsuccessfulAuthorization(HttpServletRequest request, 
HttpServletResponse response, AuthenticationException failed) {
+        if (StringUtils.isNotBlank(request.getHeader(PROXY_ENTITIES_CHAIN))) {
+            response.setHeader(PROXY_ENTITIES_DETAILS, failed.getMessage());
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/UntrustedProxyException.java
----------------------------------------------------------------------
diff --git 
a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/UntrustedProxyException.java
 
b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/UntrustedProxyException.java
new file mode 100644
index 0000000..fad8cc1
--- /dev/null
+++ 
b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/UntrustedProxyException.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.registry.web.security;
+
+import org.springframework.security.core.AuthenticationException;
+
+/**
+ *
+ */
+public class UntrustedProxyException extends AuthenticationException {
+
+    public UntrustedProxyException(String msg) {
+        super(msg);
+    }
+
+    public UntrustedProxyException(String msg, Throwable t) {
+        super(msg, t);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/token/LoginAuthenticationToken.java
----------------------------------------------------------------------
diff --git 
a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/token/LoginAuthenticationToken.java
 
b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/token/LoginAuthenticationToken.java
new file mode 100644
index 0000000..cbb3b1f
--- /dev/null
+++ 
b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/token/LoginAuthenticationToken.java
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.registry.web.security.token;
+
+import org.apache.nifi.registry.web.security.util.CertificateUtils;
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+
+/**
+ * This is an Authentication Token for logging in. Once a user is 
authenticated, they can be issued an ID token.
+ */
+public class LoginAuthenticationToken extends AbstractAuthenticationToken {
+
+    private final String identity;
+    private final String username;
+    private final long expiration;
+    private final String issuer;
+
+    /**
+     * Creates a representation of the authentication token for a user.
+     *
+     * @param identity   The unique identifier for this user
+     * @param expiration The relative time to expiration in milliseconds
+     * @param issuer     The IdentityProvider implementation that generated 
this token
+     */
+    public LoginAuthenticationToken(final String identity, final long 
expiration, final String issuer) {
+        this(identity, null, expiration, issuer);
+    }
+
+    /**
+     * Creates a representation of the authentication token for a user.
+     *
+     * @param identity   The unique identifier for this user (cannot be null 
or empty)
+     * @param username   The preferred username for this user
+     * @param expiration The relative time to expiration in milliseconds
+     * @param issuer     The IdentityProvider implementation that generated 
this token
+     */
+    public LoginAuthenticationToken(final String identity, final String 
username, final long expiration, final String issuer) {
+        super(null);
+        setAuthenticated(true);
+        this.identity = identity;
+        this.username = username;
+        this.issuer = issuer;
+        Calendar now = Calendar.getInstance();
+        this.expiration = now.getTimeInMillis() + expiration;
+    }
+
+    @Override
+    public Object getCredentials() {
+        return null;
+    }
+
+    @Override
+    public Object getPrincipal() {
+        return identity;
+    }
+
+    /**
+     * Returns the expiration instant in milliseconds. This value is an 
absolute point in time (i.e. Nov
+     * 16, 2015 11:30:00.000 GMT), not a relative time (i.e. 60 minutes). It 
is calculated by adding the
+     * relative expiration from the constructor to the timestamp at object 
creation.
+     *
+     * @return the expiration in millis
+     */
+    public long getExpiration() {
+        return expiration;
+    }
+
+    public String getIssuer() {
+        return issuer;
+    }
+
+    @Override
+    public String getName() {
+        if (username == null) {
+            // if the username is a DN this will extract the username or CN... 
if not will return what was passed
+            return CertificateUtils.extractUsername(identity);
+        } else {
+            return username;
+        }
+    }
+
+    @Override
+    public String toString() {
+        Calendar expirationTime = Calendar.getInstance();
+        expirationTime.setTimeInMillis(getExpiration());
+        long remainingTime = expirationTime.getTimeInMillis() - 
Calendar.getInstance().getTimeInMillis();
+
+        SimpleDateFormat dateFormat = new SimpleDateFormat("dd-MM-yyyy 
HH:mm:ss.SSS");
+        dateFormat.setTimeZone(expirationTime.getTimeZone());
+        String expirationTimeString = 
dateFormat.format(expirationTime.getTime());
+
+        return new StringBuilder("LoginAuthenticationToken for ")
+                .append(getName())
+                .append(" issued by ")
+                .append(getIssuer())
+                .append(" expiring at ")
+                .append(expirationTimeString)
+                .append(" [")
+                .append(getExpiration())
+                .append(" ms, ")
+                .append(remainingTime)
+                .append(" ms remaining]")
+                .toString();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/token/NiFiAuthenticationToken.java
----------------------------------------------------------------------
diff --git 
a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/token/NiFiAuthenticationToken.java
 
b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/token/NiFiAuthenticationToken.java
new file mode 100644
index 0000000..5ea9e5b
--- /dev/null
+++ 
b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/token/NiFiAuthenticationToken.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.registry.web.security.token;
+
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.userdetails.UserDetails;
+
+/**
+ * An authentication token that represents an Authenticated and Authorized 
user of the NiFi Apis. The authorities are based off the specified UserDetails.
+ */
+public class NiFiAuthenticationToken extends AbstractAuthenticationToken {
+
+    final UserDetails nifiUserDetails;
+
+    public NiFiAuthenticationToken(final UserDetails nifiUserDetails) {
+        super(nifiUserDetails.getAuthorities());
+        super.setAuthenticated(true);
+        setDetails(nifiUserDetails);
+        this.nifiUserDetails = nifiUserDetails;
+    }
+
+    @Override
+    public Object getCredentials() {
+        return nifiUserDetails.getPassword();
+    }
+
+    @Override
+    public Object getPrincipal() {
+        return nifiUserDetails;
+    }
+
+    @Override
+    public final void setAuthenticated(boolean authenticated) {
+        throw new IllegalArgumentException("Cannot change the authenticated 
state.");
+    }
+
+    @Override
+    public String toString() {
+        return nifiUserDetails.getUsername();
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/token/OtpAuthenticationToken.java
----------------------------------------------------------------------
diff --git 
a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/token/OtpAuthenticationToken.java
 
b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/token/OtpAuthenticationToken.java
new file mode 100644
index 0000000..f49e97d
--- /dev/null
+++ 
b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/token/OtpAuthenticationToken.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.registry.web.security.token;
+
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+
+/**
+ * This is an Authentication Token for logging in. Once a user is 
authenticated, they can be issued an ID token.
+ */
+public class OtpAuthenticationToken extends AbstractAuthenticationToken {
+
+    private final String identity;
+
+    /**
+     * Creates a representation of the otp authentication token for a user.
+     *
+     * @param identity   The unique identifier for this user
+     */
+    public OtpAuthenticationToken(final String identity) {
+        super(null);
+        setAuthenticated(true);
+        this.identity = identity;
+    }
+
+    @Override
+    public Object getCredentials() {
+        return null;
+    }
+
+    @Override
+    public Object getPrincipal() {
+        return identity;
+    }
+
+    @Override
+    public String toString() {
+        return new StringBuilder("OtpAuthenticationToken for ")
+                .append(getName())
+                .toString();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/util/CertificateUtils.java
----------------------------------------------------------------------
diff --git 
a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/util/CertificateUtils.java
 
b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/util/CertificateUtils.java
new file mode 100644
index 0000000..3282c42
--- /dev/null
+++ 
b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/util/CertificateUtils.java
@@ -0,0 +1,668 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.registry.web.security.util;
+
+import org.apache.commons.lang3.StringUtils;
+import org.bouncycastle.asn1.ASN1Encodable;
+import org.bouncycastle.asn1.ASN1ObjectIdentifier;
+import org.bouncycastle.asn1.ASN1Set;
+import org.bouncycastle.asn1.DERSequence;
+import org.bouncycastle.asn1.pkcs.Attribute;
+import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
+import org.bouncycastle.asn1.x500.AttributeTypeAndValue;
+import org.bouncycastle.asn1.x500.RDN;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x500.style.BCStyle;
+import org.bouncycastle.asn1.x509.BasicConstraints;
+import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
+import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.asn1.x509.Extensions;
+import org.bouncycastle.asn1.x509.KeyPurposeId;
+import org.bouncycastle.asn1.x509.KeyUsage;
+import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
+import org.bouncycastle.cert.CertIOException;
+import org.bouncycastle.cert.X509CertificateHolder;
+import org.bouncycastle.cert.X509v3CertificateBuilder;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.operator.ContentSigner;
+import org.bouncycastle.operator.OperatorCreationException;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+import javax.naming.ldap.Rdn;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSocket;
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.net.Socket;
+import java.net.URL;
+import java.security.KeyPair;
+import java.security.KeyStore;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.Security;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+public final class CertificateUtils {
+    private static final Logger logger = 
LoggerFactory.getLogger(CertificateUtils.class);
+    private static final String PEER_NOT_AUTHENTICATED_MSG = "peer not 
authenticated";
+    private static final Map<ASN1ObjectIdentifier, Integer> dnOrderMap = 
createDnOrderMap();
+
+    static {
+        Security.addProvider(new BouncyCastleProvider());
+    }
+
+    /**
+     * The time in milliseconds that the last unique serial number was 
generated
+     */
+    private static long lastSerialNumberMillis = 0L;
+
+    /**
+     * An incrementor to add uniqueness to serial numbers generated in the 
same millisecond
+     */
+    private static int serialNumberIncrementor = 0;
+
+    /**
+     * BigInteger value to use for the base of the unique serial number
+     */
+    private static BigInteger millisecondBigInteger;
+
+    private static Map<ASN1ObjectIdentifier, Integer> createDnOrderMap() {
+        Map<ASN1ObjectIdentifier, Integer> orderMap = new HashMap<>();
+        int count = 0;
+        orderMap.put(BCStyle.CN, count++);
+        orderMap.put(BCStyle.L, count++);
+        orderMap.put(BCStyle.ST, count++);
+        orderMap.put(BCStyle.O, count++);
+        orderMap.put(BCStyle.OU, count++);
+        orderMap.put(BCStyle.C, count++);
+        orderMap.put(BCStyle.STREET, count++);
+        orderMap.put(BCStyle.DC, count++);
+        orderMap.put(BCStyle.UID, count++);
+        return Collections.unmodifiableMap(orderMap);
+    }
+
+    public enum ClientAuth {
+        NONE(0, "none"),
+        WANT(1, "want"),
+        NEED(2, "need");
+
+        private int value;
+        private String description;
+
+        ClientAuth(int value, String description) {
+            this.value = value;
+            this.description = description;
+        }
+
+        @Override
+        public String toString() {
+            return "Client Auth: " + this.description + " (" + this.value + 
")";
+        }
+    }
+
+    /**
+     * Returns true if the given keystore can be loaded using the given 
keystore type and password. Returns false otherwise.
+     *
+     * @param keystore     the keystore to validate
+     * @param keystoreType the type of the keystore
+     * @param password     the password to access the keystore
+     * @return true if valid; false otherwise
+     */
+    public static boolean isStoreValid(final URL keystore, final KeystoreType 
keystoreType, final char[] password) {
+
+        if (keystore == null) {
+            throw new IllegalArgumentException("keystore may not be null");
+        } else if (keystoreType == null) {
+            throw new IllegalArgumentException("keystore type may not be 
null");
+        } else if (password == null) {
+            throw new IllegalArgumentException("password may not be null");
+        }
+
+        BufferedInputStream bis = null;
+        final KeyStore ks;
+        try {
+
+            // load the keystore
+            bis = new BufferedInputStream(keystore.openStream());
+            ks = KeyStoreUtils.getKeyStore(keystoreType.name());
+            ks.load(bis, password);
+
+            return true;
+
+        } catch (Exception e) {
+            return false;
+        } finally {
+            if (bis != null) {
+                try {
+                    bis.close();
+                } catch (final IOException ioe) {
+                    logger.warn("Failed to close input stream", ioe);
+                }
+            }
+        }
+    }
+
+    /**
+     * Extracts the username from the specified DN. If the username cannot be 
extracted because the CN is in an unrecognized format, the entire CN is 
returned. If the CN cannot be extracted because
+     * the DN is in an unrecognized format, the entire DN is returned.
+     *
+     * @param dn the dn to extract the username from
+     * @return the exatracted username
+     */
+    public static String extractUsername(String dn) {
+        String username = dn;
+
+        // ensure the dn is specified
+        if (StringUtils.isNotBlank(dn)) {
+            // determine the separate
+            final String separator = StringUtils.indexOfIgnoreCase(dn, "/cn=") 
> 0 ? "/" : ",";
+
+            // attempt to locate the cd
+            final String cnPattern = "cn=";
+            final int cnIndex = StringUtils.indexOfIgnoreCase(dn, cnPattern);
+            if (cnIndex >= 0) {
+                int separatorIndex = StringUtils.indexOf(dn, separator, 
cnIndex);
+                if (separatorIndex > 0) {
+                    username = StringUtils.substring(dn, cnIndex + 
cnPattern.length(), separatorIndex);
+                } else {
+                    username = StringUtils.substring(dn, cnIndex + 
cnPattern.length());
+                }
+            }
+        }
+
+        return username;
+    }
+
+    /**
+     * Returns a list of subject alternative names. Any name that is 
represented as a String by X509Certificate.getSubjectAlternativeNames() is 
converted to lowercase and returned.
+     *
+     * @param certificate a certificate
+     * @return a list of subject alternative names; list is never null
+     * @throws CertificateParsingException if parsing the certificate failed
+     */
+    public static List<String> getSubjectAlternativeNames(final 
X509Certificate certificate) throws CertificateParsingException {
+
+        final Collection<List<?>> altNames = 
certificate.getSubjectAlternativeNames();
+        if (altNames == null) {
+            return new ArrayList<>();
+        }
+
+        final List<String> result = new ArrayList<>();
+        for (final List<?> generalName : altNames) {
+            /**
+             * generalName has the name type as the first element a String or 
byte array for the second element. We return any general names that are String 
types.
+             *
+             * We don't inspect the numeric name type because some 
certificates incorrectly put IPs and DNS names under the wrong name types.
+             */
+            final Object value = generalName.get(1);
+            if (value instanceof String) {
+                result.add(((String) value).toLowerCase());
+            }
+
+        }
+
+        return result;
+    }
+
+    /**
+     * Returns the DN extracted from the peer certificate (the server DN if 
run on the client; the client DN (if available) if run on the server).
+     *
+     * If the client auth setting is WANT or NONE and a client certificate is 
not present, this method will return {@code null}.
+     * If the client auth is NEED, it will throw a {@link 
CertificateException}.
+     *
+     * @param socket the SSL Socket
+     * @return the extracted DN
+     * @throws CertificateException if there is a problem parsing the 
certificate
+     */
+    public static String extractPeerDNFromSSLSocket(Socket socket) throws 
CertificateException {
+        String dn = null;
+        if (socket instanceof SSLSocket) {
+            final SSLSocket sslSocket = (SSLSocket) socket;
+
+            boolean clientMode = sslSocket.getUseClientMode();
+            logger.debug("SSL Socket in {} mode", clientMode ? "client" : 
"server");
+            ClientAuth clientAuth = getClientAuthStatus(sslSocket);
+            logger.debug("SSL Socket client auth status: {}", clientAuth);
+
+            if (clientMode) {
+                logger.debug("This socket is in client mode, so attempting to 
extract certificate from remote 'server' socket");
+               dn = extractPeerDNFromServerSSLSocket(sslSocket);
+            } else {
+                logger.debug("This socket is in server mode, so attempting to 
extract certificate from remote 'client' socket");
+               dn = extractPeerDNFromClientSSLSocket(sslSocket);
+            }
+        }
+
+        return dn;
+    }
+
+    /**
+     * Returns the DN extracted from the client certificate.
+     *
+     * If the client auth setting is WANT or NONE and a certificate is not 
present (and {@code respectClientAuth} is {@code true}), this method will 
return {@code null}.
+     * If the client auth is NEED, it will throw a {@link 
CertificateException}.
+     *
+     * @param sslSocket the SSL Socket
+     * @return the extracted DN
+     * @throws CertificateException if there is a problem parsing the 
certificate
+     */
+    private static String extractPeerDNFromClientSSLSocket(SSLSocket 
sslSocket) throws CertificateException {
+        String dn = null;
+
+            /** The clientAuth value can be "need", "want", or "none"
+             * A client must send client certificates for need, should for 
want, and will not for none.
+             * This method should throw an exception if none are provided for 
need, return null if none are provided for want, and return null (without 
checking) for none.
+             */
+
+            ClientAuth clientAuth = getClientAuthStatus(sslSocket);
+            logger.debug("SSL Socket client auth status: {}", clientAuth);
+
+            if (clientAuth != ClientAuth.NONE) {
+                try {
+                    final Certificate[] certChains = 
sslSocket.getSession().getPeerCertificates();
+                    if (certChains != null && certChains.length > 0) {
+                        X509Certificate x509Certificate = 
convertAbstractX509Certificate(certChains[0]);
+                        dn = x509Certificate.getSubjectDN().getName().trim();
+                        logger.debug("Extracted DN={} from client 
certificate", dn);
+                    }
+                } catch (SSLPeerUnverifiedException e) {
+                    if (e.getMessage().equals(PEER_NOT_AUTHENTICATED_MSG)) {
+                        logger.error("The incoming request did not contain 
client certificates and thus the DN cannot" +
+                                " be extracted. Check that the other endpoint 
is providing a complete client certificate chain");
+                    }
+                    if (clientAuth == ClientAuth.WANT) {
+                        logger.warn("Suppressing missing client certificate 
exception because client auth is set to 'want'");
+                        return dn;
+                    }
+                    throw new CertificateException(e);
+                }
+            }
+        return dn;
+    }
+
+    /**
+     * Returns the DN extracted from the server certificate.
+     *
+     * @param socket the SSL Socket
+     * @return the extracted DN
+     * @throws CertificateException if there is a problem parsing the 
certificate
+     */
+    private static String extractPeerDNFromServerSSLSocket(Socket socket) 
throws CertificateException {
+        String dn = null;
+        if (socket instanceof SSLSocket) {
+            final SSLSocket sslSocket = (SSLSocket) socket;
+                try {
+                    final Certificate[] certChains = 
sslSocket.getSession().getPeerCertificates();
+                    if (certChains != null && certChains.length > 0) {
+                        X509Certificate x509Certificate = 
convertAbstractX509Certificate(certChains[0]);
+                        dn = x509Certificate.getSubjectDN().getName().trim();
+                        logger.debug("Extracted DN={} from server 
certificate", dn);
+                    }
+                } catch (SSLPeerUnverifiedException e) {
+                    if (e.getMessage().equals(PEER_NOT_AUTHENTICATED_MSG)) {
+                        logger.error("The server did not present a certificate 
and thus the DN cannot" +
+                                " be extracted. Check that the other endpoint 
is providing a complete certificate chain");
+                    }
+                    throw new CertificateException(e);
+                }
+        }
+        return dn;
+    }
+
+    private static ClientAuth getClientAuthStatus(SSLSocket sslSocket) {
+        return sslSocket.getNeedClientAuth() ? ClientAuth.NEED : 
sslSocket.getWantClientAuth() ? ClientAuth.WANT : ClientAuth.NONE;
+    }
+
+    /**
+     * Accepts a legacy {@link javax.security.cert.X509Certificate} and 
returns an {@link X509Certificate}. The {@code javax.*} package certificate 
classes are for legacy compatibility and should
+     * not be used for new development.
+     *
+     * @param legacyCertificate the {@code javax.security.cert.X509Certificate}
+     * @return a new {@code java.security.cert.X509Certificate}
+     * @throws CertificateException if there is an error generating the new 
certificate
+     */
+    public static X509Certificate 
convertLegacyX509Certificate(javax.security.cert.X509Certificate 
legacyCertificate) throws CertificateException {
+        if (legacyCertificate == null) {
+            throw new IllegalArgumentException("The X.509 certificate cannot 
be null");
+        }
+
+        try {
+            return formX509Certificate(legacyCertificate.getEncoded());
+        } catch (javax.security.cert.CertificateEncodingException e) {
+            throw new CertificateException(e);
+        }
+    }
+
+    /**
+     * Accepts an abstract {@link Certificate} and returns an {@link 
X509Certificate}. Because {@code sslSocket.getSession().getPeerCertificates()} 
returns an array of the
+     * abstract certificates, they must be translated to X.509 to replace the 
functionality of {@code sslSocket.getSession().getPeerCertificateChain()}.
+     *
+     * @param abstractCertificate the {@code java.security.cert.Certificate}
+     * @return a new {@code java.security.cert.X509Certificate}
+     * @throws CertificateException if there is an error generating the new 
certificate
+     */
+    public static X509Certificate convertAbstractX509Certificate(Certificate 
abstractCertificate) throws CertificateException {
+        if (abstractCertificate == null || !(abstractCertificate instanceof 
X509Certificate)) {
+            throw new IllegalArgumentException("The certificate cannot be null 
and must be an X.509 certificate");
+        }
+
+        try {
+            return formX509Certificate(abstractCertificate.getEncoded());
+        } catch (java.security.cert.CertificateEncodingException e) {
+            throw new CertificateException(e);
+        }
+    }
+
+    private static X509Certificate formX509Certificate(byte[] 
encodedCertificate) throws CertificateException {
+        try {
+            CertificateFactory cf = CertificateFactory.getInstance("X.509");
+            ByteArrayInputStream bais = new 
ByteArrayInputStream(encodedCertificate);
+            return (X509Certificate) cf.generateCertificate(bais);
+        } catch (CertificateException e) {
+            logger.error("Error converting the certificate", e);
+            throw e;
+        }
+    }
+
+    /**
+     * Reorders DN to the order the elements appear in the RFC 2253 table
+     *
+     * https://www.ietf.org/rfc/rfc2253.txt
+     *
+     * String  X.500 AttributeType
+     * ------------------------------
+     * CN      commonName
+     * L       localityName
+     * ST      stateOrProvinceName
+     * O       organizationName
+     * OU      organizationalUnitName
+     * C       countryName
+     * STREET  streetAddress
+     * DC      domainComponent
+     * UID     userid
+     *
+     * @param dn a possibly unordered DN
+     * @return the ordered dn
+     */
+    public static String reorderDn(String dn) {
+        RDN[] rdNs = new X500Name(dn).getRDNs();
+        Arrays.sort(rdNs, new Comparator<RDN>() {
+            @Override
+            public int compare(RDN o1, RDN o2) {
+                AttributeTypeAndValue o1First = o1.getFirst();
+                AttributeTypeAndValue o2First = o2.getFirst();
+
+                ASN1ObjectIdentifier o1Type = o1First.getType();
+                ASN1ObjectIdentifier o2Type = o2First.getType();
+
+                Integer o1Rank = dnOrderMap.get(o1Type);
+                Integer o2Rank = dnOrderMap.get(o2Type);
+                if (o1Rank == null) {
+                    if (o2Rank == null) {
+                        int idComparison = 
o1Type.getId().compareTo(o2Type.getId());
+                        if (idComparison != 0) {
+                            return idComparison;
+                        }
+                        return 
String.valueOf(o1Type).compareTo(String.valueOf(o2Type));
+                    }
+                    return 1;
+                } else if (o2Rank == null) {
+                    return -1;
+                }
+                return o1Rank - o2Rank;
+            }
+        });
+        return new X500Name(rdNs).toString();
+    }
+
+    /**
+     * Reverses the X500Name in order make the certificate be in the right 
order
+     * [see 
http://stackoverflow.com/questions/7567837/attributes-reversed-in-certificate-subject-and-issuer/12645265]
+     *
+     * @param x500Name the X500Name created with the intended order
+     * @return the X500Name reversed
+     */
+    private static X500Name reverseX500Name(X500Name x500Name) {
+        List<RDN> rdns = Arrays.asList(x500Name.getRDNs());
+        Collections.reverse(rdns);
+        return new X500Name(rdns.toArray(new RDN[rdns.size()]));
+    }
+
+    /**
+     * Generates a unique serial number by using the current time in 
milliseconds left shifted 32 bits (to make room for incrementor) with an 
incrementor added
+     *
+     * @return a unique serial number (technically unique to this classloader)
+     */
+    protected static synchronized BigInteger getUniqueSerialNumber() {
+        final long currentTimeMillis = System.currentTimeMillis();
+        final int incrementorValue;
+
+        if (lastSerialNumberMillis != currentTimeMillis) {
+            // We can only get into this block once per millisecond
+            millisecondBigInteger = 
BigInteger.valueOf(currentTimeMillis).shiftLeft(32);
+            lastSerialNumberMillis = currentTimeMillis;
+            incrementorValue = 0;
+            serialNumberIncrementor = 1;
+        } else {
+            // Already created at least one serial number this millisecond
+            incrementorValue = serialNumberIncrementor++;
+        }
+
+        return millisecondBigInteger.add(BigInteger.valueOf(incrementorValue));
+    }
+
+    /**
+     * Generates a self-signed {@link X509Certificate} suitable for use as a 
Certificate Authority.
+     *
+     * @param keyPair                 the {@link KeyPair} to generate the 
{@link X509Certificate} for
+     * @param dn                      the distinguished name to user for the 
{@link X509Certificate}
+     * @param signingAlgorithm        the signing algorithm to use for the 
{@link X509Certificate}
+     * @param certificateDurationDays the duration in days for which the 
{@link X509Certificate} should be valid
+     * @return a self-signed {@link X509Certificate} suitable for use as a 
Certificate Authority
+     * @throws CertificateException      if there is an generating the new 
certificate
+     */
+    public static X509Certificate generateSelfSignedX509Certificate(KeyPair 
keyPair, String dn, String signingAlgorithm, int certificateDurationDays)
+            throws CertificateException {
+        try {
+            ContentSigner sigGen = new 
JcaContentSignerBuilder(signingAlgorithm).setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate());
+            SubjectPublicKeyInfo subPubKeyInfo = 
SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded());
+            Date startDate = new Date();
+            Date endDate = new Date(startDate.getTime() + 
TimeUnit.DAYS.toMillis(certificateDurationDays));
+
+            X509v3CertificateBuilder certBuilder = new 
X509v3CertificateBuilder(
+                    reverseX500Name(new X500Name(dn)),
+                    getUniqueSerialNumber(),
+                    startDate, endDate,
+                    reverseX500Name(new X500Name(dn)),
+                    subPubKeyInfo);
+
+            // Set certificate extensions
+            // (1) digitalSignature extension
+            certBuilder.addExtension(Extension.keyUsage, true, new 
KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment | 
KeyUsage.dataEncipherment
+                    | KeyUsage.keyAgreement | KeyUsage.nonRepudiation | 
KeyUsage.cRLSign | KeyUsage.keyCertSign));
+
+            certBuilder.addExtension(Extension.basicConstraints, false, new 
BasicConstraints(true));
+
+            certBuilder.addExtension(Extension.subjectKeyIdentifier, false, 
new JcaX509ExtensionUtils().createSubjectKeyIdentifier(keyPair.getPublic()));
+
+            certBuilder.addExtension(Extension.authorityKeyIdentifier, false, 
new JcaX509ExtensionUtils().createAuthorityKeyIdentifier(keyPair.getPublic()));
+
+            // (2) extendedKeyUsage extension
+            certBuilder.addExtension(Extension.extendedKeyUsage, false, new 
ExtendedKeyUsage(new KeyPurposeId[]{KeyPurposeId.id_kp_clientAuth, 
KeyPurposeId.id_kp_serverAuth}));
+
+            // Sign the certificate
+            X509CertificateHolder certificateHolder = 
certBuilder.build(sigGen);
+            return new 
JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certificateHolder);
+        } catch (CertIOException | NoSuchAlgorithmException | 
OperatorCreationException e) {
+            throw new CertificateException(e);
+        }
+    }
+
+    /**
+     * Generates an issued {@link X509Certificate} from the given issuer 
certificate and {@link KeyPair}
+     *
+     * @param dn the distinguished name to use
+     * @param publicKey the public key to issue the certificate to
+     * @param issuer the issuer's certificate
+     * @param issuerKeyPair the issuer's keypair
+     * @param signingAlgorithm the signing algorithm to use
+     * @param days the number of days it should be valid for
+     * @return an issued {@link X509Certificate} from the given issuer 
certificate and {@link KeyPair}
+     * @throws CertificateException if there is an error issuing the 
certificate
+     */
+    public static X509Certificate generateIssuedCertificate(String dn, 
PublicKey publicKey, X509Certificate issuer, KeyPair issuerKeyPair, String 
signingAlgorithm, int days)
+            throws CertificateException {
+        return generateIssuedCertificate(dn, publicKey, null, issuer, 
issuerKeyPair, signingAlgorithm, days);
+    }
+
+    /**
+     * Generates an issued {@link X509Certificate} from the given issuer 
certificate and {@link KeyPair}
+     *
+     * @param dn the distinguished name to use
+     * @param publicKey the public key to issue the certificate to
+     * @param extensions extensions extracted from the CSR
+     * @param issuer the issuer's certificate
+     * @param issuerKeyPair the issuer's keypair
+     * @param signingAlgorithm the signing algorithm to use
+     * @param days the number of days it should be valid for
+     * @return an issued {@link X509Certificate} from the given issuer 
certificate and {@link KeyPair}
+     * @throws CertificateException if there is an error issuing the 
certificate
+     */
+    public static X509Certificate generateIssuedCertificate(String dn, 
PublicKey publicKey, Extensions extensions, X509Certificate issuer, KeyPair 
issuerKeyPair, String signingAlgorithm, int days)
+            throws CertificateException {
+        try {
+            ContentSigner sigGen = new 
JcaContentSignerBuilder(signingAlgorithm).setProvider(BouncyCastleProvider.PROVIDER_NAME).build(issuerKeyPair.getPrivate());
+            SubjectPublicKeyInfo subPubKeyInfo = 
SubjectPublicKeyInfo.getInstance(publicKey.getEncoded());
+            Date startDate = new Date();
+            Date endDate = new Date(startDate.getTime() + 
TimeUnit.DAYS.toMillis(days));
+
+            X509v3CertificateBuilder certBuilder = new 
X509v3CertificateBuilder(
+                    reverseX500Name(new 
X500Name(issuer.getSubjectX500Principal().getName())),
+                    getUniqueSerialNumber(),
+                    startDate, endDate,
+                    reverseX500Name(new X500Name(dn)),
+                    subPubKeyInfo);
+
+            certBuilder.addExtension(Extension.subjectKeyIdentifier, false, 
new JcaX509ExtensionUtils().createSubjectKeyIdentifier(publicKey));
+
+            certBuilder.addExtension(Extension.authorityKeyIdentifier, false, 
new 
JcaX509ExtensionUtils().createAuthorityKeyIdentifier(issuerKeyPair.getPublic()));
+            // Set certificate extensions
+            // (1) digitalSignature extension
+            certBuilder.addExtension(Extension.keyUsage, true,
+                    new KeyUsage(KeyUsage.digitalSignature | 
KeyUsage.keyEncipherment | KeyUsage.dataEncipherment | KeyUsage.keyAgreement | 
KeyUsage.nonRepudiation));
+
+            certBuilder.addExtension(Extension.basicConstraints, false, new 
BasicConstraints(false));
+
+            // (2) extendedKeyUsage extension
+            certBuilder.addExtension(Extension.extendedKeyUsage, false, new 
ExtendedKeyUsage(new KeyPurposeId[]{KeyPurposeId.id_kp_clientAuth, 
KeyPurposeId.id_kp_serverAuth}));
+
+            // (3) subjectAlternativeName
+            if(extensions != null && 
extensions.getExtension(Extension.subjectAlternativeName) != null) {
+                certBuilder.addExtension(Extension.subjectAlternativeName, 
false, extensions.getExtensionParsedValue(Extension.subjectAlternativeName));
+            }
+
+            X509CertificateHolder certificateHolder = 
certBuilder.build(sigGen);
+            return new 
JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certificateHolder);
+        } catch (CertIOException | NoSuchAlgorithmException | 
OperatorCreationException e) {
+            throw new CertificateException(e);
+        }
+    }
+
+    /**
+     * Returns true if the two provided DNs are equivalent, regardless of the 
order of the elements. Returns false if one or both are invalid DNs.
+     *
+     * Example:
+     *
+     * CN=test1, O=testOrg, C=US compared to CN=test1, O=testOrg, C=US -> true
+     * CN=test1, O=testOrg, C=US compared to O=testOrg, CN=test1, C=US -> true
+     * CN=test1, O=testOrg, C=US compared to CN=test2, O=testOrg, C=US -> false
+     * CN=test1, O=testOrg, C=US compared to O=testOrg, CN=test2, C=US -> false
+     * CN=test1, O=testOrg, C=US compared to                           -> false
+     *                           compared to                           -> true
+     *
+     * @param dn1 the first DN to compare
+     * @param dn2 the second DN to compare
+     * @return true if the DNs are equivalent, false otherwise
+     */
+    public static boolean compareDNs(String dn1, String dn2) {
+        if (dn1 == null) {
+            dn1 = "";
+        }
+
+        if (dn2 == null) {
+            dn2 = "";
+        }
+
+        if (StringUtils.isEmpty(dn1) || StringUtils.isEmpty(dn2)) {
+            return dn1.equals(dn2);
+        }
+        try {
+            List<Rdn> rdn1 = new LdapName(dn1).getRdns();
+            List<Rdn> rdn2 = new LdapName(dn2).getRdns();
+
+            return rdn1.size() == rdn2.size() && rdn1.containsAll(rdn2);
+        } catch (InvalidNameException e) {
+            logger.warn("Cannot compare DNs: {} and {} because one or both is 
not a valid DN", dn1, dn2);
+            return false;
+        }
+    }
+
+    /**
+     * Extract extensions from CSR object
+     */
+    public static Extensions 
getExtensionsFromCSR(JcaPKCS10CertificationRequest csr) {
+        Attribute[] attributess = 
csr.getAttributes(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest);
+        for (Attribute attribute : attributess) {
+            ASN1Set attValue = attribute.getAttrValues();
+            if (attValue != null) {
+                ASN1Encodable extension = attValue.getObjectAt(0);
+                if (extension instanceof Extensions) {
+                    return (Extensions) extension;
+                } else if (extension instanceof DERSequence) {
+                    return Extensions.getInstance(extension);
+                }
+            }
+        }
+        return null;
+    }
+
+    private CertificateUtils() {
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/util/KeyStoreUtils.java
----------------------------------------------------------------------
diff --git 
a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/util/KeyStoreUtils.java
 
b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/util/KeyStoreUtils.java
new file mode 100644
index 0000000..877c5b7
--- /dev/null
+++ 
b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/util/KeyStoreUtils.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.nifi.registry.web.security.util;
+
+import org.apache.commons.lang3.StringUtils;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.Security;
+
+public class KeyStoreUtils {
+    private static final Logger logger = 
LoggerFactory.getLogger(KeyStoreUtils.class);
+
+    static {
+        Security.addProvider(new BouncyCastleProvider());
+    }
+
+    /**
+     * Returns the provider that will be used for the given keyStoreType
+     *
+     * @param keyStoreType the keyStoreType
+     * @return the provider that will be used
+     */
+    public static String getKeyStoreProvider(String keyStoreType) {
+        if (KeystoreType.PKCS12.toString().equalsIgnoreCase(keyStoreType)) {
+            return BouncyCastleProvider.PROVIDER_NAME;
+        }
+        return null;
+    }
+
+    /**
+     * Returns an empty KeyStore backed by the appropriate provider
+     *
+     * @param keyStoreType the keyStoreType
+     * @return an empty KeyStore
+     * @throws KeyStoreException if a KeyStore of the given type cannot be 
instantiated
+     */
+    public static KeyStore getKeyStore(String keyStoreType) throws 
KeyStoreException {
+        String keyStoreProvider = getKeyStoreProvider(keyStoreType);
+        if (StringUtils.isNotEmpty(keyStoreProvider)) {
+            try {
+                return KeyStore.getInstance(keyStoreType, keyStoreProvider);
+            } catch (Exception e) {
+                logger.error("Unable to load " + keyStoreProvider + " " + 
keyStoreType
+                        + " keystore.  This may cause issues getting trusted 
CA certificates as well as Certificate Chains for use in TLS.", e);
+            }
+        }
+        return KeyStore.getInstance(keyStoreType);
+    }
+
+    /**
+     * Returns an empty KeyStore intended for use as a TrustStore backed by 
the appropriate provider
+     *
+     * @param trustStoreType the trustStoreType
+     * @return an empty KeyStore
+     * @throws KeyStoreException if a KeyStore of the given type cannot be 
instantiated
+     */
+    public static KeyStore getTrustStore(String trustStoreType) throws 
KeyStoreException {
+        if (KeystoreType.PKCS12.toString().equalsIgnoreCase(trustStoreType)) {
+            logger.warn(trustStoreType + " truststores are deprecated.  " + 
KeystoreType.JKS.toString() + " is preferred.");
+        }
+        return getKeyStore(trustStoreType);
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/util/KeystoreType.java
----------------------------------------------------------------------
diff --git 
a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/util/KeystoreType.java
 
b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/util/KeystoreType.java
new file mode 100644
index 0000000..f519b58
--- /dev/null
+++ 
b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/util/KeystoreType.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.registry.web.security.util;
+
+/**
+ * Keystore types.
+ */
+public enum KeystoreType {
+    PKCS12,
+    JKS;
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/SubjectDnX509PrincipalExtractor.java
----------------------------------------------------------------------
diff --git 
a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/SubjectDnX509PrincipalExtractor.java
 
b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/SubjectDnX509PrincipalExtractor.java
new file mode 100644
index 0000000..f8ab1fe
--- /dev/null
+++ 
b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/SubjectDnX509PrincipalExtractor.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.registry.web.security.x509;
+
+import 
org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
+
+import java.security.cert.X509Certificate;
+
+/**
+ * Principal extractor for extracting a DN.
+ */
+public class SubjectDnX509PrincipalExtractor implements X509PrincipalExtractor 
{
+
+    @Override
+    public Object extractPrincipal(X509Certificate cert) {
+        return cert.getSubjectDN().getName().trim();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509AuthenticationFilter.java
----------------------------------------------------------------------
diff --git 
a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509AuthenticationFilter.java
 
b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509AuthenticationFilter.java
new file mode 100644
index 0000000..df8bbbf
--- /dev/null
+++ 
b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509AuthenticationFilter.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.registry.web.security.x509;
+
+import org.apache.nifi.registry.web.security.NiFiAuthenticationFilter;
+import org.apache.nifi.registry.web.security.ProxiedEntitiesUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.core.Authentication;
+import 
org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
+
+import javax.servlet.http.HttpServletRequest;
+import java.security.cert.X509Certificate;
+
+/**
+ * Custom X509 filter that will inspect the HTTP headers for a proxied user 
before extracting the user details from the client certificate.
+ */
+public class X509AuthenticationFilter extends NiFiAuthenticationFilter {
+
+    private static final Logger logger = 
LoggerFactory.getLogger(X509AuthenticationFilter.class);
+
+    private X509CertificateExtractor certificateExtractor;
+    private X509PrincipalExtractor principalExtractor;
+
+    @Override
+    public Authentication attemptAuthentication(final HttpServletRequest 
request) {
+        // only suppport x509 login when running securely
+        if (!request.isSecure()) {
+            return null;
+        }
+
+        // look for a client certificate
+        final X509Certificate[] certificates = 
certificateExtractor.extractClientCertificate(request);
+        if (certificates == null) {
+            return null;
+        }
+
+        return new 
X509AuthenticationRequestToken(request.getHeader(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN),
 principalExtractor, certificates, request.getRemoteAddr());
+    }
+
+    /* setters */
+    public void setCertificateExtractor(X509CertificateExtractor 
certificateExtractor) {
+        this.certificateExtractor = certificateExtractor;
+    }
+
+    public void setPrincipalExtractor(X509PrincipalExtractor 
principalExtractor) {
+        this.principalExtractor = principalExtractor;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509AuthenticationProvider.java
----------------------------------------------------------------------
diff --git 
a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509AuthenticationProvider.java
 
b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509AuthenticationProvider.java
new file mode 100644
index 0000000..5ed7859
--- /dev/null
+++ 
b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509AuthenticationProvider.java
@@ -0,0 +1,165 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.registry.web.security.x509;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.authorization.exception.AccessDeniedException;
+import org.apache.nifi.registry.authorization.Authorizer;
+import org.apache.nifi.registry.authorization.RequestAction;
+import org.apache.nifi.registry.authorization.Resource;
+import org.apache.nifi.registry.authorization.UserContextKeys;
+import org.apache.nifi.registry.authorization.resource.Authorizable;
+import org.apache.nifi.registry.authorization.resource.ResourceFactory;
+import org.apache.nifi.registry.authorization.user.NiFiUser;
+import org.apache.nifi.registry.authorization.user.NiFiUserDetails;
+import org.apache.nifi.registry.authorization.user.StandardNiFiUser;
+import org.apache.nifi.registry.properties.NiFiRegistryProperties;
+import org.apache.nifi.registry.web.response.AuthenticationResponse;
+import org.apache.nifi.registry.web.security.InvalidAuthenticationException;
+import org.apache.nifi.registry.web.security.NiFiAuthenticationProvider;
+import org.apache.nifi.registry.web.security.ProxiedEntitiesUtils;
+import org.apache.nifi.registry.web.security.UntrustedProxyException;
+import org.apache.nifi.registry.web.security.token.NiFiAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ *
+ */
+public class X509AuthenticationProvider extends NiFiAuthenticationProvider {
+
+    private static final Authorizable PROXY_AUTHORIZABLE = new Authorizable() {
+        @Override
+        public Authorizable getParentAuthorizable() {
+            return null;
+        }
+
+        @Override
+        public Resource getResource() {
+            return ResourceFactory.getProxyResource();
+        }
+    };
+
+    private X509IdentityProvider certificateIdentityProvider;
+    private Authorizer authorizer;
+
+    public X509AuthenticationProvider(
+            final X509IdentityProvider certificateIdentityProvider,
+            final Authorizer authorizer,
+            final NiFiRegistryProperties properties) {
+        super(properties, authorizer);
+        this.certificateIdentityProvider = certificateIdentityProvider;
+        this.authorizer = authorizer;
+    }
+
+    @Override
+    public Authentication authenticate(Authentication authentication) throws 
AuthenticationException {
+        final X509AuthenticationRequestToken request = 
(X509AuthenticationRequestToken) authentication;
+
+        // attempt to authenticate if certificates were found
+        final AuthenticationResponse authenticationResponse;
+        try {
+            authenticationResponse = 
certificateIdentityProvider.authenticate(request.getCertificates());
+        } catch (final IllegalArgumentException iae) {
+            throw new InvalidAuthenticationException(iae.getMessage(), iae);
+        }
+
+        if (StringUtils.isBlank(request.getProxiedEntitiesChain())) {
+            final String mappedIdentity = 
mapIdentity(authenticationResponse.getIdentity());
+            return new NiFiAuthenticationToken(new NiFiUserDetails(
+                    new StandardNiFiUser.Builder()
+                            .identity(mappedIdentity)
+                            .groups(getUserGroups(mappedIdentity))
+                            .clientAddress(request.getClientAddress())
+                            .build()));
+        } else {
+            // build the entire proxy chain if applicable - 
<end-user><proxy1><proxy2>
+            final List<String> proxyChain = new 
ArrayList<>(ProxiedEntitiesUtils.tokenizeProxiedEntitiesChain(request.getProxiedEntitiesChain()));
+            proxyChain.add(authenticationResponse.getIdentity());
+
+            // add the chain as appropriate to each proxy
+            NiFiUser proxy = null;
+            for (final ListIterator<String> chainIter = 
proxyChain.listIterator(proxyChain.size()); chainIter.hasPrevious(); ) {
+                String identity = chainIter.previous();
+
+                // determine if the user is anonymous
+                final boolean isAnonymous = StringUtils.isBlank(identity);
+                if (isAnonymous) {
+                    identity = StandardNiFiUser.ANONYMOUS_IDENTITY;
+                } else {
+                    identity = mapIdentity(identity);
+                }
+
+                final Set<String> groups = getUserGroups(identity);
+
+                // Only set the client address for client making the request 
because we don't know the clientAddress of the proxied entities
+                String clientAddress = (proxy == null) ? 
request.getClientAddress() : null;
+                proxy = createUser(identity, groups, proxy, clientAddress, 
isAnonymous);
+
+                if (chainIter.hasPrevious()) {
+                    try {
+                        PROXY_AUTHORIZABLE.authorize(authorizer, 
RequestAction.WRITE, proxy);
+                    } catch (final AccessDeniedException e) {
+                        throw new 
UntrustedProxyException(String.format("Untrusted proxy %s", identity));
+                    }
+                }
+            }
+
+            return new NiFiAuthenticationToken(new NiFiUserDetails(proxy));
+        }
+    }
+
+    /**
+     * Returns a regular user populated with the provided values, or if the 
user should be anonymous, a well-formed instance of the anonymous user with the 
provided values.
+     *
+     * @param identity      the user's identity
+     * @param chain         the proxied entities
+     * @param clientAddress the requesting IP address
+     * @param isAnonymous   if true, an anonymous user will be returned 
(identity will be ignored)
+     * @return the populated user
+     */
+    protected static NiFiUser createUser(String identity, Set<String> groups, 
NiFiUser chain, String clientAddress, boolean isAnonymous) {
+        if (isAnonymous) {
+            return StandardNiFiUser.populateAnonymousUser(chain, 
clientAddress);
+        } else {
+            return new 
StandardNiFiUser.Builder().identity(identity).groups(groups).chain(chain).clientAddress(clientAddress).build();
+        }
+    }
+
+    private Map<String, String> getUserContext(final 
X509AuthenticationRequestToken request) {
+        final Map<String, String> userContext;
+        if (!StringUtils.isBlank(request.getClientAddress())) {
+            userContext = new HashMap<>();
+            userContext.put(UserContextKeys.CLIENT_ADDRESS.name(), 
request.getClientAddress());
+        } else {
+            userContext = null;
+        }
+        return userContext;
+    }
+
+    @Override
+    public boolean supports(Class<?> authentication) {
+        return 
X509AuthenticationRequestToken.class.isAssignableFrom(authentication);
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509AuthenticationRequestToken.java
----------------------------------------------------------------------
diff --git 
a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509AuthenticationRequestToken.java
 
b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509AuthenticationRequestToken.java
new file mode 100644
index 0000000..22bc8bd
--- /dev/null
+++ 
b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509AuthenticationRequestToken.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.registry.web.security.x509;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.web.security.NiFiAuthenticationRequestToken;
+import 
org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
+
+import java.security.cert.X509Certificate;
+
+/**
+ * This is an authentication request with a given JWT token.
+ */
+public class X509AuthenticationRequestToken extends 
NiFiAuthenticationRequestToken {
+
+    private final String proxiedEntitiesChain;
+    private final X509PrincipalExtractor principalExtractor;
+    private final X509Certificate[] certificates;
+
+    /**
+     * Creates a representation of the jwt authentication request for a user.
+     *
+     * @param proxiedEntitiesChain   The http servlet request
+     * @param certificates  The certificate chain
+     */
+    public X509AuthenticationRequestToken(final String proxiedEntitiesChain, 
final X509PrincipalExtractor principalExtractor, final X509Certificate[] 
certificates, final String clientAddress) {
+        super(clientAddress);
+        setAuthenticated(false);
+        this.proxiedEntitiesChain = proxiedEntitiesChain;
+        this.principalExtractor = principalExtractor;
+        this.certificates = certificates;
+    }
+
+    @Override
+    public Object getCredentials() {
+        return null;
+    }
+
+    @Override
+    public Object getPrincipal() {
+        if (StringUtils.isBlank(proxiedEntitiesChain)) {
+            return principalExtractor.extractPrincipal(certificates[0]);
+        } else {
+            return String.format("%s<%s>", proxiedEntitiesChain, 
principalExtractor.extractPrincipal(certificates[0]));
+        }
+    }
+
+    public String getProxiedEntitiesChain() {
+        return proxiedEntitiesChain;
+    }
+
+    public X509Certificate[] getCertificates() {
+        return certificates;
+    }
+
+    @Override
+    public String toString() {
+        return getName();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509CertificateExtractor.java
----------------------------------------------------------------------
diff --git 
a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509CertificateExtractor.java
 
b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509CertificateExtractor.java
new file mode 100644
index 0000000..ad62b37
--- /dev/null
+++ 
b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509CertificateExtractor.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.registry.web.security.x509;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.http.HttpServletRequest;
+import java.security.cert.X509Certificate;
+
+/**
+ * Extracts client certificates from Http requests.
+ */
+public class X509CertificateExtractor {
+
+    private final Logger logger = LoggerFactory.getLogger(getClass());
+
+    /**
+     * Extract the client certificate from the specified HttpServletRequest or
+     * null if none is specified.
+     *
+     * @param request http request
+     * @return cert
+     */
+    public X509Certificate[] extractClientCertificate(HttpServletRequest 
request) {
+        X509Certificate[] certs = (X509Certificate[]) 
request.getAttribute("javax.servlet.request.X509Certificate");
+
+        if (certs != null && certs.length > 0) {
+            return certs;
+        }
+
+        if (logger.isDebugEnabled()) {
+            logger.debug("No client certificate found in request.");
+        }
+
+        return null;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509CertificateValidator.java
----------------------------------------------------------------------
diff --git 
a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509CertificateValidator.java
 
b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509CertificateValidator.java
new file mode 100644
index 0000000..fe2c3e5
--- /dev/null
+++ 
b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509CertificateValidator.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.registry.web.security.x509;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.X509Certificate;
+
+/**
+ * Extracts client certificates from Http requests.
+ */
+public class X509CertificateValidator {
+
+    private final Logger logger = LoggerFactory.getLogger(getClass());
+
+    /**
+     * Extract the client certificate from the specified HttpServletRequest or 
null if none is specified.
+     *
+     * @param certificates the client certificates
+     * @throws CertificateExpiredException cert is expired
+     * @throws CertificateNotYetValidException cert is not yet valid
+     */
+    public void validateClientCertificate(final X509Certificate[] certificates)
+            throws CertificateExpiredException, 
CertificateNotYetValidException {
+
+        // ensure the cert is valid
+        certificates[0].checkValidity();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509IdentityProvider.java
----------------------------------------------------------------------
diff --git 
a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509IdentityProvider.java
 
b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509IdentityProvider.java
new file mode 100644
index 0000000..38aa0b4
--- /dev/null
+++ 
b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509IdentityProvider.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.registry.web.security.x509;
+
+import org.apache.nifi.registry.web.response.AuthenticationResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import 
org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
+
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.X509Certificate;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Identity provider for extract the authenticating a ServletRequest with a 
X509Certificate.
+ */
+public class X509IdentityProvider {
+
+    private static final Logger logger = 
LoggerFactory.getLogger(X509IdentityProvider.class);
+
+    private final String issuer = getClass().getSimpleName();
+
+    private X509CertificateValidator certificateValidator;
+    private X509PrincipalExtractor principalExtractor;
+
+    /**
+     * Authenticates the specified request by checking certificate validity.
+     *
+     * @param certificates the client certificates
+     * @return an authentication response
+     * @throws IllegalArgumentException the request did not contain a valid 
certificate (or no certificate)
+     */
+    public AuthenticationResponse authenticate(final X509Certificate[] 
certificates) throws IllegalArgumentException {
+        // ensure the cert was found
+        if (certificates == null || certificates.length == 0) {
+            throw new IllegalArgumentException("The specified request does not 
contain a client certificate.");
+        }
+
+        // extract the principal
+        final Object certificatePrincipal = 
principalExtractor.extractPrincipal(certificates[0]);
+        final String principal = certificatePrincipal.toString();
+
+        try {
+            certificateValidator.validateClientCertificate(certificates);
+        } catch (CertificateExpiredException cee) {
+            final String message = String.format("Client certificate for (%s) 
is expired.", principal);
+            logger.info(message, cee);
+            if (logger.isDebugEnabled()) {
+                logger.debug("", cee);
+            }
+            throw new IllegalArgumentException(message, cee);
+        } catch (CertificateNotYetValidException cnyve) {
+            final String message = String.format("Client certificate for (%s) 
is not yet valid.", principal);
+            logger.info(message, cnyve);
+            if (logger.isDebugEnabled()) {
+                logger.debug("", cnyve);
+            }
+            throw new IllegalArgumentException(message, cnyve);
+        } catch (final Exception e) {
+            logger.info(e.getMessage());
+            if (logger.isDebugEnabled()) {
+                logger.debug("", e);
+            }
+            throw new IllegalArgumentException(e.getMessage(), e);
+        }
+
+        // build the authentication response
+        return new AuthenticationResponse(principal, principal, 
TimeUnit.MILLISECONDS.convert(12, TimeUnit.HOURS), issuer);
+    }
+
+    /* setters */
+    public void setCertificateValidator(X509CertificateValidator 
certificateValidator) {
+        this.certificateValidator = certificateValidator;
+    }
+
+    public void setPrincipalExtractor(X509PrincipalExtractor 
principalExtractor) {
+        this.principalExtractor = principalExtractor;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/link/TestLinkService.java
----------------------------------------------------------------------
diff --git 
a/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/link/TestLinkService.java
 
b/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/link/TestLinkService.java
index d204b80..bfc9a46 100644
--- 
a/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/link/TestLinkService.java
+++ 
b/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/link/TestLinkService.java
@@ -72,10 +72,12 @@ public class TestLinkService {
         final VersionedFlowSnapshotMetadata snapshotMetadata1 = new 
VersionedFlowSnapshotMetadata();
         snapshotMetadata1.setFlowIdentifier(flow1.getIdentifier());
         snapshotMetadata1.setVersion(1);
+        snapshotMetadata1.setBucketIdentifier(bucket1.getIdentifier());
 
         final VersionedFlowSnapshotMetadata snapshotMetadata2 = new 
VersionedFlowSnapshotMetadata();
         snapshotMetadata2.setFlowIdentifier(flow1.getIdentifier());
         snapshotMetadata2.setVersion(2);
+        snapshotMetadata2.setBucketIdentifier(bucket1.getIdentifier());
 
         snapshots = new ArrayList<>();
         snapshots.add(snapshotMetadata1);
@@ -91,14 +93,16 @@ public class TestLinkService {
     public void testPopulateBucketLinks() {
         buckets.stream().forEach(b -> Assert.assertNull(b.getLink()));
         linkService.populateBucketLinks(buckets);
-        buckets.stream().forEach(b -> Assert.assertEquals("buckets/" + 
b.getIdentifier(), b.getLink().getUri().toString()));
+        buckets.stream().forEach(b -> Assert.assertEquals(
+                "buckets/" + b.getIdentifier(), 
b.getLink().getUri().toString()));
     }
 
     @Test
     public void testPopulateFlowLinks() {
         flows.stream().forEach(f -> Assert.assertNull(f.getLink()));
         linkService.populateFlowLinks(flows);
-        flows.stream().forEach(f -> Assert.assertEquals("flows/" + 
f.getIdentifier(), f.getLink().getUri().toString()));
+        flows.stream().forEach(f -> Assert.assertEquals(
+                "buckets/" + f.getBucketIdentifier() + "/flows/" + 
f.getIdentifier(), f.getLink().getUri().toString()));
     }
 
     @Test
@@ -106,14 +110,15 @@ public class TestLinkService {
         snapshots.stream().forEach(s -> Assert.assertNull(s.getLink()));
         linkService.populateSnapshotLinks(snapshots);
         snapshots.stream().forEach(s -> Assert.assertEquals(
-                "flows/" + s.getFlowIdentifier() + "/versions/" + 
s.getVersion(), s.getLink().getUri().toString()));
+                "buckets/" + s.getBucketIdentifier() + "/flows/" + 
s.getFlowIdentifier() + "/versions/" + s.getVersion(), 
s.getLink().getUri().toString()));
     }
 
     @Test
     public void testPopulateItemLinks() {
         items.stream().forEach(i -> Assert.assertNull(i.getLink()));
         linkService.populateItemLinks(items);
-        items.stream().forEach(i -> Assert.assertEquals("flows/" + 
i.getIdentifier(), i.getLink().getUri().toString()));
+        items.stream().forEach(i -> Assert.assertEquals(
+                "buckets/" + i.getBucketIdentifier() + "/flows/" + 
i.getIdentifier(), i.getLink().getUri().toString()));
     }
 
 }

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/pom.xml
----------------------------------------------------------------------
diff --git a/pom.xml b/pom.xml
index 1c61e41..66c8197 100644
--- a/pom.xml
+++ b/pom.xml
@@ -37,7 +37,8 @@
         <module>nifi-registry-jetty</module>
         <module>nifi-registry-resources</module>
         <module>nifi-registry-runtime</module>
-        <module>nifi-registry-security</module>
+        <module>nifi-registry-security-api</module>
+        <module>nifi-registry-security-api-impl</module>
         <module>nifi-registry-framework</module>
         <module>nifi-registry-provider-api</module>
         <module>nifi-registry-web-api</module>
@@ -265,7 +266,7 @@
                 <version>2.0.0.Final</version>
             </dependency>
             <dependency>
-                <groupId>org.hibernate</groupId>
+                <groupId>org.hibernate.validator</groupId>
                 <artifactId>hibernate-validator</artifactId>
                 <version>6.0.2.Final</version>
             </dependency>
@@ -396,7 +397,7 @@
                 <plugin>
                     <groupId>org.codehaus.mojo</groupId>
                     <artifactId>jaxb2-maven-plugin</artifactId>
-                    <version>1.6</version>
+                    <version>2.3.1</version>
                 </plugin>
                 <plugin>
                     <groupId>org.apache.maven.plugins</groupId>

Reply via email to