cmd/ca-signed: clean up the codebase
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
|||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/goutils/certlib/certerr"
|
"git.wntrmute.dev/kyle/goutils/certlib/certerr"
|
||||||
@@ -93,3 +94,18 @@ func LoadCertificates(path string) ([]*x509.Certificate, error) {
|
|||||||
|
|
||||||
return ReadCertificates(in)
|
return ReadCertificates(in)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func PoolFromBytes(certBytes []byte) (*x509.CertPool, error) {
|
||||||
|
pool := x509.NewCertPool()
|
||||||
|
|
||||||
|
certs, err := ReadCertificates(certBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read certificates: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cert := range certs {
|
||||||
|
pool.AddCert(cert)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pool, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,157 +1,22 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"embed"
|
"embed"
|
||||||
"errors"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/goutils/certlib"
|
"git.wntrmute.dev/kyle/goutils/certlib"
|
||||||
|
"git.wntrmute.dev/kyle/goutils/certlib/verify"
|
||||||
|
"git.wntrmute.dev/kyle/goutils/die"
|
||||||
|
"git.wntrmute.dev/kyle/goutils/lib"
|
||||||
)
|
)
|
||||||
|
|
||||||
// loadCertsFromFile attempts to parse certificates from a file that may be in
|
|
||||||
// PEM or DER/PKCS#7 format. Returns the parsed certificates or an error.
|
|
||||||
func loadCertsFromFile(path string) ([]*x509.Certificate, error) {
|
|
||||||
var certs []*x509.Certificate
|
|
||||||
|
|
||||||
data, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if certs, err = certlib.ParseCertificatesPEM(data); err == nil {
|
|
||||||
return certs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if certs, _, err = certlib.ParseCertificatesDER(data, ""); err == nil {
|
|
||||||
return certs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func makePoolFromFile(path string) (*x509.CertPool, error) {
|
|
||||||
// Try PEM via helper (it builds a pool)
|
|
||||||
if pool, err := certlib.LoadPEMCertPool(path); err == nil && pool != nil {
|
|
||||||
return pool, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: read as DER(s), add to a new pool
|
|
||||||
certs, err := loadCertsFromFile(path)
|
|
||||||
if err != nil || len(certs) == 0 {
|
|
||||||
return nil, fmt.Errorf("failed to load CA certificates from %s", path)
|
|
||||||
}
|
|
||||||
pool := x509.NewCertPool()
|
|
||||||
for _, c := range certs {
|
|
||||||
pool.AddCert(c)
|
|
||||||
}
|
|
||||||
return pool, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
//go:embed testdata/*.pem
|
//go:embed testdata/*.pem
|
||||||
var embeddedTestdata embed.FS
|
var embeddedTestdata embed.FS
|
||||||
|
|
||||||
// loadCertsFromBytes attempts to parse certificates from bytes that may be in
|
|
||||||
// PEM or DER/PKCS#7 format.
|
|
||||||
func loadCertsFromBytes(data []byte) ([]*x509.Certificate, error) {
|
|
||||||
certs, err := certlib.ParseCertificatesPEM(data)
|
|
||||||
if err == nil {
|
|
||||||
return certs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
certs, _, err = certlib.ParseCertificatesDER(data, "")
|
|
||||||
if err == nil {
|
|
||||||
return certs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func makePoolFromBytes(data []byte) (*x509.CertPool, error) {
|
|
||||||
certs, err := loadCertsFromBytes(data)
|
|
||||||
if err != nil || len(certs) == 0 {
|
|
||||||
return nil, errors.New("failed to load CA certificates from embedded bytes")
|
|
||||||
}
|
|
||||||
pool := x509.NewCertPool()
|
|
||||||
for _, c := range certs {
|
|
||||||
pool.AddCert(c)
|
|
||||||
}
|
|
||||||
return pool, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// isSelfSigned returns true if the given certificate is self-signed.
|
|
||||||
// It checks that the subject and issuer match and that the certificate's
|
|
||||||
// signature verifies against its own public key.
|
|
||||||
func isSelfSigned(cert *x509.Certificate) bool {
|
|
||||||
if cert == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// Quick check: subject and issuer match
|
|
||||||
if cert.Subject.String() != cert.Issuer.String() {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// Cryptographic check: the certificate is signed by itself
|
|
||||||
if err := cert.CheckSignatureFrom(cert); err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func verifyAgainstCA(caPool *x509.CertPool, path string) (bool, string) {
|
|
||||||
certs, err := loadCertsFromFile(path)
|
|
||||||
if err != nil || len(certs) == 0 {
|
|
||||||
return false, ""
|
|
||||||
}
|
|
||||||
|
|
||||||
leaf := certs[0]
|
|
||||||
ints := x509.NewCertPool()
|
|
||||||
if len(certs) > 1 {
|
|
||||||
for _, ic := range certs[1:] {
|
|
||||||
ints.AddCert(ic)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
opts := x509.VerifyOptions{
|
|
||||||
Roots: caPool,
|
|
||||||
Intermediates: ints,
|
|
||||||
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
|
|
||||||
}
|
|
||||||
if _, err = leaf.Verify(opts); err != nil {
|
|
||||||
return false, ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return true, leaf.NotAfter.Format("2006-01-02")
|
|
||||||
}
|
|
||||||
|
|
||||||
func verifyAgainstCABytes(caPool *x509.CertPool, certData []byte) (bool, string) {
|
|
||||||
certs, err := loadCertsFromBytes(certData)
|
|
||||||
if err != nil || len(certs) == 0 {
|
|
||||||
return false, ""
|
|
||||||
}
|
|
||||||
|
|
||||||
leaf := certs[0]
|
|
||||||
ints := x509.NewCertPool()
|
|
||||||
if len(certs) > 1 {
|
|
||||||
for _, ic := range certs[1:] {
|
|
||||||
ints.AddCert(ic)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
opts := x509.VerifyOptions{
|
|
||||||
Roots: caPool,
|
|
||||||
Intermediates: ints,
|
|
||||||
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
|
|
||||||
}
|
|
||||||
if _, err = leaf.Verify(opts); err != nil {
|
|
||||||
return false, ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return true, leaf.NotAfter.Format("2006-01-02")
|
|
||||||
}
|
|
||||||
|
|
||||||
type testCase struct {
|
type testCase struct {
|
||||||
name string
|
name string
|
||||||
caFile string
|
caFile string
|
||||||
@@ -170,18 +35,25 @@ func (tc testCase) Run() error {
|
|||||||
return fmt.Errorf("selftest: failed to read embedded %s: %w", tc.certFile, err)
|
return fmt.Errorf("selftest: failed to read embedded %s: %w", tc.certFile, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
pool, err := makePoolFromBytes(caBytes)
|
pool, err := certlib.PoolFromBytes(caBytes)
|
||||||
if err != nil || pool == nil {
|
if err != nil || pool == nil {
|
||||||
return fmt.Errorf("selftest: failed to build CA pool for %s: %w", tc.caFile, err)
|
return fmt.Errorf("selftest: failed to build CA pool for %s: %w", tc.caFile, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ok, exp := verifyAgainstCABytes(pool, certBytes)
|
cert, _, err := certlib.ReadCertificate(certBytes)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("selftest: failed to parse certificate from %s: %w", tc.certFile, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = verify.CertWith(cert, pool, nil, false)
|
||||||
|
ok := err == nil
|
||||||
|
|
||||||
if ok != tc.expectOK {
|
if ok != tc.expectOK {
|
||||||
return fmt.Errorf("%s: unexpected result: got %v, want %v", tc.name, ok, tc.expectOK)
|
return fmt.Errorf("%s: unexpected result: got %v, want %v", tc.name, ok, tc.expectOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ok {
|
if ok {
|
||||||
fmt.Printf("%s: OK (expires %s)\n", tc.name, exp)
|
fmt.Printf("%s: OK (expires %s)\n", tc.name, cert.NotAfter.Format(lib.DateShortFormat))
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s: INVALID (as expected)\n", tc.name)
|
fmt.Printf("%s: INVALID (as expected)\n", tc.name)
|
||||||
@@ -237,14 +109,16 @@ func selftest() int {
|
|||||||
failures++
|
failures++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
certs, err := loadCertsFromBytes(b)
|
|
||||||
|
certs, err := certlib.ReadCertificates(b)
|
||||||
if err != nil || len(certs) == 0 {
|
if err != nil || len(certs) == 0 {
|
||||||
fmt.Fprintf(os.Stderr, "selftest: failed to parse cert(s) from %s: %v\n", root, err)
|
fmt.Fprintf(os.Stderr, "selftest: failed to parse cert(s) from %s: %v\n", root, err)
|
||||||
failures++
|
failures++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
leaf := certs[0]
|
leaf := certs[0]
|
||||||
if isSelfSigned(leaf) {
|
if len(leaf.AuthorityKeyId) == 0 || bytes.Equal(leaf.AuthorityKeyId, leaf.SubjectKeyId) {
|
||||||
fmt.Printf("%s: SELF-SIGNED (as expected)\n", root)
|
fmt.Printf("%s: SELF-SIGNED (as expected)\n", root)
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("%s: expected SELF-SIGNED, but was not detected as such\n", root)
|
fmt.Printf("%s: expected SELF-SIGNED, but was not detected as such\n", root)
|
||||||
@@ -260,66 +134,58 @@ func selftest() int {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// expiryString returns a YYYY-MM-DD date string to display for certificate
|
|
||||||
// expiry. If an explicit exp string is provided, it is used. Otherwise, if a
|
|
||||||
// leaf certificate is available, its NotAfter is formatted. As a last resort,
|
|
||||||
// it falls back to today's date (should not normally happen).
|
|
||||||
func expiryString(leaf *x509.Certificate, exp string) string {
|
|
||||||
if exp != "" {
|
|
||||||
return exp
|
|
||||||
}
|
|
||||||
if leaf != nil {
|
|
||||||
return leaf.NotAfter.Format("2006-01-02")
|
|
||||||
}
|
|
||||||
return time.Now().Format("2006-01-02")
|
|
||||||
}
|
|
||||||
|
|
||||||
// processCert verifies a single certificate file against the provided CA pool
|
|
||||||
// and prints the result in the required format, handling self-signed
|
|
||||||
// certificates specially.
|
|
||||||
func processCert(caPool *x509.CertPool, certPath string) {
|
|
||||||
ok, exp := verifyAgainstCA(caPool, certPath)
|
|
||||||
name := filepath.Base(certPath)
|
|
||||||
|
|
||||||
// Try to load the leaf cert for self-signed detection and expiry fallback
|
|
||||||
var leaf *x509.Certificate
|
|
||||||
if certs, err := loadCertsFromFile(certPath); err == nil && len(certs) > 0 {
|
|
||||||
leaf = certs[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prefer the SELF-SIGNED label if applicable
|
|
||||||
if isSelfSigned(leaf) {
|
|
||||||
fmt.Printf("%s: SELF-SIGNED\n", name)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if ok {
|
|
||||||
fmt.Printf("%s: OK (expires %s)\n", name, expiryString(leaf, exp))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Printf("%s: INVALID\n", name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Special selftest mode: single argument "selftest"
|
var skipVerify, useStrict bool
|
||||||
if len(os.Args) == 2 && os.Args[1] == "selftest" {
|
|
||||||
|
lib.StrictTLSFlag(&useStrict)
|
||||||
|
flag.BoolVar(&skipVerify, "k", false, "don't verify certificates")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
tcfg, err := lib.BaselineTLSConfig(skipVerify, useStrict)
|
||||||
|
die.If(err)
|
||||||
|
|
||||||
|
args := flag.Args()
|
||||||
|
|
||||||
|
if len(args) == 1 && args[0] == "selftest" {
|
||||||
os.Exit(selftest())
|
os.Exit(selftest())
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(os.Args) < 3 {
|
if len(args) < 2 {
|
||||||
prog := filepath.Base(os.Args[0])
|
fmt.Println("No certificates to check.")
|
||||||
fmt.Fprintf(os.Stderr, "Usage:\n %s ca.pem cert1.pem cert2.pem ...\n %s selftest\n", prog, prog)
|
|
||||||
os.Exit(2)
|
|
||||||
}
|
|
||||||
|
|
||||||
caPath := os.Args[1]
|
|
||||||
caPool, err := makePoolFromFile(caPath)
|
|
||||||
if err != nil || caPool == nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "failed to load CA certificate(s): %v\n", err)
|
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, certPath := range os.Args[2:] {
|
caFile := args[0]
|
||||||
processCert(caPool, certPath)
|
args = args[1:]
|
||||||
|
|
||||||
|
caCert, err := certlib.LoadCertificates(caFile)
|
||||||
|
die.If(err)
|
||||||
|
|
||||||
|
if len(caCert) != 1 {
|
||||||
|
die.With("only one CA certificate should be presented.")
|
||||||
|
}
|
||||||
|
|
||||||
|
roots := x509.NewCertPool()
|
||||||
|
roots.AddCert(caCert[0])
|
||||||
|
|
||||||
|
for _, arg := range args {
|
||||||
|
var cert *x509.Certificate
|
||||||
|
|
||||||
|
cert, err = lib.GetCertificate(arg, tcfg)
|
||||||
|
if err != nil {
|
||||||
|
lib.Warn(err, "while parsing certificate from %s", arg)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if bytes.Equal(cert.AuthorityKeyId, caCert[0].AuthorityKeyId) {
|
||||||
|
fmt.Printf("%s: SELF-SIGNED\n", arg)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := verify.CertWith(cert, roots, nil, false); err != nil {
|
||||||
|
fmt.Printf("%s: INVALID\n", arg)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("%s: OK (expires %s)\n", arg, cert.NotAfter.Format(lib.DateShortFormat))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user