MicrosoftAuthRootImporter.java
/*
* Copyright 2026 Emmanuel Bourg
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.jsign;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.Files;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.TimeZone;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import org.apache.commons.io.IOUtils;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
import org.bouncycastle.asn1.x509.KeyPurposeId;
import org.bouncycastle.cms.CMSException;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.util.encoders.Hex;
import net.jsign.asn1.authenticode.CertificateTrustList;
import net.jsign.asn1.authenticode.TrustedSubject;
import static java.nio.charset.StandardCharsets.*;
/**
* Tool to import the certificates trusted by Microsoft for code signing and timestamping.
* The tool writes the SHA-256 hash of the Subject Public Key Information (SPKI) of each certificate
* in the windows-trusted-keys.csv file. This file is used to verify the certificate chains.
*
* @since 8.0
*/
public class MicrosoftAuthRootImporter {
public static void main(String[] args) throws Exception {
CertificateTrustList certificateTrustList = getWindowsCertificateTrustList();
List<X509Certificate> certificates = getWindowsTrustedCertificates(certificateTrustList);
System.err.println("Fetched " + certificates.size() + " certificates");
File outputFile = new File("src/main/resources/windows-trusted-keys.csv");
outputFile.getParentFile().mkdirs();
try (PrintWriter out = new PrintWriter(new OutputStreamWriter(new BufferedOutputStream(Files.newOutputStream(outputFile.toPath())), UTF_8))) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
out.print("# Windows trusted root certificates (" + dateFormat.format(certificateTrustList.getCtlThisUpdate().getDate()) + ")\n\n");
generateTrustedSubjectPublicKeyHashFile(certificates, out);
}
}
private static void generateTrustedSubjectPublicKeyHashFile(List<X509Certificate> certificates, PrintWriter out) {
certificates.sort(Comparator.comparing(t -> t.getSubjectX500Principal().toString()));
for (X509Certificate certificate : certificates) {
out.print(new PublicKeyHash(certificate) + "\t" + certificate.getSubjectX500Principal() + "\n");
}
}
/**
* Fetches all the certificates trusted by Windows.
*/
private static List<X509Certificate> getWindowsTrustedCertificates(CertificateTrustList certificateTrustList) throws IOException {
List<X509Certificate> certificates = new ArrayList<>();
List<String> thumbprints = getWindowsCertificatesThumbprints(certificateTrustList);
System.err.println("Found " + thumbprints.size() + " certificates");
ExecutorService executor = Executors.newFixedThreadPool(10);
for (String thumbprint : thumbprints) {
executor.submit(() -> {
try {
certificates.add(getWindowsTrustedCertificate(thumbprint));
} catch (Exception e) {
e.printStackTrace();
}
});
}
executor.shutdown();
try {
executor.awaitTermination(60, TimeUnit.MINUTES);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
if (certificates.size() != thumbprints.size()) {
throw new IOException("Only " + certificates.size() + " certificates could be fetched out of " + thumbprints.size());
}
return certificates;
}
/**
* Returns the certificate trusted by Windows with the specified thumbprint.
*
* @param thumbprint the thumbprint of the certificate to download
*/
private static X509Certificate getWindowsTrustedCertificate(String thumbprint) throws IOException, CertificateException {
URL url = new URL("http://ctldl.windowsupdate.com/msdownload/update/v3/static/trustedr/en/" + thumbprint.toUpperCase() + ".crt");
ByteArrayInputStream in = new ByteArrayInputStream(fetch(url));
return (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(in);
}
/**
* Returns the thumbprints of the certificates authorized for code signing or timestamping from the specified certificate trust list.
*/
private static List<String> getWindowsCertificatesThumbprints(CertificateTrustList certificateTrustList) {
List<String> thumbprints = new ArrayList<>();
for (TrustedSubject trustedSubject : certificateTrustList.getTrustedSubjects()) {
ExtendedKeyUsage eku = trustedSubject.getEnhancedKeyUsage();
if (eku == null || eku.hasKeyPurposeId(KeyPurposeId.id_kp_codeSigning) || eku.hasKeyPurposeId(KeyPurposeId.id_kp_timeStamping)) {
thumbprints.add(Hex.toHexString(trustedSubject.getSubjectIdentifier()));
}
}
return thumbprints;
}
/**
* Downloads the Windows trusted root certificates list (from http://ctldl.windowsupdate.com/msdownload/update/v3/static/trustedr/en/authroot.stl)
*/
private static CertificateTrustList getWindowsCertificateTrustList() throws IOException, CMSException {
byte[] data = fetch(new URL("http://ctldl.windowsupdate.com/msdownload/update/v3/static/trustedr/en/authroot.stl"));
ASN1Sequence content = (ASN1Sequence) new CMSSignedData(data).getSignedContent().getContent();
return new CertificateTrustList(content);
}
private static byte[] fetch(URL url) throws IOException {
System.err.println("Fetching: " + url);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
int responseCode = conn.getResponseCode();
if (responseCode >= 400) {
throw new IOException("HTTP " + responseCode + " - " + url);
}
try (InputStream in = conn.getInputStream()) {
return IOUtils.toByteArray(in);
}
}
}