diff --git a/certlib/certlib.go b/certlib/certlib.go index 75c488c..ff4a8a0 100644 --- a/certlib/certlib.go +++ b/certlib/certlib.go @@ -4,6 +4,7 @@ import ( "crypto/x509" "encoding/pem" "errors" + "fmt" "os" "git.wntrmute.dev/kyle/goutils/certlib/certerr" @@ -93,3 +94,18 @@ func LoadCertificates(path string) ([]*x509.Certificate, error) { 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 +} diff --git a/cmd/ca-signed/main.go b/cmd/ca-signed/main.go index e2981ce..384e97c 100644 --- a/cmd/ca-signed/main.go +++ b/cmd/ca-signed/main.go @@ -1,157 +1,22 @@ package main import ( + "bytes" "crypto/x509" "embed" - "errors" + "flag" "fmt" "os" - "path/filepath" - "time" "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 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 { name 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) } - pool, err := makePoolFromBytes(caBytes) + pool, err := certlib.PoolFromBytes(caBytes) if err != nil || pool == nil { 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 { return fmt.Errorf("%s: unexpected result: got %v, want %v", tc.name, ok, tc.expectOK) } 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) @@ -237,14 +109,16 @@ func selftest() int { failures++ continue } - certs, err := loadCertsFromBytes(b) + + certs, err := certlib.ReadCertificates(b) if err != nil || len(certs) == 0 { fmt.Fprintf(os.Stderr, "selftest: failed to parse cert(s) from %s: %v\n", root, err) failures++ continue } + 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) } else { fmt.Printf("%s: expected SELF-SIGNED, but was not detected as such\n", root) @@ -260,66 +134,58 @@ func selftest() int { 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() { - // Special selftest mode: single argument "selftest" - if len(os.Args) == 2 && os.Args[1] == "selftest" { + var skipVerify, useStrict bool + + 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()) } - if len(os.Args) < 3 { - prog := filepath.Base(os.Args[0]) - 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) + if len(args) < 2 { + fmt.Println("No certificates to check.") os.Exit(1) } - for _, certPath := range os.Args[2:] { - processCert(caPool, certPath) + caFile := args[0] + 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)) + } } }