【问题标题】:Client Certificate authentication without local truststore没有本地信任库的客户端证书身份验证
【发布时间】:2019-07-02 07:40:03
【问题描述】:

好的,一开始这可能听起来很奇怪,所以请多多包涵:-)

我需要解决的问题是:
我需要在 Spring Boot 应用程序中启用客户端身份验证,允许客户端自己创建证书, 服务器无需使用服务器私钥签署 CSR。 p>

我怎样才能实现这个目标?


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

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

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

第 7 点将被实现为一个简单的Filter

【问题讨论】:

    标签: spring spring-boot spring-security


    【解决方案1】:

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

    创建信任库很容易:

    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);
    

    这段代码可以在main中执行,在SpringApplication.run之前。

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

    仍然缺少两件事:

    1. 我们的信任管理器的实现
    2. originalTrustManagerFactory的解释

    一、实现(基于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 及其相关设置

    【讨论】:

    • 您好,我已经实施了您的解决方案,以便在内存中建立信任存储。但是您能否举例说明如何在运行时将证书添加到信任存储区?说何时调用 GET 端点
    • 我也有一个关于同一个问题的赏金在这里,很遗憾我以前没有遇到过你的解决方案。 stackoverflow.com/questions/57143673/…
    • 您可以在此处发布答案并将此帖子链接到我的赏金帖子的答案中。我很乐意接受您的回答,非常感谢。
    • 有什么问题?每当您添加或删除证书时,都会重新加载信任库
    • 我的证书是 base64 编码的字符串。如果我创建ReloadableX509TrustManager 的新实例并调用 addCertificates 方法。它不会影响实际的信任存储
    猜你喜欢
    • 2011-04-09
    • 2013-10-07
    • 2012-01-03
    • 1970-01-01
    • 2022-06-23
    • 2014-02-24
    • 1970-01-01
    • 1970-01-01
    • 2018-02-12
    相关资源
    最近更新 更多