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:
XoséandClaude Opus 5.5 committed 2026-10-17 19:20:54 +02:00
1 parent f7428287eb
commit 5eeaf0f519
1 file changed
+14 -6
+14 -6
View File
@@ -49,9 +49,11 @@ public final class CertVerifier {
public static final class Untrusted extends IOException { public static final class Untrusted extends IOException {
public final String fingerprint, subject, issuer, expires; public final String fingerprint, subject, issuer, expires;
public final boolean changed; // a pin was set and this is a different certificate 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); super("untrusted certificate " + fp);
this.chain = chain;
this.fingerprint = fp; this.fingerprint = fp;
this.subject = subject; this.subject = subject;
this.issuer = issuer; 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 = "?"; String subject = "?", issuer = "?", expires = "?";
try { try {
Certificate c = Certificate.getInstance(leafDer); Certificate c = Certificate.getInstance(leafDer);
@@ -101,7 +104,7 @@ public final class CertVerifier {
issuer = String.valueOf(c.getIssuer()); issuer = String.valueOf(c.getIssuer());
expires = new java.text.SimpleDateFormat("yyyy-MM-dd", Locale.US).format(c.getEndDate().getDate()); expires = new java.text.SimpleDateFormat("yyyy-MM-dd", Locale.US).format(c.getEndDate().getDate());
} catch (Exception ignored) {} } 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. */ /** 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) { if (p.length() > 0) {
// a pinned server: exactly that certificate, whatever its name or issuer // a pinned server: exactly that certificate, whatever its name or issuer
if (norm(sha256(chainDer.get(0))).equals(p)) return; 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"); if (roots == null) throw new IOException("No trust store configured");
java.io.ByteArrayOutputStream all = new java.io.ByteArrayOutputStream(); 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 { private static void verifyFully(List<byte[]> chainDer, String host) throws IOException {
final List<byte[]> chain0 = chainDer;
List<Certificate> chain = new ArrayList<Certificate>(); List<Certificate> chain = new ArrayList<Certificate>();
for (int i = 0; i < chainDer.size(); i++) chain.add(Certificate.getInstance(chainDer.get(i))); 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? // 1. Anchored directly by a trusted root?
List<byte[]> anchors = roots.find(issuerDer); List<byte[]> anchors = roots.find(issuerDer);
if (anchors != null) { if (anchors != null) {
byte[] currentDer = current.getEncoded(ASN1Encoding.DER);
for (int i = 0; i < anchors.size(); i++) { 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)); 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 (!isCa(cand)) continue;
if (signatureValid(current, cand)) { next = cand; break; } 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; current = next;
} }
throw new IOException("Certificate chain too long"); throw new IOException("Certificate chain too long");