diff --git a/README.md b/README.md index 903d466..bf566f2 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,9 @@ Contents: certchain/ Display the certificate chain from a TLS connection. certdump/ Dump certificate information. + certexpiry/ Print a list of certificate subjects and expiry times + or warn about certificates expiring within a certain + window. certverify/ Verify a TLS X.509 certificate, optionally printing the time to expiry and checking for revocations. clustersh/ Run commands or transfer files across multiple @@ -20,16 +23,16 @@ Contents: jlp/ JSON linter/prettifier. pem2bin/ Dump the binary body of a PEM-encoded block. pembody/ Print the body of a PEM certificate. - pemit/ Dump data to a PEM file. + pemit/ Dump data to a PEM file. showimp/ List the external (e.g. non-stdlib and outside the current working directory) imports for a Go file. readchain/ Print the common name for the certificates in a bundle. - showimp Display the external imports in a package. + showimp Display the external imports in a package. stealchain/ Dump the verified chain from a TLS connection. tlskeypair/ Check whether a TLS certificate and key file match. - utc/ Convert times to UTC. + utc/ Convert times to UTC. die/ Death of a program. fileutil/ Common file functions. lib/ Commonly-useful functions for writing Go programs. diff --git a/cmd/certexpiry/README b/cmd/certexpiry/README new file mode 100644 index 0000000..bf0fe9f --- /dev/null +++ b/cmd/certexpiry/README @@ -0,0 +1,24 @@ +certexpiry + +Print a list of certificates and their expiry, or only warn about +upcoming expiries. + +It takes a list of PEM-encoded certificates, and compares the NotAfter +value to the window given by the -t flag (which defaults to 2160 hours, +or 90 days). Alternatively, given the -q flag, it will only warn about +certificates expiring in the window. + +Example, run on the cfssl-trust[1] CA bundle: + +$ certexpiry -q ca-bundle.crt +/GPKIRootCA/C=KR/O=Government of Korea/OU=GPKI/SN=93008982654396041992798201139454296355 expires on 2017-03-15 06:00:04 +0000 UTC (in 1633h0m44.686144136s) +/CA DATEV BT 01/C=DE/O=DATEV eG/SN=139288328771231810070797444106717912243 expires on 2017-01-09 13:42:30 +0000 UTC (in 80h43m10.685385355s) +/CA DATEV STD 01/C=DE/O=DATEV eG/SN=142389455970744957119921172249094394891 expires on 2017-01-09 13:42:30 +0000 UTC (in 80h43m10.685236793s) +/CA DATEV INT 01/C=DE/O=DATEV eG/SN=169035066479776292612803392462688126470 expires on 2017-01-09 13:42:30 +0000 UTC (in 80h43m10.685208087s) +$ certexpiry ca-bundle.crt | head -5 +/http://www.valicert.com//O=ValiCert, Inc./OU=ValiCert Class 3 Policy Validation Authority/L=ValiCert Validation Network/SN=1 expires on 2019-06-26 00:22:33 +0000 UTC (in 21619h22m44.060898709s) +/QuoVadis Root CA 2/C=BM/O=QuoVadis Limited/SN=1289 expires on 2031-11-24 18:23:33 +0000 UTC (in 130453h23m44.060878817s) +/C=US/O=VeriSign, Inc./OU=Class 3 Public Primary Certification Authority/SN=149843929435818692848040365716851702463 expires on 2028-08-01 23:59:59 +0000 UTC (in 101419h0m10.06087362s) +/Equifax Secure Global eBusiness CA-1/C=US/O=Equifax Secure Inc./SN=1 expires on 2020-06-21 04:00:00 +0000 UTC (in 30287h0m11.060869101s) +/thawte Primary Root CA/C=US/O=thawte, Inc./OU=Certification Services Division/OU=(c) 2006 thawte, Inc. - For authorized use only/SN=69529181992039203566298953787712940909 expires on 2036-07-16 23:59:59 +0000 UTC (in 171163h0m10.060864304s) + diff --git a/cmd/certexpiry/main.go b/cmd/certexpiry/main.go new file mode 100644 index 0000000..ed46ae0 --- /dev/null +++ b/cmd/certexpiry/main.go @@ -0,0 +1,100 @@ +package main + +import ( + "crypto/x509" + "crypto/x509/pkix" + "flag" + "fmt" + "io/ioutil" + "os" + "strings" + "time" + + "github.com/cloudflare/cfssl/helpers" + "github.com/kisom/goutils/die" + "github.com/kisom/goutils/lib" +) + +var warnOnly bool +var leeway = 2160 * time.Hour // three months + +func displayName(name pkix.Name) string { + var ns []string + + if name.CommonName != "" { + ns = append(ns, name.CommonName) + } + + for i := range name.Country { + ns = append(ns, fmt.Sprintf("C=%s", name.Country[i])) + } + + for i := range name.Organization { + ns = append(ns, fmt.Sprintf("O=%s", name.Organization[i])) + } + + for i := range name.OrganizationalUnit { + ns = append(ns, fmt.Sprintf("OU=%s", name.OrganizationalUnit[i])) + } + + for i := range name.Locality { + ns = append(ns, fmt.Sprintf("L=%s", name.Locality[i])) + } + + for i := range name.Province { + ns = append(ns, fmt.Sprintf("ST=%s", name.Province[i])) + } + + if len(ns) > 0 { + return "/" + strings.Join(ns, "/") + } + + die.With("no subject information in root") + return "" +} + +func expires(cert *x509.Certificate) time.Duration { + return cert.NotAfter.Sub(time.Now()) +} + +func inDanger(cert *x509.Certificate) bool { + return expires(cert) < leeway +} + +func checkCert(cert *x509.Certificate) { + warn := inDanger(cert) + name := displayName(cert.Subject) + name = fmt.Sprintf("%s/SN=%s", name, cert.SerialNumber) + expiry := expires(cert) + if warnOnly { + if warn { + fmt.Fprintf(os.Stderr, "%s expires on %s (in %s)\n", name, cert.NotAfter, expiry) + } + } else { + fmt.Printf("%s expires on %s (in %s)\n", name, cert.NotAfter, expiry) + } +} + +func main() { + flag.BoolVar(&warnOnly, "q", false, "only warn about expiring certs") + flag.DurationVar(&leeway, "t", leeway, "warn if certificates are closer than this to expiring") + flag.Parse() + + for _, file := range flag.Args() { + in, err := ioutil.ReadFile(file) + if err != nil { + lib.Warn(err, "failed to read file") + continue + } + + certs, err := helpers.ParseCertificatesPEM(in) + if err != nil { + lib.Warn(err, "while parsing certificates") + continue + } + + for _, cert := range certs { + checkCert(cert) + } + } +}