Access Windows Local Machine Personal Keystore with Java (SunMSCAPI)

2.3k views Asked by At

Is there a way to access the windows local machine personal key storage with Java using SunMSCAPI?

Usually, you can use either WINDOWS-ROOT (which is roughly the equivalent to the trusted storage in Java) or WINDOWS-MY (which contains the personal certificates which is roughly equivalent to the key storage in Java) to retrieve certificates from the windows certificate storage.

This works fine for users, but even when impersonating the SYSTEM user, I was not able to retrieve the local machine's personal certificates.

There exist some questions regarding this that use JNA (which I would like to avoid, especially since it seems to be complicated to retrieve the private key from there).
Also, someone used psexec to impersonate the SYSTEM user (using psexec -s). I also tried this, but have not been successful.
In the end, there is also an open bug in the Java Bug System.

If anyone has an idea on how to retrieve certificates from the local machine personal storage in windows using Java, I would be grateful.

2

There are 2 answers

3
oligofren On BEST ANSWER

Update

After fourteen years in the bug tracker, JDK-6782021 was finally fixed in Java 19 (Spring 2022). This means this is no longer an issue.


Original answer

Yes and no. You can use the wcsa utility, which will intercept JVM calls to the Windows Crypto API and let you access the local machine credentials. This is of course a hack around the real problem, JDK-6782021, which was reported ten years ago. But it does let you access the local machine certificates in a pretty painless way!

So yes, it is possible to access them, but no, it's not possible to access them using the plain Java API. You can access them using normal Java using the commercial product JCAPI, though, but for most uses the wcsa util is fine.

The Open JDK maintainers are willing to take a patch, though, so maybe you can have a stab at fixing it using the code provided in the wcsa repo :)

0
mpderbec On

Here's an alternative that works independent of the Java version and whether or not https://bugs.openjdk.org/browse/JDK-6782021 or https://bugs.openjdk.org/browse/JDK-8313367 are fixed. It uses a small Powershell script to dump the certificates to a temp folder where Java can read them in easily.

import java.io.File;
import java.io.FileInputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import org.apache.commons.io.FileUtils;

public class WindowsCertificateService {
    private static List<X509Certificate> getWindowsLocalMachineCertificates(String certType) {
        // Theoretically we could use KeyStore.getInstance("Windows-ROOT-LOCALMACHINE") and
        // KeyStore.getInstance("Windows-MY-LOCALMACHINE") to accomplish this, which were added as part of
        // https://bugs.openjdk.org/browse/JDK-6782021. Unfortunately, we'll get an Access Denied error due to
        // https://bugs.openjdk.org/browse/JDK-8313367.

        Path tempFolder;
        List<X509Certificate> certificates = new ArrayList<>();
        try {
            tempFolder = Files.createTempDirectory("seeq-link-getWindowsLocalMachineCertificates");
        } catch (Throwable e) {
            // Log an error if desired
            return Collections.emptyList();
        }

        try {
            String powershellScript = String.format("dir cert:\\localmachine\\%s | Foreach-Object { [system.IO" +
                    ".file]::WriteAllBytes(\".\\$($_.Thumbprint).cer\", ($_.Export('CERT', 'secret')) ) }", certType);
            Path scriptLocation = tempFolder.resolve("getWindowsLocalMachineCertificates.ps1");
            Files.write(scriptLocation, powershellScript.getBytes());
            ProcessBuilder processBuilder = new ProcessBuilder(
                    "powershell.exe", "-ExecutionPolicy", "Bypass", "-File", scriptLocation.toString());
            processBuilder.directory(tempFolder.toFile());
            processBuilder.redirectErrorStream(true);
            Process process = processBuilder.start();
            process.waitFor();
            if (process.exitValue() != 0) {
                // Log an error if desired
                return Collections.emptyList();
            }

            Collection<File> cerFiles = FileUtils.listFiles(tempFolder.toFile(), new String[] { "cer" }, false);
            for (File cerFile : cerFiles) {
                try (FileInputStream fis = new FileInputStream(cerFile)) {
                    CertificateFactory cf = CertificateFactory.getInstance("X.509");
                    Certificate cert = cf.generateCertificate(fis);
                    if (cert instanceof X509Certificate) {
                        certificates.add((X509Certificate) cert);
                    }
                } catch (Exception e) {
                    // Log an error if desired
                }
            }
            return certificates;
        } catch (Throwable e) {
            // Log an error if desired
            return Collections.emptyList();
        } finally {
            if (tempFolder != null) {
                try {
                    FileUtils.deleteDirectory(tempFolder.toFile());
                } catch (Exception e) {
                    // Log an error if desired
                }
            }
        }
    }
}

The certType argument can be either "root" or "my".