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