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

时间:2019-07-02 07:40:03

标签: spring spring-boot spring-security

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

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

1 个答案:

答案 0 :(得分: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
  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及其相关设置