Skip to content

Commit

Permalink
Ⓜ️ Add support for folders with certificates
Browse files Browse the repository at this point in the history
  • Loading branch information
jsnjack committed Oct 24, 2023
1 parent 22fcd2f commit 388c5a0
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 88 deletions.
4 changes: 3 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
)

var rootCertPath string
var rootTargetPath string
var rootVerbose bool

// rootCmd represents the base command when called without any subcommands
Expand All @@ -30,6 +31,7 @@ func init() {
// Cobra supports persistent flags, which, if defined here,
// will be global for your application.

rootCmd.PersistentFlags().StringVarP(&rootCertPath, "cert", "c", "", "certificate file")
rootCmd.PersistentFlags().StringVarP(&rootCertPath, "cert", "c", "", "certificate file (deprecated)")
rootCmd.PersistentFlags().StringVarP(&rootTargetPath, "target", "t", "", "a file or directory with *.pem files")
rootCmd.PersistentFlags().BoolVarP(&rootVerbose, "verbose", "v", false, "verbose output")
}
93 changes: 81 additions & 12 deletions cmd/root_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,95 @@ import (
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
)

func readCert() ([]byte, error) {
var data []byte
if rootCertPath == "" {
return nil, fmt.Errorf("provide certificate file")
// SSLCertificate represents a certificate and private key pair
type SSLCertificate struct {
Certificates []*x509.Certificate
PrivateKey crypto.PrivateKey
raw []byte
filename string
}

// NewSSLCertificate creates a new SSLCertificates
func NewSSLCertificates(target string) ([]*SSLCertificate, error) {
if target == "" {
return nil, fmt.Errorf("provide target directory or file")
}
info, err := os.Stat(target)

if err != nil {
return nil, err
}
_, err := os.Stat(rootCertPath)
if err == nil {
data, err = ioutil.ReadFile(rootCertPath)

var certs []*SSLCertificate

if info.IsDir() {
entries, err := os.ReadDir(target)
if err != nil {
return nil, err
}
for _, f := range entries {
if !f.IsDir() && strings.HasSuffix(f.Name(), ".pem") {
absPath, err := filepath.Abs(target + "/" + f.Name())
if err != nil {
return nil, err
}
d, err := os.ReadFile(absPath)
if err != nil {
return nil, err
}
// Extract certificates
sslCert, err := createSSLCertificate(d, absPath)
if err != nil {
return nil, err
}
certs = append(certs, sslCert)
}
}
} else {
absPath, err := filepath.Abs(target)
if err != nil {
return nil, err
}
data, err := os.ReadFile(absPath)
if err != nil {
return nil, err
}
sslCert, err := createSSLCertificate(data, absPath)
if err != nil {
return nil, err
}
return data, nil
certs = append(certs, sslCert)
}
return certs, nil
}

func createSSLCertificate(data []byte, filename string) (*SSLCertificate, error) {
// Extract certificates
logf("> Parsing the certificate %s...\n", filename)
sslCert := &SSLCertificate{raw: data, filename: filename}
extractedCerts, err := extractCerts(data)
if err != nil {
return nil, err
}
logf(" extracted %d certificates\n", len(extractedCerts))
if len(extractedCerts) == 0 {
return nil, fmt.Errorf("unable to extract certificates")
}
sslCert.Certificates = extractedCerts
// Extract privatekey
logln(" extracting private key...")
extractedPK, err := extractPrivateKey(data)
if err != nil {
return nil, err
}
return nil, err
sslCert.PrivateKey = extractedPK
return sslCert, nil
}

func extractCerts(data []byte) ([]*x509.Certificate, error) {
Expand All @@ -50,7 +119,7 @@ func extractCerts(data []byte) ([]*x509.Certificate, error) {
format := "%+19s: %s\n"
logf(format, "found certificate", item.Subject)
logf(format, "issuer", item.Issuer)
logf(format, "expires in", fmt.Sprintf("%.0f days\n", item.NotAfter.Sub(time.Now()).Hours()/24))
logf(format, "expires in", fmt.Sprintf("%.0f days\n", time.Until(item.NotAfter).Hours()/24))

if item.NotAfter.Before(time.Now()) {
return nil, fmt.Errorf("the certificate has expired on %v", item.NotAfter)
Expand Down Expand Up @@ -80,7 +149,7 @@ func extractPrivateKey(data []byte) (crypto.PrivateKey, error) {
}
return item, nil
}
return nil, fmt.Errorf("Private key not found")
return nil, fmt.Errorf("private key not found")
}

// Attempt to parse the given private key DER block. OpenSSL 0.9.8 generates
Expand Down
44 changes: 17 additions & 27 deletions cmd/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"fmt"
"log"
"net/http"
"strings"

"github.com/spf13/cobra"
)
Expand All @@ -18,43 +17,34 @@ var serveCmd = &cobra.Command{
Short: "Start webserver on provided port",
RunE: func(cmd *cobra.Command, args []string) error {
cmd.SilenceErrors = true
data, err := readCert()
if err != nil {
return err
if rootCertPath == "" && rootTargetPath == "" {
return fmt.Errorf("provide certificate file or directory")
}
cmd.SilenceUsage = true

// Parse and extract certificates
logf("> Parsing the certificate %s...\n", rootCertPath)

certs, err := extractCerts(data)
if err != nil {
return err
}
logf(" extracted %d certificates\n", len(certs))
if len(certs) == 0 {
return fmt.Errorf("unable to extract certificates")
target := rootTargetPath
if target == "" {
target = rootCertPath
}

logln("> Extracting private key...")
privKey, err := extractPrivateKey(data)
certs, err := NewSSLCertificates(target)
if err != nil {
return err
}
logln(" ok")

fmt.Println("Starting webserver...")
dnsName := strings.Replace(certs[0].DNSNames[0], "*.", "", 1)
fmt.Printf(" example: curl --resolve *:%d:127.0.0.1 https://%s:%d -v\n", servePort, dnsName, servePort)
fmt.Printf(" example: curl --resolve *:%d:127.0.0.1 https://example.com:%d -v\n", servePort, servePort)

var cert tls.Certificate
// Convert to tls.Certificate
tlsCerts := []tls.Certificate{}
for _, item := range certs {
cert.Certificate = append(cert.Certificate, item.Raw)
certToServe := tls.Certificate{}
certToServe.PrivateKey = item.PrivateKey
for _, cert := range item.Certificates {
certToServe.Certificate = append(certToServe.Certificate, cert.Raw)
}
tlsCerts = append(tlsCerts, certToServe)
}
cert.PrivateKey = privKey

http.HandleFunc("/", serveHandle)
cfg := &tls.Config{Certificates: []tls.Certificate{cert}}
cfg := &tls.Config{Certificates: tlsCerts}
server := &http.Server{
Addr: fmt.Sprintf(":%d", servePort),
TLSConfig: cfg,
Expand All @@ -75,7 +65,7 @@ func init() {

// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
serveCmd.Flags().IntVarP(&servePort, "port", "p", 8443, "Port to start webserver on")
serveCmd.Flags().IntVarP(&servePort, "port", "p", 8443, "port to start webserver on")
}

func serveHandle(w http.ResponseWriter, req *http.Request) {
Expand Down
104 changes: 56 additions & 48 deletions cmd/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,64 +23,73 @@ var verifyCmd = &cobra.Command{
Short: "Verify SSL certificate",
RunE: func(cmd *cobra.Command, args []string) error {
cmd.SilenceErrors = true
data, err := readCert()
if err != nil {
return err
if rootCertPath == "" && rootTargetPath == "" {
return fmt.Errorf("provide certificate file or directory")
}
cmd.SilenceUsage = true

if !rootVerbose {
fmt.Printf("> Verifying %s...\n", rootCertPath)
target := rootTargetPath
if target == "" {
target = rootCertPath
}

// Parse and extract certificates
logf("> Parsing the certificate %s...\n", rootCertPath)

certs, err := extractCerts(data)
certs, err := NewSSLCertificates(target)
if err != nil {
return err
}
logf(" extracted %d certificates\n", len(certs))
if len(certs) == 0 {
return fmt.Errorf("unable to extract certificates")
}

logln("> Extracting private key...")
privKey, err := extractPrivateKey(data)
if err != nil {
return err
if len(certs) > 1 && verifyDomainname == "" {
return fmt.Errorf("found multiple certificates; provide domain name")
}
logln(" ok")

logln("> Verifying certificates order...")
err = verifyOrder(certs)
if err != nil {
return err
}
logln(" ok")
result := make(map[*SSLCertificate]bool)

logln("> Verifying private key...")
err = verifyPrivateKey(certs[0].PublicKey, privKey)
if err != nil {
return err
}
logln(" ok")
for _, item := range certs {
logf("> Verifying certificate %s...\n", item.filename)
result[item] = true

logln("> Verifying certificates order...")
err = verifyOrder(item.Certificates)
if err != nil {
logln(err.Error())
result[item] = false
} else {
logln(" ok")
}

logln("> Verifying certificate and chain of trust...")
if verifyDomainname == "" {
logln(" WARNING: domain name is empty, extracting domain name from the certificate name")
verifyDomainname = strings.TrimSuffix(filepath.Base(rootCertPath), ".pem")
logf(" Domain name: %q\n", verifyDomainname)
logln("> Verifying private key...")
err = verifyPrivateKey(item.Certificates[0].PublicKey, item.PrivateKey)
if err != nil {
logln(err.Error())
result[item] = false
} else {
logln(" ok")
}

logln("> Verifying certificate and chain of trust...")
if verifyDomainname == "" {
logln(" WARNING: domain name is empty, extracting domain name from the certificate name")
verifyDomainname = strings.TrimSuffix(filepath.Base(item.filename), ".pem")
logf(" Domain name: %q\n", verifyDomainname)
}
err = verifyCertificate(item.Certificates, verifyDomainname)
if err != nil {
logln(err.Error())
result[item] = false
} else {
logln(" ok")
}
}
err = verifyCertificate(certs, verifyDomainname)
if err != nil {
return err

isValid := false
for item, valid := range result {
if valid {
fmt.Printf("> Certificate %s is valid\n", item.filename)
isValid = true
break
}
}
logln(" ok")

logf("> Certificate %s: ok\n", rootCertPath)
if !rootVerbose {
fmt.Println(" ok")
if !isValid {
return fmt.Errorf("no valid certificates found")
}
return nil
},
Expand All @@ -97,8 +106,8 @@ func init() {

// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
verifyCmd.Flags().StringVarP(&verifyDomainname, "domain", "d", "", "Domain name to use to verify the certificate (default: extracted from the file name)")
verifyCmd.Flags().BoolVarP(&verifySkipWildcard, "wildcard", "w", false, "Do not verify is the certificate is a wildcard certificate")
verifyCmd.Flags().StringVarP(&verifyDomainname, "domain", "d", "", "domain name to use to verify the certificate (default: extracted from the file name)")
verifyCmd.Flags().BoolVarP(&verifySkipWildcard, "no-wildcard", "w", false, "do not require wildcard certificate")
}

// Verifies certificates order
Expand All @@ -109,7 +118,6 @@ func verifyOrder(certs []*x509.Certificate) error {
if item.IsCA {
return fmt.Errorf("cert 0 should not be a CA certificate")
}
break
default:
if !item.IsCA {
return fmt.Errorf("cert %d should be a CA certificate (intermediate or root)", idx)
Expand Down Expand Up @@ -206,4 +214,4 @@ func _verifyCertificate(cert *x509.Certificate, opts x509.VerifyOptions) error {
logln(" skipping wildcard verification")
}
return nil
}
}

0 comments on commit 388c5a0

Please sign in to comment.