104 lines
3.3 KiB
Go
104 lines
3.3 KiB
Go
package mitm
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"net"
|
|
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
var _ = Describe("IssueLeaf", func() {
|
|
It("chains to CA", func() {
|
|
ca, err := NewInMemoryCA()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
leaf, err := ca.IssueLeaf("api.anthropic.com")
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(len(leaf.Certificate)).To(BeNumerically(">=", 1), "leaf has no DER")
|
|
parsed, err := x509.ParseCertificate(leaf.Certificate[0])
|
|
Expect(err).NotTo(HaveOccurred())
|
|
// Verify it's actually signed by the CA we generated.
|
|
pool := x509.NewCertPool()
|
|
pool.AddCert(ca.Cert())
|
|
_, err = parsed.Verify(x509.VerifyOptions{
|
|
Roots: pool,
|
|
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
|
DNSName: "api.anthropic.com",
|
|
})
|
|
Expect(err).NotTo(HaveOccurred(), "verify chain")
|
|
})
|
|
|
|
It("populates DNS and IP SANs correctly", func() {
|
|
ca, err := NewInMemoryCA()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
// Hostname → DNSNames
|
|
leafDNS, err := ca.IssueLeaf("example.com")
|
|
Expect(err).NotTo(HaveOccurred())
|
|
parsedDNS, _ := x509.ParseCertificate(leafDNS.Certificate[0])
|
|
Expect(parsedDNS.DNSNames).NotTo(BeEmpty())
|
|
Expect(parsedDNS.DNSNames[0]).To(Equal("example.com"))
|
|
Expect(parsedDNS.IPAddresses).To(BeEmpty(), "hostname leaf should have no IP SAN")
|
|
|
|
// IP → IPAddresses
|
|
leafIP, err := ca.IssueLeaf("127.0.0.1")
|
|
Expect(err).NotTo(HaveOccurred())
|
|
parsedIP, _ := x509.ParseCertificate(leafIP.Certificate[0])
|
|
Expect(parsedIP.IPAddresses).NotTo(BeEmpty())
|
|
Expect(parsedIP.IPAddresses[0].Equal(net.ParseIP("127.0.0.1"))).To(BeTrue())
|
|
Expect(parsedIP.DNSNames).To(BeEmpty(), "IP leaf should have no DNS SAN")
|
|
})
|
|
|
|
It("caches by host", func() {
|
|
ca, err := NewInMemoryCA()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
a, _ := ca.IssueLeaf("api.example.com")
|
|
b, _ := ca.IssueLeaf("api.example.com")
|
|
Expect(a).To(BeIdenticalTo(b), "expected cached leaf to be returned, got distinct certs")
|
|
c, _ := ca.IssueLeaf("API.Example.com") // case-insensitive
|
|
Expect(a).To(BeIdenticalTo(c), "expected case-insensitive cache hit")
|
|
d, _ := ca.IssueLeaf("api.example.com:443") // host:port stripped
|
|
Expect(a).To(BeIdenticalTo(d), "expected port-stripped cache hit")
|
|
})
|
|
|
|
It("handshake accepted by client", func() {
|
|
// End-to-end check: a TLS server using the leaf, with a client
|
|
// trusting the CA, completes a handshake. This is the property
|
|
// every other flow in this package depends on.
|
|
ca, err := NewInMemoryCA()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
leaf, err := ca.IssueLeaf("localhost")
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
pool := x509.NewCertPool()
|
|
pool.AddCert(ca.Cert())
|
|
|
|
listener, err := tls.Listen("tcp", "127.0.0.1:0", &tls.Config{
|
|
Certificates: []tls.Certificate{*leaf},
|
|
})
|
|
Expect(err).NotTo(HaveOccurred())
|
|
defer func() { _ = listener.Close() }()
|
|
|
|
go func() {
|
|
conn, err := listener.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer func() { _ = conn.Close() }()
|
|
_, _ = conn.Write([]byte("ok"))
|
|
}()
|
|
|
|
conn, err := tls.Dial("tcp", listener.Addr().String(), &tls.Config{
|
|
RootCAs: pool,
|
|
ServerName: "localhost",
|
|
})
|
|
Expect(err).NotTo(HaveOccurred(), "client TLS dial")
|
|
defer func() { _ = conn.Close() }()
|
|
buf := make([]byte, 2)
|
|
_, err = conn.Read(buf)
|
|
Expect(err).NotTo(HaveOccurred(), "read")
|
|
Expect(string(buf)).To(Equal("ok"))
|
|
})
|
|
})
|