Shared TLS client: trust anchors must be CAs, untrusted errors carry the chain
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f7428287eb
commit
5eeaf0f519
1 file changed
+14
-6
@@ -49,9 +49,11 @@ public final class CertVerifier {
|
||||
public static final class Untrusted extends IOException {
|
||||
public final String fingerprint, subject, issuer, expires;
|
||||
public final boolean changed; // a pin was set and this is a different certificate
|
||||
public final List<byte[]> chain; // as the server sent it, leaf first
|
||||
|
||||
Untrusted(String fp, String subject, String issuer, String expires, boolean changed) {
|
||||
Untrusted(String fp, String subject, String issuer, String expires, boolean changed, List<byte[]> chain) {
|
||||
super("untrusted certificate " + fp);
|
||||
this.chain = chain;
|
||||
this.fingerprint = fp;
|
||||
this.subject = subject;
|
||||
this.issuer = issuer;
|
||||
@@ -93,7 +95,8 @@ public final class CertVerifier {
|
||||
}
|
||||
}
|
||||
|
||||
private static Untrusted untrusted(byte[] leafDer, boolean changed) {
|
||||
private static Untrusted untrusted(List<byte[]> chainDer, boolean changed) {
|
||||
byte[] leafDer = chainDer.get(0);
|
||||
String subject = "?", issuer = "?", expires = "?";
|
||||
try {
|
||||
Certificate c = Certificate.getInstance(leafDer);
|
||||
@@ -101,7 +104,7 @@ public final class CertVerifier {
|
||||
issuer = String.valueOf(c.getIssuer());
|
||||
expires = new java.text.SimpleDateFormat("yyyy-MM-dd", Locale.US).format(c.getEndDate().getDate());
|
||||
} catch (Exception ignored) {}
|
||||
return new Untrusted(sha256(leafDer), subject, issuer, expires, changed);
|
||||
return new Untrusted(sha256(leafDer), subject, issuer, expires, changed, chainDer);
|
||||
}
|
||||
|
||||
/** Lookup of trusted root certificates (DER) by the DER encoding of their subject name. */
|
||||
@@ -130,7 +133,7 @@ public final class CertVerifier {
|
||||
if (p.length() > 0) {
|
||||
// a pinned server: exactly that certificate, whatever its name or issuer
|
||||
if (norm(sha256(chainDer.get(0))).equals(p)) return;
|
||||
throw untrusted(chainDer.get(0), true);
|
||||
throw untrusted(chainDer, true);
|
||||
}
|
||||
if (roots == null) throw new IOException("No trust store configured");
|
||||
java.io.ByteArrayOutputStream all = new java.io.ByteArrayOutputStream();
|
||||
@@ -149,6 +152,7 @@ public final class CertVerifier {
|
||||
}
|
||||
|
||||
private static void verifyFully(List<byte[]> chainDer, String host) throws IOException {
|
||||
final List<byte[]> chain0 = chainDer;
|
||||
List<Certificate> chain = new ArrayList<Certificate>();
|
||||
for (int i = 0; i < chainDer.size(); i++) chain.add(Certificate.getInstance(chainDer.get(i)));
|
||||
|
||||
@@ -171,9 +175,13 @@ public final class CertVerifier {
|
||||
// 1. Anchored directly by a trusted root?
|
||||
List<byte[]> anchors = roots.find(issuerDer);
|
||||
if (anchors != null) {
|
||||
byte[] currentDer = current.getEncoded(ASN1Encoding.DER);
|
||||
for (int i = 0; i < anchors.size(); i++) {
|
||||
// the very certificate the user chose to trust (e.g. self-signed)
|
||||
if (java.util.Arrays.equals(anchors.get(i), currentDer)) return;
|
||||
Certificate root = Certificate.getInstance(anchors.get(i));
|
||||
if (signatureValid(current, root)) return; // trusted
|
||||
// anything that signs must be a CA: a trusted site's own certificate can't vouch for another
|
||||
if (isCa(root) && signatureValid(current, root)) return; // trusted
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,7 +194,7 @@ public final class CertVerifier {
|
||||
if (!isCa(cand)) continue;
|
||||
if (signatureValid(current, cand)) { next = cand; break; }
|
||||
}
|
||||
if (next == null) throw untrusted(chainDer.get(0), false);
|
||||
if (next == null) throw untrusted(chain0, false);
|
||||
current = next;
|
||||
}
|
||||
throw new IOException("Certificate chain too long");
|
||||
|
||||
Reference in new issue
Block a user