add cert-revcheck
This commit is contained in:
36
cmd/cert-revcheck/README.txt
Normal file
36
cmd/cert-revcheck/README.txt
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
cert-revcheck: check certificate expiry and revocation
|
||||||
|
-----------------------------------------------------
|
||||||
|
|
||||||
|
Description
|
||||||
|
cert-revcheck accepts a list of certificate files (PEM or DER) or
|
||||||
|
site addresses (host[:port]) and checks whether the leaf certificate
|
||||||
|
is expired or revoked. Revocation checks use CRL and OCSP via the
|
||||||
|
certlib/revoke package.
|
||||||
|
|
||||||
|
Usage
|
||||||
|
cert-revcheck [options] <target> [<target>...]
|
||||||
|
|
||||||
|
Options
|
||||||
|
-hardfail treat revocation check failures as fatal (default: false)
|
||||||
|
-timeout dur HTTP/OCSP/CRL timeout for network operations (default: 10s)
|
||||||
|
-v verbose output
|
||||||
|
|
||||||
|
Targets
|
||||||
|
- File paths to certificates in PEM or DER format.
|
||||||
|
- Site addresses in the form host or host:port. If no port is
|
||||||
|
provided, 443 is assumed.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
# Check a PEM file
|
||||||
|
cert-revcheck ./server.pem
|
||||||
|
|
||||||
|
# Check a DER (single) certificate
|
||||||
|
cert-revcheck ./server.der
|
||||||
|
|
||||||
|
# Check a live site (leaf certificate)
|
||||||
|
cert-revcheck example.com:443
|
||||||
|
|
||||||
|
Notes
|
||||||
|
- For sites, only the leaf certificate is checked.
|
||||||
|
- When -hardfail is set, network issues during OCSP/CRL fetch will
|
||||||
|
cause the check to fail (treated as revoked).
|
||||||
139
cmd/cert-revcheck/main.go
Normal file
139
cmd/cert-revcheck/main.go
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/kyle/goutils/certlib"
|
||||||
|
hosts "git.wntrmute.dev/kyle/goutils/certlib/hosts"
|
||||||
|
"git.wntrmute.dev/kyle/goutils/certlib/revoke"
|
||||||
|
"git.wntrmute.dev/kyle/goutils/fileutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
hardfail bool
|
||||||
|
timeout time.Duration
|
||||||
|
verbose bool
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.BoolVar(&hardfail, "hardfail", false, "treat revocation check failures as fatal")
|
||||||
|
flag.DurationVar(&timeout, "timeout", 10*time.Second, "network timeout for OCSP/CRL fetches and TLS site connects")
|
||||||
|
flag.BoolVar(&verbose, "v", false, "verbose output")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
revoke.HardFail = hardfail
|
||||||
|
// Set HTTP client timeout for revocation library
|
||||||
|
revoke.HTTPClient.Timeout = timeout
|
||||||
|
|
||||||
|
if flag.NArg() == 0 {
|
||||||
|
fmt.Fprintf(os.Stderr, "Usage: %s [options] <target> [<target>...]\n", os.Args[0])
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
exitCode := 0
|
||||||
|
for _, target := range flag.Args() {
|
||||||
|
status, err := processTarget(target)
|
||||||
|
switch status {
|
||||||
|
case "OK":
|
||||||
|
fmt.Printf("%s: OK\n", target)
|
||||||
|
case "EXPIRED":
|
||||||
|
fmt.Printf("%s: EXPIRED: %v\n", target, err)
|
||||||
|
exitCode = 1
|
||||||
|
case "REVOKED":
|
||||||
|
fmt.Printf("%s: REVOKED\n", target)
|
||||||
|
exitCode = 1
|
||||||
|
case "UNKNOWN":
|
||||||
|
fmt.Printf("%s: UNKNOWN: %v\n", target, err)
|
||||||
|
if hardfail {
|
||||||
|
// In hardfail, treat unknown as failure
|
||||||
|
exitCode = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Exit(exitCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func processTarget(target string) (string, error) {
|
||||||
|
if fileutil.FileDoesExist(target) {
|
||||||
|
return checkFile(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not a file; treat as site
|
||||||
|
return checkSite(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkFile(path string) (string, error) {
|
||||||
|
in, err := ioutil.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return "UNKNOWN", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try PEM first; if that fails, try single DER cert
|
||||||
|
certs, err := certlib.ReadCertificates(in)
|
||||||
|
if err != nil || len(certs) == 0 {
|
||||||
|
cert, _, derr := certlib.ReadCertificate(in)
|
||||||
|
if derr != nil || cert == nil {
|
||||||
|
if err == nil {
|
||||||
|
err = derr
|
||||||
|
}
|
||||||
|
return "UNKNOWN", err
|
||||||
|
}
|
||||||
|
return evaluateCert(cert)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evaluate the first certificate (leaf) by default
|
||||||
|
return evaluateCert(certs[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkSite(hostport string) (string, error) {
|
||||||
|
// Use certlib/hosts to parse host/port (supports https URLs and host:port)
|
||||||
|
target, err := hosts.ParseHost(hostport)
|
||||||
|
if err != nil {
|
||||||
|
return "UNKNOWN", err
|
||||||
|
}
|
||||||
|
|
||||||
|
d := &net.Dialer{Timeout: timeout}
|
||||||
|
conn, err := tls.DialWithDialer(d, "tcp", target.String(), &tls.Config{InsecureSkipVerify: true, ServerName: target.Host})
|
||||||
|
if err != nil {
|
||||||
|
return "UNKNOWN", err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
state := conn.ConnectionState()
|
||||||
|
if len(state.PeerCertificates) == 0 {
|
||||||
|
return "UNKNOWN", fmt.Errorf("no peer certificates presented")
|
||||||
|
}
|
||||||
|
return evaluateCert(state.PeerCertificates[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func evaluateCert(cert *x509.Certificate) (string, error) {
|
||||||
|
// Expiry check
|
||||||
|
now := time.Now()
|
||||||
|
if !now.Before(cert.NotAfter) {
|
||||||
|
return "EXPIRED", fmt.Errorf("expired at %s", cert.NotAfter)
|
||||||
|
}
|
||||||
|
if !now.After(cert.NotBefore) {
|
||||||
|
return "EXPIRED", fmt.Errorf("not valid until %s", cert.NotBefore)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Revocation check using certlib/revoke
|
||||||
|
revoked, ok, err := revoke.VerifyCertificateError(cert)
|
||||||
|
if revoked {
|
||||||
|
// If revoked is true, ok will be true per implementation, err may describe why
|
||||||
|
return "REVOKED", err
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
// Revocation status could not be determined
|
||||||
|
return "UNKNOWN", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return "OK", nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user