diff --git a/cert/selfsigned.go b/cert/selfsigned.go index 574a37f3..f67e8cd8 100644 --- a/cert/selfsigned.go +++ b/cert/selfsigned.go @@ -100,6 +100,83 @@ func dnsNames(tlsExtraDomains []string) (string, []string) { return host, dnsNames } +// IsOutdated returns whether the given certificate is outdated w.r.t. the IPs +// and domains given. The certificate is considered up to date if it was +// created with _exactly_ the IPs and domains given. +func IsOutdated(cert *x509.Certificate, tlsExtraIPs, + tlsExtraDomains []string) (bool, error) { + + // Parse the slice of IP strings. + ips, err := ipAddresses(tlsExtraIPs) + if err != nil { + return false, err + } + + // To not consider the certificate outdated if it has duplicate IPs or + // if only the order has changed, we create two maps from the slice of + // IPs to compare. + ips1 := make(map[string]net.IP) + for _, ip := range ips { + ips1[ip.String()] = ip + } + + ips2 := make(map[string]net.IP) + for _, ip := range cert.IPAddresses { + ips2[ip.String()] = ip + } + + // If the certificate has a different number of IP addresses, it is + // definitely out of date. + if len(ips1) != len(ips2) { + return true, nil + } + + // Go through each IP address, and check that they are equal. We expect + // both the string representation and the exact IP to match. + for s, ip1 := range ips1 { + // Assert the IP string is found in both sets. + ip2, ok := ips2[s] + if !ok { + return true, nil + } + + // And that the IPs are considered equal. + if !ip1.Equal(ip2) { + return true, nil + } + } + + // Get the full list of DNS names to use. + _, dnsNames := dnsNames(tlsExtraDomains) + + // We do the same kind of deduplication for the DNS names. + dns1 := make(map[string]struct{}) + for _, n := range cert.DNSNames { + dns1[n] = struct{}{} + } + + dns2 := make(map[string]struct{}) + for _, n := range dnsNames { + dns2[n] = struct{}{} + } + + // If the number of domains are different, it is out of date. + if len(dns1) != len(dns2) { + return true, nil + } + + // Similarly, check that each DNS name matches what is found in the + // certificate. + for k := range dns1 { + if _, ok := dns2[k]; !ok { + return true, nil + } + } + + // Certificate was up-to-date. + return false, nil +} + // GenCertPair generates a key/cert pair to the paths provided. The // auto-generated certificates should *not* be used in production for public // access as they're self-signed and don't necessarily contain all of the diff --git a/cert/selfsigned_test.go b/cert/selfsigned_test.go new file mode 100644 index 00000000..0a5888a9 --- /dev/null +++ b/cert/selfsigned_test.go @@ -0,0 +1,135 @@ +package cert_test + +import ( + "io/ioutil" + "testing" + + "github.com/lightningnetwork/lnd/cert" +) + +var ( + extraIPs = []string{"1.1.1.1", "123.123.123.1", "199.189.12.12"} + extraDomains = []string{"home", "and", "away"} +) + +// TestIsOutdatedCert checks that we'll consider the TLS certificate outdated +// if the ip addresses or dns names don't match. +func TestIsOutdatedCert(t *testing.T) { + tempDir, err := ioutil.TempDir("", "certtest") + if err != nil { + t.Fatal(err) + } + + certPath := tempDir + "/tls.cert" + keyPath := tempDir + "/tls.key" + + // Generate TLS files with two extra IPs and domains. + err = cert.GenCertPair( + "lnd autogenerated cert", certPath, keyPath, extraIPs[:2], + extraDomains[:2], cert.DefaultAutogenValidity, + ) + if err != nil { + t.Fatal(err) + } + + // We'll attempt to check up-to-date status for all variants of 1-3 + // number of IPs and domains. + for numIPs := 1; numIPs <= len(extraIPs); numIPs++ { + for numDomains := 1; numDomains <= len(extraDomains); numDomains++ { + _, parsedCert, err := cert.LoadCert( + certPath, keyPath, + ) + if err != nil { + t.Fatal(err) + } + + // Using the test case's number of IPs and domains, get + // the outdated status of the certificate we created + // above. + outdated, err := cert.IsOutdated( + parsedCert, extraIPs[:numIPs], + extraDomains[:numDomains], + ) + if err != nil { + t.Fatal(err) + } + + // We expect it to be considered outdated if the IPs or + // domains don't match exactly what we created. + expected := numIPs != 2 || numDomains != 2 + if outdated != expected { + t.Fatalf("expected certificate to be "+ + "outdated=%v, got=%v", expected, + outdated) + } + } + } +} + +// TestIsOutdatedPermutation tests that the order of listed IPs or DNS names, +// nor dulicates in the lists, matter for whether we consider the certificate +// outdated. +func TestIsOutdatedPermutation(t *testing.T) { + tempDir, err := ioutil.TempDir("", "certtest") + if err != nil { + t.Fatal(err) + } + + certPath := tempDir + "/tls.cert" + keyPath := tempDir + "/tls.key" + + // Generate TLS files from the IPs and domains. + err = cert.GenCertPair( + "lnd autogenerated cert", certPath, keyPath, extraIPs[:], + extraDomains[:], cert.DefaultAutogenValidity, + ) + if err != nil { + t.Fatal(err) + } + _, parsedCert, err := cert.LoadCert(certPath, keyPath) + if err != nil { + t.Fatal(err) + } + + // If we have duplicate IPs or DNS names listed, that shouldn't matter. + dupIPs := make([]string, len(extraIPs)*2) + for i := range dupIPs { + dupIPs[i] = extraIPs[i/2] + } + + dupDNS := make([]string, len(extraDomains)*2) + for i := range dupDNS { + dupDNS[i] = extraDomains[i/2] + } + + outdated, err := cert.IsOutdated(parsedCert, dupIPs, dupDNS) + if err != nil { + t.Fatal(err) + } + + if outdated { + t.Fatalf("did not expect duplicate IPs or DNS names be " + + "considered outdated") + } + + // Similarly, the order of the lists shouldn't matter. + revIPs := make([]string, len(extraIPs)) + for i := range revIPs { + revIPs[i] = extraIPs[len(extraIPs)-1-i] + } + + revDNS := make([]string, len(extraDomains)) + for i := range revDNS { + revDNS[i] = extraDomains[len(extraDomains)-1-i] + } + + outdated, err = cert.IsOutdated(parsedCert, revIPs, revDNS) + if err != nil { + t.Fatal(err) + } + + if outdated { + t.Fatalf("did not expect reversed IPs or DNS names be " + + "considered outdated") + } +}