Skip to content

Commit ad79293

Browse files
committed
fix(x509): enforce leaf SVID validation and reject multiple URI SANs
Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
1 parent df407fa commit ad79293

1 file changed

Lines changed: 95 additions & 0 deletions

File tree

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package io.spiffe.svid.x509svid;
2+
3+
import io.spiffe.internal.CertificateUtils;
4+
import io.spiffe.spiffeid.SpiffeId;
5+
6+
import java.security.cert.CertificateException;
7+
import java.security.cert.X509Certificate;
8+
import java.util.Objects;
9+
10+
/**
11+
* Shared validation checks for X.509-SVID certificates.
12+
* <p>
13+
* References:
14+
* <ul>
15+
* <li>SPIFFE X.509-SVID specification, section 5.2 (Leaf Validation)</li>
16+
* <li>SPIFFE X.509-SVID specification, section 4.3 (Key Usage)</li>
17+
* </ul>
18+
*/
19+
final class X509SvidChecks {
20+
21+
private X509SvidChecks() {
22+
}
23+
24+
/**
25+
* Validates the leaf-certificate constraints that are shared across parse-time and authentication-time
26+
* validation paths.
27+
* <p>
28+
* Contract:
29+
* <ul>
30+
* <li>The certificate is treated as a leaf and MUST NOT be a CA certificate.</li>
31+
* <li>The certificate MUST NOT set {@code keyCertSign} or {@code cRLSign} key usages.</li>
32+
* <li>The SPIFFE ID MUST have a non-root path component.</li>
33+
* </ul>
34+
* This method intentionally excludes parse-only requirements (for example, requiring
35+
* {@code digitalSignature}, which is defined in section 4.3) so callers can reuse this
36+
* baseline policy in multiple contexts.
37+
*
38+
* @param leaf leaf certificate to validate
39+
* @param spiffeId parsed SPIFFE ID from the leaf certificate
40+
* @throws CertificateException if any shared leaf constraint is violated
41+
*/
42+
static void validateLeafConstraints(final X509Certificate leaf, final SpiffeId spiffeId)
43+
throws CertificateException {
44+
Objects.requireNonNull(leaf, "leaf must not be null");
45+
Objects.requireNonNull(spiffeId, "spiffeId must not be null");
46+
47+
if (CertificateUtils.isCA(leaf)) {
48+
throw new CertificateException("Leaf certificate must not have CA flag set to true");
49+
}
50+
if (CertificateUtils.hasKeyUsageCertSign(leaf)) {
51+
throw new CertificateException("Leaf certificate must not have 'keyCertSign' as key usage");
52+
}
53+
if (CertificateUtils.hasKeyUsageCRLSign(leaf)) {
54+
throw new CertificateException("Leaf certificate must not have 'cRLSign' as key usage");
55+
}
56+
57+
final String path = spiffeId.getPath();
58+
if (path == null || path.isEmpty()) {
59+
throw new CertificateException("Leaf certificate SPIFFE ID must have a non-root path");
60+
}
61+
}
62+
63+
/**
64+
* Validates leaf-certificate requirements for {@link X509Svid} parsing.
65+
* <p>
66+
* Contract:
67+
* <ul>
68+
* <li>Applies all checks from {@link #validateLeafConstraints(X509Certificate, SpiffeId)}.</li>
69+
* <li>Additionally requires {@code digitalSignature} key usage to be set (section 4.3).</li>
70+
* </ul>
71+
* This parse-time contract is intentionally stricter than authentication-time baseline checks.
72+
*
73+
* @param leaf leaf certificate to validate
74+
* @param spiffeId parsed SPIFFE ID from the leaf certificate
75+
* @throws CertificateException if any parse-time leaf requirement is violated
76+
*/
77+
static void validateLeafForParsing(final X509Certificate leaf, final SpiffeId spiffeId)
78+
throws CertificateException {
79+
if (!CertificateUtils.hasKeyUsageDigitalSignature(leaf)) {
80+
throw new CertificateException("Leaf certificate must have 'digitalSignature' as key usage");
81+
}
82+
validateLeafConstraints(leaf, spiffeId);
83+
}
84+
85+
static void validateSigningCertificate(final X509Certificate cert) throws CertificateException {
86+
Objects.requireNonNull(cert, "cert must not be null");
87+
88+
if (!CertificateUtils.isCA(cert)) {
89+
throw new CertificateException("Signing certificate must have CA flag set to true");
90+
}
91+
if (!CertificateUtils.hasKeyUsageCertSign(cert)) {
92+
throw new CertificateException("Signing certificate must have 'keyCertSign' as key usage");
93+
}
94+
}
95+
}

0 commit comments

Comments
 (0)