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 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");
|
||||||
|
|||||||
Reference in new issue
Block a user