在Mac OS X上编程访问“系统根”下的SSL证书

8
我正在编写一个Java应用程序,它通过远程Https站点进行rest Api调用。远程站点使用受信任的证书进行签名。它在Windows上运行良好,但是在OS X上由于SSL证书问题而无法正常运行。
我进行了一些调查,并发现问题与我在代码中如何使用getInstance调用初始化KeyStore对象有关。它只从“System”钥匙串读取证书,而不是从“System Roots”钥匙串中读取证书。以下是代码片段,可打印密钥库中的所有证书。
// In windows use "WINDOWS-ROOT"
KeyStore osTrustManager = KeyStore.getInstance("KeychainStore");
osTrustManager.load(null, null);

Enumeration<String> enumerator = osTrustManager.aliases();
while (enumerator.hasMoreElements()) {
    String alias = enumerator.nextElement();
    if (osTrustManager.isCertificateEntry(alias)) {
        m_logger.info(String.format("%s (certificate)\n", alias));
    }
}

如何修改代码以实现该目标?如果有人能够参与讨论,我们将不胜感激。

以下是“系统根证书”下的示例 来自OS X的截图

2个回答

6

我不知道是否存在某种KeyStore可以让您访问Mac OS X系统根证书,但是您可以尝试其他方式。

在Mac OS X中,您可以使用security命令从任何钥匙串获取证书列表。

例如,此命令将向您提供有关安装在系统根钥匙串中的不同证书的信息:

security find-certificate -a "/System/Library/Keychains/SystemRootCertificates.keychain"

这个实用程序有两个标志,-p会将每个证书输出为PEM编码,-a允许我们按名称过滤结果,这非常方便,因为系统中安装了大量CA。
想法是从Java中使用该实用程序。
不久前,我遇到了一个名为clienteafirma的库,专门用于处理数字签名。
这个库有一个叫做AppleScript的类。这个类基本上是Process的包装器,允许我们运行任意命令。
以下代码使用该类和security命令获取由例如VeriSign颁发的所有证书:
public static void main(String... args) {
  // Keychains that we can use
  final String KEYCHAIN_PATH = "/Library/Keychains/System.keychain";
  final String SYSTEM_KEYCHAIN_PATH = "/System/Library/Keychains/SystemRootCertificates.keychain";
  // Show me only certificates from VeriSign
  final String AUTHORITY = "VeriSign";
  final String OSX_SEC_COMMAND = "security find-certificate -a -p -c %AUTHORITY% %KEYCHAIN%";
  final String cmd = OSX_SEC_COMMAND.replace("%AUTHORITY%", AUTHORITY).replace("%KEYCHAIN%", SYSTEM_KEYCHAIN_PATH);
  System.out.println(cmd);
  System.out.println();

  final AppleScript script = new AppleScript(cmd);
  InputStream certificateStream = null;
  try {
    // Run script
    final String result = script.run();
    certificateStream = new ByteArrayInputStream(result.getBytes());
    // Process the output of the command
    final CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
    final Collection<X509Certificate> certificates = (Collection<X509Certificate>) certificateFactory.generateCertificates(certificateStream);
    // Use the certificates as you need
    for (X509Certificate certificate : certificates) {
      String alias = certificate.getSubjectX500Principal().getName();
      System.out.println("Certificate: " + alias);
    }
  } catch (Throwable t) {
    t.printStackTrace();
  } finally {
    if (certificateStream != null) {
      try {
        certificateStream.close();
      } catch (IOException io) {
        io.printStackTrace();
      }
    }
  }
}

1
您不需要将PEM转换为DER格式--CertificateFactory已经可以读取PEM格式。您也不需要拆分(解析)数据并循环--CertificateFactory.generateCertificates(注意s)可以自行读取PEM证书序列。 - dave_thompson_085
@dave_thompson_085 非常感谢。这段代码重点解释了如何获取存储在Mac系统根密钥链中的证书,而不是证书的处理。无论如何,我会根据您的评论更新答案。再次非常感谢! - jccampanero

2

需求

  • 检索 macOS 锚点根证书
  • 为其获取一个 Java X509Certificate 实例

可能的解决方案

据我所知,没有纯 Java 的解决方法。但是,您可以创建一个本地的 C 库,通过操作系统特定的调用检索证书,并通过 JNI 将它们返回给 Java。

自 macOS 10.3 以来,Security 框架中有一个名为 SecTrustCopyAnchorCertificates 的函数,

它检索由 macOS 存储的锚(根)证书。

详见https://developer.apple.com/documentation/security/1401507-sectrustcopyanchorcertificates?language=objc

要构造 Java X509Certificate 实例,您需要以 DER 编码格式获得证书数据,详见https://docs.oracle.com/javase/8/docs/api/java/security/cert/CertificateFactory.html#generateCertificate-java.io.InputStream-

在 macOS 上,您可以通过 SecCertificateCopyData 获取 DER 编码的证书数据。

注意:由于两个函数 SecTrustCopyAnchorCertificates 和 SecCertificateCopyData 都包含单词“Copy”,因此必须在使用后调用 CFRelease 以避免创建内存泄漏。

每个证书的数据可以存储在 Java 字节数组中,并返回给 Java 调用方。

在 Java 端,可以通过调用 CertificateFactory.getInstance("X.509") 来获取一个 CertificateFactory。然后,您可以通过调用 certFactory.generateCertificate(in) 将字节转换为 X509Certificate,其中 in 是 ByteArrayInputStream,其中实际来自本地 C 库的证书字节。

这是一个自包含的示例:

本地 C 库

#include <stdio.h>
#include <string.h>
#include <CoreFoundation/CoreFoundation.h>
#include <Security/Security.h>
#include "com_software7_test_MacOSX509Certificates.h"


JNIEXPORT jobjectArray JNICALL Java_com_software7_test_MacOSX509Certificates_retrieveCertificates
  (JNIEnv *env, jobject obj) {
      CFArrayRef certs = NULL;
      OSStatus status = SecTrustCopyAnchorCertificates(&certs);
      if (status != noErr) {
        jclass rte = (*env)->FindClass(env, "java/lang/RuntimeException");
        if (rte != NULL)
            (*env)->ThrowNew(env, rte, "error retrieving anchor certificates"); 
        (*env)->DeleteLocalRef(env, rte);
      }
  
      CFIndex ncerts = CFArrayGetCount(certs);
      jclass byteArrayClass = (*env)->FindClass(env, "[B");
      jobjectArray array = (*env)->NewObjectArray(env, ncerts, byteArrayClass, (*env)->NewByteArray(env, 0));

      for (int i = 0; i < ncerts; i++) {   
          SecCertificateRef certRef = (SecCertificateRef)CFArrayGetValueAtIndex(certs, i);
          CFDataRef certData = SecCertificateCopyData(certRef);
          int numBytes = CFDataGetLength(certData);
          jbyteArray jCert = (*env)->NewByteArray(env, numBytes);
          (*env)->SetByteArrayRegion(env, jCert, 0, numBytes, (const jbyte *)CFDataGetBytePtr(certData));         
          CFRelease(certData);
      
          (*env)->SetObjectArrayElement(env, array, i, jCert);
          (*env)->DeleteLocalRef(env, jCert);
      }
      CFRelease(certs);  
      return array;
  }
  

Java

package com.software7.test;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class MacOSX509Certificates {

    static {
        System.loadLibrary("maccerts");
    }

    private native byte[][] retrieveCertificates();

    public static void main(String[] args) {
        MacOSX509Certificates mc = new MacOSX509Certificates();
        mc.retrieveAndPrint();
    }

    private void retrieveAndPrint() {
        List<X509Certificate> x509Certificates = retrieve();
        for (X509Certificate x509c : x509Certificates) {
            System.out.println(x509c.getSubjectX500Principal());
        }
    }

    private List<X509Certificate> retrieve() {
        byte[][] certs = retrieveCertificates();
        return Arrays.stream(certs)
                .<X509Certificate>map(MacOSX509Certificates::convertToX509Certificate)
                .collect(Collectors.toList());
    }

    @SuppressWarnings("unchecked")
    private static <X509Certificate> X509Certificate convertToX509Certificate(byte[] bytes) {
        try {
            CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
            try (InputStream in = new ByteArrayInputStream(bytes)) {
                return (X509Certificate) certFactory.generateCertificate(in);
            }
        } catch (CertificateException | IOException e) {
            throw new RuntimeException(e);
        }
    }
}
  

构建

一个构建过程可能包括以下步骤:

export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk-14.0.1.jdk/Contents/Home/
javac -h . com/software7/test/MacOSX509Certificates.java
clang -c -fPIC -I$JAVA_HOME/include -I$JAVA_HOME/include/darwin com_software7_test_MacOSX509Certificates.c -o com_software7_test_MacOSX509Certificates.o
clang -dynamiclib -o libmaccerts.dylib com_software7_test_MacOSX509Certificates.o -lc -framework CoreFoundation -framework Security
mv libmaccerts.dylib ../out/production/read_mac_system_certs
rm com_software7_test_MacOSX509Certificates.o
rm com/software7/test/MacOSX509Certificates.class

测试

如果您在 Mac 上运行此示例,则会返回:

CN=Go Daddy Root Certificate Authority - G2, O="GoDaddy.com, Inc.", L=Scottsdale, ST=Arizona, C=US
CN=SwissSign Platinum CA - G2, O=SwissSign AG, C=CH
CN=AddTrust Class 1 CA Root, OU=AddTrust TTP Network, O=AddTrust AB, C=SE
CN=Global Chambersign Root, OU=http://www.chambersign.org, O=AC Camerfirma SA CIF A82743287, C=EU
...

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接