Skip to content

Commit 8819503

Browse files
committed
windows intermediate certificates: check intermediate certificate is validated by root
1 parent cc7cba2 commit 8819503

File tree

3 files changed

+136
-7
lines changed

3 files changed

+136
-7
lines changed

src/main/java/org/jetbrains/nativecerts/win32/Crypt32Ext.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,15 @@ public interface Crypt32Ext extends StdCallLibrary {
6161
int CERT_SYSTEM_STORE_UNPROTECTED_FLAG = 0x40000000;
6262
int CERT_SYSTEM_STORE_RELOCATE_FLAG = 0x80000000;
6363

64+
// Revocation flags for CertGetCertificateChain https://learn.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-certgetcertificatechain
65+
int CERT_CHAIN_REVOCATION_CHECK_END_CERT = 0x10000000;
66+
int CERT_CHAIN_REVOCATION_CHECK_CHAIN = 0x20000000;
67+
int CERT_CHAIN_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT = 0x40000000;
68+
int CERT_CHAIN_REVOCATION_CHECK_CACHE_ONLY = 0x80000000;
69+
70+
// for CertVerifyCertificateChainPolicy https://learn.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-certverifycertificatechainpolicy
71+
int CERT_CHAIN_POLICY_SSL = 4;
72+
6473
Crypt32Ext INSTANCE = Native.load("Crypt32", Crypt32Ext.class, W32APIOptions.DEFAULT_OPTIONS);
6574

6675
/**
@@ -98,4 +107,31 @@ WinCrypt.HCERTSTORE CertOpenStore(
98107
WinCrypt.HCRYPTPROV_LEGACY hCryptProv,
99108
int dwFlags,
100109
WTypes.LPWSTR pvPara);
110+
111+
/**
112+
* The {@code CertCreateCertificateContext} function creates a certificate context from an encoded certificate.
113+
* The created context is not persisted to a certificate store.
114+
* The function makes a copy of the encoded certificate within the created context.
115+
* @param dwCertEncodingType
116+
* [in] Specifies the type of encoding used. It is always acceptable to specify both the certificate and message
117+
* encoding types by combining them with a bitwise-OR operation as shown in the following example:
118+
* X509_ASN_ENCODING | PKCS_7_ASN_ENCODING. Currently, defined encoding types are:
119+
* X509_ASN_ENCODING PKCS_7_ASN_ENCODING
120+
* @param pbCertEncoded
121+
* [in] A pointer to a buffer that contains the encoded certificate from which the context is to be created.
122+
* @param cbCertEncoded
123+
* [in] The size, in bytes, of the {@code pbCertEncoded} buffer.
124+
* @return
125+
* If the function succeeds, the function returns a pointer to a read-only {@link WinCrypt.CERT_CONTEXT}.
126+
* When you have finished using the certificate context, free it by calling the {@link com.sun.jna.platform.win32.Crypt32#CertFreeCertificateContext(WinCrypt.CERT_CONTEXT)} function.
127+
* If the function is unable to decode and create the certificate context, it returns NULL.
128+
* For extended error information, call {@link Native#getLastError()}.
129+
*
130+
* @see <a href="https://learn.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-certcreatecertificatecontext">MSDN</a>
131+
*/
132+
WinCrypt.CERT_CONTEXT CertCreateCertificateContext(
133+
int dwCertEncodingType,
134+
byte[] pbCertEncoded,
135+
int cbCertEncoded
136+
);
101137
}

src/main/java/org/jetbrains/nativecerts/win32/Crypt32ExtUtil.java

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22

33
import com.sun.jna.Native;
44
import com.sun.jna.Pointer;
5+
import com.sun.jna.Structure;
56
import com.sun.jna.platform.win32.Crypt32;
67
import com.sun.jna.platform.win32.Kernel32Util;
78
import com.sun.jna.platform.win32.WTypes;
89
import com.sun.jna.platform.win32.Win32Exception;
910
import com.sun.jna.platform.win32.WinCrypt;
11+
import com.sun.jna.ptr.PointerByReference;
1012
import org.jetbrains.nativecerts.NativeTrustedRootsInternalUtils;
1113

1214
import java.security.cert.X509Certificate;
@@ -44,12 +46,13 @@ public static Collection<X509Certificate> getCustomTrustedRootCertificates() {
4446

4547
if (LOGGER.isLoggable(Level.FINE)) {
4648
StringBuilder message = new StringBuilder();
47-
message.append("Received ").append(root.size()).append(" certificates from store ROOT / ").append(entry.getKey());
48-
message.append("Received ").append(intermediates.size()).append(" certificates from store CA (Intermediates) / ").append(entry.getKey());
4949

50+
message.append("Received ").append(root.size()).append(" certificates from store ROOT / ").append(entry.getKey());
5051
for (X509Certificate certificate : root) {
5152
message.append("\n ROOT/").append(entry.getKey()).append(": ").append(certificate.getSubjectX500Principal());
5253
}
54+
55+
message.append("\nReceived ").append(intermediates.size()).append(" certificates from store CA (Intermediates) / ").append(entry.getKey());
5356
for (X509Certificate certificate : intermediates) {
5457
message.append("\n CA/").append(entry.getKey()).append(": ").append(certificate.getSubjectX500Principal());
5558
}
@@ -58,7 +61,18 @@ public static Collection<X509Certificate> getCustomTrustedRootCertificates() {
5861
}
5962

6063
result.addAll(root);
61-
result.addAll(intermediates);
64+
65+
for (X509Certificate intermediate : intermediates) {
66+
try {
67+
validateCertificate(intermediate.getEncoded());
68+
result.add(intermediate);
69+
} catch (Throwable t) {
70+
LOGGER.log(
71+
Level.FINE,
72+
"Unable to validate whether certificate '" + intermediate.getSubjectX500Principal() + "' is trusted: " + t.getMessage(),
73+
t);
74+
}
75+
}
6276
}
6377

6478
return result;
@@ -127,4 +141,67 @@ public static List<X509Certificate> gatherEnterpriseCertsForLocation(int locatio
127141
CertCloseStore(hcertstore);
128142
}
129143
}
144+
145+
public static void validateCertificate(byte[] encodedCertificate) {
146+
var certificateContext = Crypt32Ext.INSTANCE.CertCreateCertificateContext(
147+
WinCrypt.X509_ASN_ENCODING | WinCrypt.PKCS_7_ASN_ENCODING, encodedCertificate, encodedCertificate.length);
148+
if (certificateContext == null) {
149+
throw new Win32Exception(Native.getLastError());
150+
}
151+
152+
try {
153+
WinCrypt.CERT_CHAIN_PARA pChainPara = new WinCrypt.CERT_CHAIN_PARA();
154+
pChainPara.cbSize = pChainPara.size();
155+
pChainPara.RequestedUsage.dwType = WinCrypt.USAGE_MATCH_TYPE_AND;
156+
pChainPara.RequestedUsage.Usage.cUsageIdentifier = 0;
157+
pChainPara.RequestedUsage.Usage.rgpszUsageIdentifier = null;
158+
159+
PointerByReference ppChainContext = new PointerByReference();
160+
if (!Crypt32.INSTANCE.CertGetCertificateChain(
161+
null, certificateContext, null, null, pChainPara,
162+
Crypt32Ext.CERT_CHAIN_REVOCATION_CHECK_CACHE_ONLY,
163+
null, ppChainContext)) {
164+
throw new Win32Exception(Native.getLastError());
165+
}
166+
167+
if (ppChainContext.getValue() == null) {
168+
throw new IllegalStateException("CertGetCertificateChain was successful, but returned chain context is null");
169+
}
170+
171+
WinCrypt.CERT_CHAIN_CONTEXT pChainContext = Structure.newInstance(WinCrypt.CERT_CHAIN_CONTEXT.class, ppChainContext.getValue());
172+
pChainContext.read();
173+
174+
if (pChainContext.cbSize != pChainContext.size()) {
175+
throw new IllegalStateException("CertGetCertificateChain was successful, but returned chain context size is incorrect." +
176+
"returned cbSize is " + pChainContext.cbSize + ", but the structure size is " + pChainContext.size());
177+
}
178+
179+
try {
180+
WinCrypt.CERT_CHAIN_POLICY_PARA chainPolicyPara = new WinCrypt.CERT_CHAIN_POLICY_PARA();
181+
chainPolicyPara.cbSize = chainPolicyPara.size();
182+
chainPolicyPara.dwFlags = 0;
183+
184+
WinCrypt.CERT_CHAIN_POLICY_STATUS policyStatus = new WinCrypt.CERT_CHAIN_POLICY_STATUS();
185+
policyStatus.dwError = 1; // extra check that CertVerifyCertificateChainPolicy actually sets this field
186+
policyStatus.cbSize = policyStatus.size();
187+
188+
if (!Crypt32.INSTANCE.CertVerifyCertificateChainPolicy(
189+
new WTypes.LPSTR(Pointer.createConstant(Crypt32Ext.CERT_CHAIN_POLICY_SSL)),
190+
pChainContext, chainPolicyPara, policyStatus)) {
191+
throw new Win32Exception(Native.getLastError());
192+
}
193+
194+
int dwError = policyStatus.dwError;
195+
if (dwError != 0) {
196+
throw new Win32Exception(dwError, null, "CertVerifyCertificateChainPolicy failed to validate certificate with code " +
197+
Integer.toHexString(dwError) + ":\n" + Kernel32Util.formatMessage(dwError)) {
198+
};
199+
}
200+
} finally {
201+
Crypt32.INSTANCE.CertFreeCertificateChain(pChainContext);
202+
}
203+
} finally {
204+
Crypt32.INSTANCE.CertFreeCertificateContext(certificateContext);
205+
}
206+
}
130207
}

src/test/java/org/jetbrains/nativecerts/win32/Crypt32ExtUtilTest.java

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package org.jetbrains.nativecerts.win32;
22

3+
import com.sun.jna.platform.win32.Win32Exception;
4+
import com.sun.jna.platform.win32.WinError;
35
import org.jetbrains.nativecerts.NativeCertsSetupLoggingRule;
46
import org.junit.After;
57
import org.junit.Assert;
@@ -21,9 +23,9 @@
2123
import static org.jetbrains.nativecerts.NativeCertsTestUtil.isManualTestingEnabled;
2224
import static org.jetbrains.nativecerts.NativeTrustedRootsInternalUtils.isWindows;
2325
import static org.jetbrains.nativecerts.NativeTrustedRootsInternalUtils.sha1hex;
24-
import static org.jetbrains.nativecerts.NativeTrustedRootsInternalUtils.sha256hex;
2526
import static org.junit.Assert.assertEquals;
2627
import static org.junit.Assert.assertFalse;
28+
import static org.junit.Assert.assertThrows;
2729
import static org.junit.Assert.assertTrue;
2830

2931
public class Crypt32ExtUtilTest {
@@ -60,7 +62,9 @@ public void realUserTrustedCertificateTest() throws Exception {
6062

6163
byte[] encoded = getTestCertificate().getEncoded();
6264
String sha1 = sha1hex(encoded);
63-
String sha256 = sha256hex(encoded);
65+
66+
Win32Exception notTrustedException = assertThrows(Win32Exception.class, () -> Crypt32ExtUtil.validateCertificate(encoded));
67+
assertEquals(WinError.CERT_E_UNTRUSTEDROOT, notTrustedException.getErrorCode());
6468

6569
// cleanup just in case it was imported before
6670
removeTrustedCert(sha1);
@@ -75,6 +79,7 @@ public void realUserTrustedCertificateTest() throws Exception {
7579
List.of("certutil", "-user", "-addstore", "Root", getTestCertificatePath().toString())
7680
);
7781
assertTrue(verifyCert(sha1));
82+
Crypt32ExtUtil.validateCertificate(encoded);
7883

7984
Collection<X509Certificate> rootsAfter = Crypt32ExtUtil.getCustomTrustedRootCertificates();
8085
assertTrue(rootsAfter.contains(getTestCertificate()));
@@ -110,8 +115,8 @@ public void intermediateCACertsAreIncluded() throws Exception {
110115
try {
111116
Collection<X509Certificate> rootsBefore = Crypt32ExtUtil.getCustomTrustedRootCertificates();
112117
assertFalse(rootsBefore.contains(rootCertificate));
113-
114-
Assert.assertFalse(verifyCert(sha1Root));
118+
assertFalse(rootsBefore.contains(intermediateCertificate));
119+
assertFalse(verifyCert(sha1Root));
115120

116121
executeProcess(
117122
List.of("certutil", "-user", "-addstore", "Root", getTestCertificatePath().toString())
@@ -126,13 +131,24 @@ public void intermediateCACertsAreIncluded() throws Exception {
126131
Collection<X509Certificate> rootsAfter = Crypt32ExtUtil.getCustomTrustedRootCertificates();
127132
assertTrue(rootsAfter.contains(rootCertificate));
128133
assertTrue(rootsAfter.contains(intermediateCertificate));
134+
Crypt32ExtUtil.validateCertificate(intermediateEncoded);
129135

136+
// Remove only root
130137
assertTrue(removeTrustedCert(sha1Root));
131138
Assert.assertFalse(verifyCert(sha1Root));
132139

140+
// intermediate certificate should not be accepted after removing root
141+
Collection<X509Certificate> rootsAfterRootRemoval = Crypt32ExtUtil.getCustomTrustedRootCertificates();
142+
assertFalse(rootsAfterRootRemoval.contains(rootCertificate));
143+
assertFalse(rootsAfterRootRemoval.contains(intermediateCertificate));
144+
var noTrustedRoot = assertThrows(Win32Exception.class, () -> Crypt32ExtUtil.validateCertificate(intermediateEncoded));
145+
assertEquals(WinError.CERT_E_UNTRUSTEDROOT, noTrustedRoot.getErrorCode());
146+
147+
// Remove intermediate too
133148
assertTrue(removeTrustedCert(sha1Intermediate, "CA"));
134149
Assert.assertFalse(verifyCert(sha1Intermediate));
135150

151+
// Check that cleanup was successful
136152
Collection<X509Certificate> rootsAfterRemoval = Crypt32ExtUtil.getCustomTrustedRootCertificates();
137153
assertFalse(rootsAfterRemoval.contains(rootCertificate));
138154
assertFalse(rootsAfterRemoval.contains(intermediateCertificate));

0 commit comments

Comments
 (0)