一尘不染

没有本地信任库的客户端证书身份验证

spring-boot

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

我需要解决的问题是:
我需要以某种方式在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


阅读 279

收藏
2020-05-30

共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的请求,则该请求将失败,因为我们将无法验证服务器证书的有效性。因此,
  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及其相关设置

2020-05-30