没有本地信任库的客户端证书身份验证 [英] Client Certificate authentication without local truststore

查看:148
本文介绍了没有本地信任库的客户端证书身份验证的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

好的,起初听起来很奇怪,所以请忍受我:-)

我需要解决的问题是:
我需要以某种方式在Spring Boot应用程序中启用客户端身份验证,该方法允许客户端自己创建证书,而无需服务器使用服务器私钥对CSR进行签名. >

我如何实现这个目标?


背景:为什么我需要这个?

我们已经设置了一个Spring Cloud Config Server.它包含许多不同应用程序的配置值.现在,我们只允许每个应用程序访问其自己的配置值.
解决此问题的最简单但安全的方法似乎是:

  1. 应用程序创建一个自签名证书
  2. 它将包含私钥的证书存储在运行它的服务器上,并设置访问控制,以便只有其服务用户可以访问它.
  3. 它尝试从Cloud Config Server请求其配置值.
  4. 它将失败,因为服务器不知道客户端证书
  5. 应用程序将使用其尝试访问的URL和其证书的公钥记录一个错误
  6. 管理员用户将在Cloud Config Server可以读取的安全配置存储中的URL和公钥之间手动创建映射
  7. 现在,当应用程序尝试从服务器读取其配置值时,服务器将查看其安全配置存储,并检查它们是否是所请求URL的条目,如果是,则该请求是否已使用私有签名与该URL存储的公共密钥相匹配的密钥.
  8. 如果一切成功,则返回配置值

第7点将作为简单的Filter实现.

解决方案

我想要实现的目标基本上可以归结为一个问题:
代替从文件加载信任库,必须基于安全配置存储中的数据在内存中创建信任库.
事实证明这有点棘手,但绝对有可能.

创建信任库很容易:

KeyStore ts = KeyStore.getInstance(KeyStore.getDefaultType());
ts.load(null);

for (Certificate cert : certList) {
    ts.setCertificateEntry(UUID.randomUUID().toString(), cert);
}

但是,将其提供给SSL处理管道有点棘手.基本上,我们需要做的是提供X509ExtendedTrustManager的实现,该实现使用上面创建的信任库.
为了让SSL处理管道知道该实现,我们需要实现自己的提供程序:

public class ReloadableTrustManagerProvider extends Provider {
    public ReloadableTrustManagerProvider() {
        super("ReloadableTrustManager", 1, "Provider to load client certificates from memory");
        put("TrustManagerFactory." + TrustManagerFactory.getDefaultAlgorithm(), ReloadableTrustManagerFactory.class.getName());
    }
}

此提供者依次使用TrustManagerFactorySpi实现:

public class ReloadableTrustManagerFactory extends TrustManagerFactorySpi {

    private final TrustManagerFactory originalTrustManagerFactory;

    public ReloadableTrustManagerFactory() throws NoSuchAlgorithmException {
        ProviderList originalProviders = ProviderList.newList(
                Arrays.stream(Security.getProviders()).filter(p -> p.getClass() != ReloadableTrustManagerProvider.class)
                        .toArray(Provider[]::new));

        Provider.Service service = originalProviders.getService("TrustManagerFactory", TrustManagerFactory.getDefaultAlgorithm());
        originalTrustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm(), service.getProvider());
    }

    @Override
    protected void engineInit(KeyStore keyStore) throws KeyStoreException {
    }

    @Override
    protected void engineInit(ManagerFactoryParameters managerFactoryParameters) throws InvalidAlgorithmParameterException {
    }

    @Override
    protected TrustManager[] engineGetTrustManagers() {
        try {
            return new TrustManager[]{new ReloadableX509TrustManager(originalTrustManagerFactory)};
        } catch (Exception e) {
            return new TrustManager[0];
        }
    }
}

稍后有关originalTrustManagerFactoryReloadableX509TrustManager的更多信息.
最后,我们需要以一种使提供程序成为默认提供程序的方式注册提供程序,以便SSL管道可以使用它:

Security.insertProviderAt(new ReloadableTrustManagerProvider(), 1);

此代码可以在SpringApplication.run之前的main中执行.

回顾一下:我们需要将我们的提供程序插入安全提供程序列表中.我们的提供商使用我们自己的信任管理器工厂来创建我们自己的信任管理器的实例.

两件事仍然缺失:

  1. 实施我们的信任管理器
  2. originalTrustManagerFactory
  3. 的说明

首先,实现(基于 https://donneyfan.com/blog/dynamic-java-truststore-for-a-jax-ws-client ):

public class ReloadableX509TrustManager extends X509ExtendedTrustManager implements X509TrustManager {
    private final TrustManagerFactory originalTrustManagerFactory;
    private X509ExtendedTrustManager clientCertsTrustManager;
    private X509ExtendedTrustManager serverCertsTrustManager;
    private ArrayList<Certificate> certList;
    private static Log logger = LogFactory.getLog(ReloadableX509TrustManager.class);

    public ReloadableX509TrustManager(TrustManagerFactory originalTrustManagerFactory) throws Exception {
        try {
            this.originalTrustManagerFactory = originalTrustManagerFactory;
            certList = new ArrayList<>();
            /* Example on how to load and add a certificate. Instead of loading it here, it should be loaded externally and added via addCertificates
            // Should get from secure configuration store
            String cert64 = "base64 encoded certificate";
            byte encodedCert[] = Base64.getDecoder().decode(cert64);
            CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
            X509Certificate cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(encodedCert));
            certList.add(cert); */
            reloadTrustManager();
        } catch (Exception e) {
            logger.fatal(e);
            throw e;
        }
    }

    /**
     * Removes a certificate from the pending list. Automatically reloads the TrustManager
     *
     * @param cert is not null and was already added
     * @throws Exception if cannot be reloaded
     */
    public void removeCertificate(Certificate cert) throws Exception {
        certList.remove(cert);
        reloadTrustManager();
    }

    /**
     * Adds a list of certificates to the manager. Automatically reloads the TrustManager
     *
     * @param certs is not null
     * @throws Exception if cannot be reloaded
     */
    public void addCertificates(List<Certificate> certs) throws Exception {
        certList.addAll(certs);
        reloadTrustManager();
    }

    @Override
    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        clientCertsTrustManager.checkClientTrusted(chain, authType);
    }

    @Override
    public void checkClientTrusted(X509Certificate[] x509Certificates, String s, Socket socket) throws CertificateException {
        clientCertsTrustManager.checkClientTrusted(x509Certificates, s, socket);
    }

    @Override
    public void checkClientTrusted(X509Certificate[] x509Certificates, String s, SSLEngine sslEngine) throws CertificateException {
        clientCertsTrustManager.checkClientTrusted(x509Certificates, s, sslEngine);
    }

    @Override
    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        serverCertsTrustManager.checkServerTrusted(chain, authType);
    }

    @Override
    public void checkServerTrusted(X509Certificate[] x509Certificates, String s, Socket socket) throws CertificateException {
        serverCertsTrustManager.checkServerTrusted(x509Certificates, s, socket);
    }

    @Override
    public void checkServerTrusted(X509Certificate[] x509Certificates, String s, SSLEngine sslEngine) throws CertificateException {
        serverCertsTrustManager.checkServerTrusted(x509Certificates, s, sslEngine);
    }

    @Override
    public X509Certificate[] getAcceptedIssuers() {
        return ArrayUtils.addAll(serverCertsTrustManager.getAcceptedIssuers(), clientCertsTrustManager.getAcceptedIssuers());
    }

    private void reloadTrustManager() throws Exception {
        KeyStore ts = KeyStore.getInstance(KeyStore.getDefaultType());
        ts.load(null);

        for (Certificate cert : certList) {
            ts.setCertificateEntry(UUID.randomUUID().toString(), cert);
        }

        clientCertsTrustManager = getTrustManager(ts);
        serverCertsTrustManager = getTrustManager(null);
    }

    private X509ExtendedTrustManager getTrustManager(KeyStore ts) throws NoSuchAlgorithmException, KeyStoreException {
        originalTrustManagerFactory.init(ts);
        TrustManager tms[] = originalTrustManagerFactory.getTrustManagers();
        for (int i = 0; i < tms.length; i++) {
            if (tms[i] instanceof X509ExtendedTrustManager) {
                return (X509ExtendedTrustManager) tms[i];
            }
        }

        throw new NoSuchAlgorithmException("No X509TrustManager in TrustManagerFactory");
    }
}

此实现有一些值得注意的要点:

  1. 它实际上将所有工作委派给普通的默认信任管理器.为了获得它,我们需要具有SSL管道通常使用的默认信任管理器工厂.这就是为什么我们在构造函数中将其作为参数originalTrustManagerFactory传递的原因.
  2. 我们实际上正在使用两个不同的信任管理器实例:一个用于验证客户端证书-当客户端向我们发送请求并使用客户端证书对其自身进行身份验证时使用-另一个用于验证服务器证书-在以下情况下使用我们使用HTTPS向服务器发送请求.为了验证客户端证书,我们使用自己的信任库创建了一个信任管理器.这将仅包含存储在我们的安全配置存储中的证书,因此将不包含Java通常信任的任何根CA.如果我们使用此信任管理器向我们作为客户端的HTTPS URL发出请求,则该请求将失败,因为我们将无法验证服务器证书的有效性.因此,将在不传递信任库的情况下创建用于服务器证书验证的信任管理器,因此将使用默认的Java信任库.
  3. getAcceptedIssuers需要从我们的两个信任管理器中返回接受的颁发者,因为在这种方法中,我们不知道客户端或服务器证书是否正在进行证书验证.这样做的缺点很小,就是我们的信任管理器还会信任使用自签名客户端证书作为HTTPS的服务器.

要使所有这些工作正常进行,我们需要启用ssl客户端身份验证:

server.ssl.key-store: classpath:keyStore.p12 # secures our API with SSL. Needed, to enable client certificates handling
server.ssl.key-store-password: very-secure
server.ssl.client-auth: need

因为我们正在创建自己的信任库,所以不需要设置server.ssl.trust-store及其相关设置

OK, this might sound strange at first, so please bear with me :-)

The problem I need to solve is this:
I need to enable client authentication in a Spring Boot application in a way, that allows the client to create the certificate themselves, without the need for the server to sign the CSR with the servers private key.

How can I achieve this goal?


Background: Why do I need this?

We have setup a Spring Cloud Config Server. It contains the configuration values for many different applications. We now want to allow each application access only to its own configuration values.
The easiest - yet secure - solution to this problem seems to be the following:

  1. The application creates a self signed certificate
  2. It stores the certificate including its private key on the server it runs on and sets the access control so that only its service user has access to it
  3. It tries to request its configuration values from the Cloud Config Server.
  4. It will fail, because the client certificate is unknown to the server
  5. The application will log an error with the URL it tried to reach and the public key of its certificate
  6. An admin user will manually create a mapping between the URL and the public key in a secure configuration storage that the Cloud Config Server can read
  7. Now, when the application tries to read its config values from the server, the server will look into its secure configuration storage and check if their is an entry for the requested URL and if so, if the request was signed with the private key that matches the public key that was stored for that URL.
  8. If all succeeds, the configuration values are returned

Point 7 will be implemented as a simple Filter.

解决方案

What I want to achieve basically boils down to one issue:
Instead of loading the truststore from a file, the truststore has to be created in-memory, based on data from the secure configuration storage.
This turned out to be a bit tricky, but absolutely possible.

Creating a truststore is easy:

KeyStore ts = KeyStore.getInstance(KeyStore.getDefaultType());
ts.load(null);

for (Certificate cert : certList) {
    ts.setCertificateEntry(UUID.randomUUID().toString(), cert);
}

However, supplying it to the SSL processing pipeline is a bit tricky. Basically, what we need to do is to provide an implementation of X509ExtendedTrustManager that uses the truststore that we created above.
To make this implementation known to the SSL processing pipeline, we need to implement our own provider:

public class ReloadableTrustManagerProvider extends Provider {
    public ReloadableTrustManagerProvider() {
        super("ReloadableTrustManager", 1, "Provider to load client certificates from memory");
        put("TrustManagerFactory." + TrustManagerFactory.getDefaultAlgorithm(), ReloadableTrustManagerFactory.class.getName());
    }
}

This provider in turn uses a TrustManagerFactorySpi implementation:

public class ReloadableTrustManagerFactory extends TrustManagerFactorySpi {

    private final TrustManagerFactory originalTrustManagerFactory;

    public ReloadableTrustManagerFactory() throws NoSuchAlgorithmException {
        ProviderList originalProviders = ProviderList.newList(
                Arrays.stream(Security.getProviders()).filter(p -> p.getClass() != ReloadableTrustManagerProvider.class)
                        .toArray(Provider[]::new));

        Provider.Service service = originalProviders.getService("TrustManagerFactory", TrustManagerFactory.getDefaultAlgorithm());
        originalTrustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm(), service.getProvider());
    }

    @Override
    protected void engineInit(KeyStore keyStore) throws KeyStoreException {
    }

    @Override
    protected void engineInit(ManagerFactoryParameters managerFactoryParameters) throws InvalidAlgorithmParameterException {
    }

    @Override
    protected TrustManager[] engineGetTrustManagers() {
        try {
            return new TrustManager[]{new ReloadableX509TrustManager(originalTrustManagerFactory)};
        } catch (Exception e) {
            return new TrustManager[0];
        }
    }
}

More about originalTrustManagerFactory and ReloadableX509TrustManager later.
Finally, we need to register the provider in a way that makes it the default one, so that the SSL pipeline will use it:

Security.insertProviderAt(new ReloadableTrustManagerProvider(), 1);

This code can be execute in main, before SpringApplication.run.

To recap: We need to insert our provider into the list of security providers. Our provider uses our own trust manager factory to create instances of our own trust manager.

Two things are still missing:

  1. The implementation of our trust manager
  2. The explanation for originalTrustManagerFactory

First, the implementation (based on https://donneyfan.com/blog/dynamic-java-truststore-for-a-jax-ws-client):

public class ReloadableX509TrustManager extends X509ExtendedTrustManager implements X509TrustManager {
    private final TrustManagerFactory originalTrustManagerFactory;
    private X509ExtendedTrustManager clientCertsTrustManager;
    private X509ExtendedTrustManager serverCertsTrustManager;
    private ArrayList<Certificate> certList;
    private static Log logger = LogFactory.getLog(ReloadableX509TrustManager.class);

    public ReloadableX509TrustManager(TrustManagerFactory originalTrustManagerFactory) throws Exception {
        try {
            this.originalTrustManagerFactory = originalTrustManagerFactory;
            certList = new ArrayList<>();
            /* Example on how to load and add a certificate. Instead of loading it here, it should be loaded externally and added via addCertificates
            // Should get from secure configuration store
            String cert64 = "base64 encoded certificate";
            byte encodedCert[] = Base64.getDecoder().decode(cert64);
            CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
            X509Certificate cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(encodedCert));
            certList.add(cert); */
            reloadTrustManager();
        } catch (Exception e) {
            logger.fatal(e);
            throw e;
        }
    }

    /**
     * Removes a certificate from the pending list. Automatically reloads the TrustManager
     *
     * @param cert is not null and was already added
     * @throws Exception if cannot be reloaded
     */
    public void removeCertificate(Certificate cert) throws Exception {
        certList.remove(cert);
        reloadTrustManager();
    }

    /**
     * Adds a list of certificates to the manager. Automatically reloads the TrustManager
     *
     * @param certs is not null
     * @throws Exception if cannot be reloaded
     */
    public void addCertificates(List<Certificate> certs) throws Exception {
        certList.addAll(certs);
        reloadTrustManager();
    }

    @Override
    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        clientCertsTrustManager.checkClientTrusted(chain, authType);
    }

    @Override
    public void checkClientTrusted(X509Certificate[] x509Certificates, String s, Socket socket) throws CertificateException {
        clientCertsTrustManager.checkClientTrusted(x509Certificates, s, socket);
    }

    @Override
    public void checkClientTrusted(X509Certificate[] x509Certificates, String s, SSLEngine sslEngine) throws CertificateException {
        clientCertsTrustManager.checkClientTrusted(x509Certificates, s, sslEngine);
    }

    @Override
    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        serverCertsTrustManager.checkServerTrusted(chain, authType);
    }

    @Override
    public void checkServerTrusted(X509Certificate[] x509Certificates, String s, Socket socket) throws CertificateException {
        serverCertsTrustManager.checkServerTrusted(x509Certificates, s, socket);
    }

    @Override
    public void checkServerTrusted(X509Certificate[] x509Certificates, String s, SSLEngine sslEngine) throws CertificateException {
        serverCertsTrustManager.checkServerTrusted(x509Certificates, s, sslEngine);
    }

    @Override
    public X509Certificate[] getAcceptedIssuers() {
        return ArrayUtils.addAll(serverCertsTrustManager.getAcceptedIssuers(), clientCertsTrustManager.getAcceptedIssuers());
    }

    private void reloadTrustManager() throws Exception {
        KeyStore ts = KeyStore.getInstance(KeyStore.getDefaultType());
        ts.load(null);

        for (Certificate cert : certList) {
            ts.setCertificateEntry(UUID.randomUUID().toString(), cert);
        }

        clientCertsTrustManager = getTrustManager(ts);
        serverCertsTrustManager = getTrustManager(null);
    }

    private X509ExtendedTrustManager getTrustManager(KeyStore ts) throws NoSuchAlgorithmException, KeyStoreException {
        originalTrustManagerFactory.init(ts);
        TrustManager tms[] = originalTrustManagerFactory.getTrustManagers();
        for (int i = 0; i < tms.length; i++) {
            if (tms[i] instanceof X509ExtendedTrustManager) {
                return (X509ExtendedTrustManager) tms[i];
            }
        }

        throw new NoSuchAlgorithmException("No X509TrustManager in TrustManagerFactory");
    }
}

This implementation has a few notable points:

  1. It actually delegates all work to the normal default trust manager. To be able to obtain it, we need to have the default trust manager factory that would normally be used by the SSL pipeline. That's why we pass it as the parameter originalTrustManagerFactory in the constructor.
  2. We are actually using two different trust manager instances: One for validating client certificates - which is used when a client sends a request to us and authenticates itself with a client certificate - and another one for validating server certificates - which is used when we send a request to a server using HTTPS. For validating the client certificates, we create a trust manager with our own truststore. This will contain only the certificates that are stored in our secure configuration store and therefore it will not contain any of the root CAs that Java usually trusts. If we would use this trust manager for requests to a HTTPS URL where we are the client, the request would fail as we would be unable to verify the validity of the server's certificate. Therefore, the trust manager for the server certificate validation is created without passing a truststore and will therefore use the default Java truststore.
  3. getAcceptedIssuers needs to return the accepted issuers from both our trust managers, because in this method we don't know if the certificate validation is happening for a client or a server certificate. This has the small drawback that our trustmanager would also trust servers which are using our self signed client certificates for their HTTPS.

To make all of this work, we need to enable ssl client authentication:

server.ssl.key-store: classpath:keyStore.p12 # secures our API with SSL. Needed, to enable client certificates handling
server.ssl.key-store-password: very-secure
server.ssl.client-auth: need

Because we are creating our own truststore, we don't need the setting server.ssl.trust-store and its related settings

这篇关于没有本地信任库的客户端证书身份验证的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆