Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d32a64dc0 | |||
| d70ca5ee87 | |||
| eca3a229a4 | |||
| 4c1eb03671 |
87
.golangci.yml
Normal file
87
.golangci.yml
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
run:
|
||||||
|
timeout: 5m
|
||||||
|
tests: true
|
||||||
|
build-tags: []
|
||||||
|
modules-download-mode: readonly
|
||||||
|
|
||||||
|
linters:
|
||||||
|
enable:
|
||||||
|
- errcheck
|
||||||
|
- gosimple
|
||||||
|
- govet
|
||||||
|
- ineffassign
|
||||||
|
- staticcheck
|
||||||
|
- unused
|
||||||
|
- gofmt
|
||||||
|
- goimports
|
||||||
|
- misspell
|
||||||
|
- unparam
|
||||||
|
- unconvert
|
||||||
|
- goconst
|
||||||
|
- gocyclo
|
||||||
|
- gosec
|
||||||
|
- prealloc
|
||||||
|
- copyloopvar
|
||||||
|
- revive
|
||||||
|
- typecheck
|
||||||
|
|
||||||
|
linters-settings:
|
||||||
|
gocyclo:
|
||||||
|
min-complexity: 15
|
||||||
|
|
||||||
|
goconst:
|
||||||
|
min-len: 3
|
||||||
|
min-occurrences: 3
|
||||||
|
|
||||||
|
misspell:
|
||||||
|
locale: US
|
||||||
|
|
||||||
|
revive:
|
||||||
|
rules:
|
||||||
|
- name: exported
|
||||||
|
disabled: false
|
||||||
|
- name: error-return
|
||||||
|
- name: error-naming
|
||||||
|
- name: if-return
|
||||||
|
- name: var-naming
|
||||||
|
- name: package-comments
|
||||||
|
disabled: true
|
||||||
|
- name: indent-error-flow
|
||||||
|
- name: context-as-argument
|
||||||
|
|
||||||
|
gosec:
|
||||||
|
excludes:
|
||||||
|
- G304 # File path from variable (common in file utilities)
|
||||||
|
- G404 # Use of weak random (acceptable for non-crypto use)
|
||||||
|
|
||||||
|
issues:
|
||||||
|
exclude-rules:
|
||||||
|
# Exclude some linters from running on tests files
|
||||||
|
- path: _test\.go
|
||||||
|
linters:
|
||||||
|
- gocyclo
|
||||||
|
- errcheck
|
||||||
|
- gosec
|
||||||
|
|
||||||
|
# Exclude embedded content from checks
|
||||||
|
- path: ".*\\.txt$"
|
||||||
|
linters:
|
||||||
|
- all
|
||||||
|
|
||||||
|
# Ignore deprecation warnings in legacy code if needed
|
||||||
|
- linters:
|
||||||
|
- staticcheck
|
||||||
|
text: "SA1019"
|
||||||
|
|
||||||
|
# Maximum issues count per one linter
|
||||||
|
max-issues-per-linter: 0
|
||||||
|
|
||||||
|
# Maximum count of issues with the same text
|
||||||
|
max-same-issues: 0
|
||||||
|
|
||||||
|
output:
|
||||||
|
formats:
|
||||||
|
- format: colored-line-number
|
||||||
|
path: stdout
|
||||||
|
print-issued-lines: true
|
||||||
|
print-linter-name: true
|
||||||
@@ -101,7 +101,12 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Process each chain group
|
// Process each chain group
|
||||||
createdFiles := []string{}
|
// Pre-allocate createdFiles based on total number of formats across all groups
|
||||||
|
totalFormats := 0
|
||||||
|
for _, group := range cfg.Chains {
|
||||||
|
totalFormats += len(group.Outputs.Formats)
|
||||||
|
}
|
||||||
|
createdFiles := make([]string, 0, totalFormats)
|
||||||
for groupName, group := range cfg.Chains {
|
for groupName, group := range cfg.Chains {
|
||||||
files, err := processChainGroup(groupName, group, expiryDuration)
|
files, err := processChainGroup(groupName, group, expiryDuration)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -168,68 +173,79 @@ func parseDuration(s string) (time.Duration, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func processChainGroup(groupName string, group ChainGroup, expiryDuration time.Duration) ([]string, error) {
|
func processChainGroup(groupName string, group ChainGroup, expiryDuration time.Duration) ([]string, error) {
|
||||||
var createdFiles []string
|
|
||||||
|
|
||||||
// Default encoding to "pem" if not specified
|
// Default encoding to "pem" if not specified
|
||||||
encoding := group.Outputs.Encoding
|
encoding := group.Outputs.Encoding
|
||||||
if encoding == "" {
|
if encoding == "" {
|
||||||
encoding = "pem"
|
encoding = "pem"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect data from all chains in the group
|
// Collect certificates from all chains in the group
|
||||||
|
singleFileCerts, individualCerts, err := loadAndCollectCerts(group.Certs, group.Outputs, expiryDuration)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare files for inclusion in archives
|
||||||
|
archiveFiles, err := prepareArchiveFiles(singleFileCerts, individualCerts, group.Outputs, encoding)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create archives for the entire group
|
||||||
|
createdFiles, err := createArchiveFiles(groupName, group.Outputs.Formats, archiveFiles)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return createdFiles, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadAndCollectCerts loads all certificates from chains and collects them for processing
|
||||||
|
func loadAndCollectCerts(chains []CertChain, outputs Outputs, expiryDuration time.Duration) ([]*x509.Certificate, []certWithPath, error) {
|
||||||
var singleFileCerts []*x509.Certificate
|
var singleFileCerts []*x509.Certificate
|
||||||
var individualCerts []certWithPath
|
var individualCerts []certWithPath
|
||||||
|
|
||||||
for _, chain := range group.Certs {
|
for _, chain := range chains {
|
||||||
// Step 1: Load all certificates for this chain
|
|
||||||
allCerts := make(map[string]*x509.Certificate)
|
|
||||||
|
|
||||||
// Load root certificate
|
// Load root certificate
|
||||||
rootCert, err := certlib.LoadCertificate(chain.Root)
|
rootCert, err := certlib.LoadCertificate(chain.Root)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to load root certificate %s: %v", chain.Root, err)
|
return nil, nil, fmt.Errorf("failed to load root certificate %s: %v", chain.Root, err)
|
||||||
}
|
}
|
||||||
allCerts[chain.Root] = rootCert
|
|
||||||
|
|
||||||
// Check expiry for root
|
// Check expiry for root
|
||||||
checkExpiry(chain.Root, rootCert, expiryDuration)
|
checkExpiry(chain.Root, rootCert, expiryDuration)
|
||||||
|
|
||||||
// Add root to single file if needed
|
// Add root to collections if needed
|
||||||
if group.Outputs.IncludeSingle {
|
if outputs.IncludeSingle {
|
||||||
singleFileCerts = append(singleFileCerts, rootCert)
|
singleFileCerts = append(singleFileCerts, rootCert)
|
||||||
}
|
}
|
||||||
|
if outputs.IncludeIndividual {
|
||||||
// Add root to individual files if needed
|
|
||||||
if group.Outputs.IncludeIndividual {
|
|
||||||
individualCerts = append(individualCerts, certWithPath{
|
individualCerts = append(individualCerts, certWithPath{
|
||||||
cert: rootCert,
|
cert: rootCert,
|
||||||
path: chain.Root,
|
path: chain.Root,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Load and validate intermediates
|
// Load and validate intermediates
|
||||||
for _, intPath := range chain.Intermediates {
|
for _, intPath := range chain.Intermediates {
|
||||||
intCert, err := certlib.LoadCertificate(intPath)
|
intCert, err := certlib.LoadCertificate(intPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to load intermediate certificate %s: %v", intPath, err)
|
return nil, nil, fmt.Errorf("failed to load intermediate certificate %s: %v", intPath, err)
|
||||||
}
|
}
|
||||||
allCerts[intPath] = intCert
|
|
||||||
|
|
||||||
// Validate that intermediate is signed by root
|
// Validate that intermediate is signed by root
|
||||||
if err := intCert.CheckSignatureFrom(rootCert); err != nil {
|
if err := intCert.CheckSignatureFrom(rootCert); err != nil {
|
||||||
return nil, fmt.Errorf("intermediate %s is not properly signed by root %s: %v", intPath, chain.Root, err)
|
return nil, nil, fmt.Errorf("intermediate %s is not properly signed by root %s: %v", intPath, chain.Root, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check expiry for intermediate
|
// Check expiry for intermediate
|
||||||
checkExpiry(intPath, intCert, expiryDuration)
|
checkExpiry(intPath, intCert, expiryDuration)
|
||||||
|
|
||||||
// Add intermediate to a single file if needed
|
// Add intermediate to collections if needed
|
||||||
if group.Outputs.IncludeSingle {
|
if outputs.IncludeSingle {
|
||||||
singleFileCerts = append(singleFileCerts, intCert)
|
singleFileCerts = append(singleFileCerts, intCert)
|
||||||
}
|
}
|
||||||
|
if outputs.IncludeIndividual {
|
||||||
// Add intermediate to individual files if needed
|
|
||||||
if group.Outputs.IncludeIndividual {
|
|
||||||
individualCerts = append(individualCerts, certWithPath{
|
individualCerts = append(individualCerts, certWithPath{
|
||||||
cert: intCert,
|
cert: intCert,
|
||||||
path: intPath,
|
path: intPath,
|
||||||
@@ -238,11 +254,15 @@ func processChainGroup(groupName string, group ChainGroup, expiryDuration time.D
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare files for inclusion in archives for the entire group.
|
return singleFileCerts, individualCerts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepareArchiveFiles prepares all files to be included in archives
|
||||||
|
func prepareArchiveFiles(singleFileCerts []*x509.Certificate, individualCerts []certWithPath, outputs Outputs, encoding string) ([]fileEntry, error) {
|
||||||
var archiveFiles []fileEntry
|
var archiveFiles []fileEntry
|
||||||
|
|
||||||
// Handle a single bundle file.
|
// Handle a single bundle file
|
||||||
if group.Outputs.IncludeSingle && len(singleFileCerts) > 0 {
|
if outputs.IncludeSingle && len(singleFileCerts) > 0 {
|
||||||
files, err := encodeCertsToFiles(singleFileCerts, "bundle", encoding, true)
|
files, err := encodeCertsToFiles(singleFileCerts, "bundle", encoding, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to encode single bundle: %v", err)
|
return nil, fmt.Errorf("failed to encode single bundle: %v", err)
|
||||||
@@ -251,7 +271,7 @@ func processChainGroup(groupName string, group ChainGroup, expiryDuration time.D
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle individual files
|
// Handle individual files
|
||||||
if group.Outputs.IncludeIndividual {
|
if outputs.IncludeIndividual {
|
||||||
for _, cp := range individualCerts {
|
for _, cp := range individualCerts {
|
||||||
baseName := strings.TrimSuffix(filepath.Base(cp.path), filepath.Ext(cp.path))
|
baseName := strings.TrimSuffix(filepath.Base(cp.path), filepath.Ext(cp.path))
|
||||||
files, err := encodeCertsToFiles([]*x509.Certificate{cp.cert}, baseName, encoding, false)
|
files, err := encodeCertsToFiles([]*x509.Certificate{cp.cert}, baseName, encoding, false)
|
||||||
@@ -263,7 +283,7 @@ func processChainGroup(groupName string, group ChainGroup, expiryDuration time.D
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Generate manifest if requested
|
// Generate manifest if requested
|
||||||
if group.Outputs.Manifest {
|
if outputs.Manifest {
|
||||||
manifestContent := generateManifest(archiveFiles)
|
manifestContent := generateManifest(archiveFiles)
|
||||||
archiveFiles = append(archiveFiles, fileEntry{
|
archiveFiles = append(archiveFiles, fileEntry{
|
||||||
name: "MANIFEST",
|
name: "MANIFEST",
|
||||||
@@ -271,8 +291,14 @@ func processChainGroup(groupName string, group ChainGroup, expiryDuration time.D
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create archives for the entire group
|
return archiveFiles, nil
|
||||||
for _, format := range group.Outputs.Formats {
|
}
|
||||||
|
|
||||||
|
// createArchiveFiles creates archive files in the specified formats
|
||||||
|
func createArchiveFiles(groupName string, formats []string, archiveFiles []fileEntry) ([]string, error) {
|
||||||
|
createdFiles := make([]string, 0, len(formats))
|
||||||
|
|
||||||
|
for _, format := range formats {
|
||||||
ext, ok := formatExtensions[format]
|
ext, ok := formatExtensions[format]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("unsupported format: %s", format)
|
return nil, fmt.Errorf("unsupported format: %s", format)
|
||||||
@@ -327,10 +353,7 @@ func encodeCertsToFiles(certs []*x509.Certificate, baseName string, encoding str
|
|||||||
|
|
||||||
switch encoding {
|
switch encoding {
|
||||||
case "pem":
|
case "pem":
|
||||||
pemContent, err := encodeCertsToPEM(certs, isSingle)
|
pemContent := encodeCertsToPEM(certs)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
files = append(files, fileEntry{
|
files = append(files, fileEntry{
|
||||||
name: baseName + ".pem",
|
name: baseName + ".pem",
|
||||||
content: pemContent,
|
content: pemContent,
|
||||||
@@ -357,10 +380,7 @@ func encodeCertsToFiles(certs []*x509.Certificate, baseName string, encoding str
|
|||||||
}
|
}
|
||||||
case "both":
|
case "both":
|
||||||
// Add PEM version
|
// Add PEM version
|
||||||
pemContent, err := encodeCertsToPEM(certs, isSingle)
|
pemContent := encodeCertsToPEM(certs)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
files = append(files, fileEntry{
|
files = append(files, fileEntry{
|
||||||
name: baseName + ".pem",
|
name: baseName + ".pem",
|
||||||
content: pemContent,
|
content: pemContent,
|
||||||
@@ -391,7 +411,7 @@ func encodeCertsToFiles(certs []*x509.Certificate, baseName string, encoding str
|
|||||||
}
|
}
|
||||||
|
|
||||||
// encodeCertsToPEM encodes certificates to PEM format
|
// encodeCertsToPEM encodes certificates to PEM format
|
||||||
func encodeCertsToPEM(certs []*x509.Certificate, concatenate bool) ([]byte, error) {
|
func encodeCertsToPEM(certs []*x509.Certificate) []byte {
|
||||||
var pemContent []byte
|
var pemContent []byte
|
||||||
for _, cert := range certs {
|
for _, cert := range certs {
|
||||||
pemBlock := &pem.Block{
|
pemBlock := &pem.Block{
|
||||||
@@ -400,7 +420,7 @@ func encodeCertsToPEM(certs []*x509.Certificate, concatenate bool) ([]byte, erro
|
|||||||
}
|
}
|
||||||
pemContent = append(pemContent, pem.EncodeToMemory(pemBlock)...)
|
pemContent = append(pemContent, pem.EncodeToMemory(pemBlock)...)
|
||||||
}
|
}
|
||||||
return pemContent, nil
|
return pemContent
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateManifest(files []fileEntry) []byte {
|
func generateManifest(files []fileEntry) []byte {
|
||||||
@@ -420,22 +440,29 @@ func createZipArchive(path string, files []fileEntry) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
w := zip.NewWriter(f)
|
w := zip.NewWriter(f)
|
||||||
defer w.Close()
|
|
||||||
|
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
fw, err := w.Create(file.name)
|
fw, err := w.Create(file.name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
w.Close()
|
||||||
|
f.Close()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, err := fw.Write(file.content); err != nil {
|
if _, err := fw.Write(file.content); err != nil {
|
||||||
|
w.Close()
|
||||||
|
f.Close()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
// Check errors on close operations
|
||||||
|
if err := w.Close(); err != nil {
|
||||||
|
f.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return f.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func createTarGzArchive(path string, files []fileEntry) error {
|
func createTarGzArchive(path string, files []fileEntry) error {
|
||||||
@@ -443,13 +470,9 @@ func createTarGzArchive(path string, files []fileEntry) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
gw := gzip.NewWriter(f)
|
gw := gzip.NewWriter(f)
|
||||||
defer gw.Close()
|
|
||||||
|
|
||||||
tw := tar.NewWriter(gw)
|
tw := tar.NewWriter(gw)
|
||||||
defer tw.Close()
|
|
||||||
|
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
hdr := &tar.Header{
|
hdr := &tar.Header{
|
||||||
@@ -458,14 +481,30 @@ func createTarGzArchive(path string, files []fileEntry) error {
|
|||||||
Size: int64(len(file.content)),
|
Size: int64(len(file.content)),
|
||||||
}
|
}
|
||||||
if err := tw.WriteHeader(hdr); err != nil {
|
if err := tw.WriteHeader(hdr); err != nil {
|
||||||
|
tw.Close()
|
||||||
|
gw.Close()
|
||||||
|
f.Close()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, err := tw.Write(file.content); err != nil {
|
if _, err := tw.Write(file.content); err != nil {
|
||||||
|
tw.Close()
|
||||||
|
gw.Close()
|
||||||
|
f.Close()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
// Check errors on close operations in the correct order
|
||||||
|
if err := tw.Close(); err != nil {
|
||||||
|
gw.Close()
|
||||||
|
f.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := gw.Close(); err != nil {
|
||||||
|
f.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return f.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateHashFile(path string, files []string) error {
|
func generateHashFile(path string, files []string) error {
|
||||||
|
|||||||
@@ -186,4 +186,9 @@ to provide the same detailed information.
|
|||||||
|
|
||||||
It may be easier to embed the README.txt in the program on build.
|
It may be easier to embed the README.txt in the program on build.
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
|
For the archive (tar.gz and zip) writers, make sure errors are
|
||||||
|
checked at the end, and don't just defer the close operations.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
@@ -64,11 +64,7 @@ func LoadFile(path string) error {
|
|||||||
addLine(line)
|
addLine(line)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = scanner.Err(); err != nil {
|
return scanner.Err()
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadFileFor scans the ini file at path, loading the default section
|
// LoadFileFor scans the ini file at path, loading the default section
|
||||||
|
|||||||
@@ -42,69 +42,96 @@ func ParseReader(r io.Reader) (cfg ConfigMap, err error) {
|
|||||||
line string
|
line string
|
||||||
longLine bool
|
longLine bool
|
||||||
currentSection string
|
currentSection string
|
||||||
lineBytes []byte
|
|
||||||
isPrefix bool
|
|
||||||
)
|
)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
err = nil
|
line, longLine, err = readConfigLine(buf, line, longLine)
|
||||||
lineBytes, isPrefix, err = buf.ReadLine()
|
if err == io.EOF {
|
||||||
if io.EOF == err {
|
|
||||||
err = nil
|
err = nil
|
||||||
break
|
break
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
break
|
break
|
||||||
} else if isPrefix {
|
|
||||||
line += string(lineBytes)
|
|
||||||
|
|
||||||
longLine = true
|
|
||||||
continue
|
|
||||||
} else if longLine {
|
|
||||||
line += string(lineBytes)
|
|
||||||
longLine = false
|
|
||||||
} else {
|
|
||||||
line = string(lineBytes)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if commentLine.MatchString(line) {
|
if line == "" {
|
||||||
continue
|
continue
|
||||||
} else if blankLine.MatchString(line) {
|
}
|
||||||
continue
|
|
||||||
} else if configSection.MatchString(line) {
|
currentSection, err = processConfigLine(cfg, line, currentSection)
|
||||||
section := configSection.ReplaceAllString(line,
|
if err != nil {
|
||||||
"$1")
|
|
||||||
if section == "" {
|
|
||||||
err = fmt.Errorf("invalid structure in file")
|
|
||||||
break
|
|
||||||
} else if !cfg.SectionInConfig(section) {
|
|
||||||
cfg[section] = make(map[string]string, 0)
|
|
||||||
}
|
|
||||||
currentSection = section
|
|
||||||
} else if configLine.MatchString(line) {
|
|
||||||
regex := configLine
|
|
||||||
if quotedConfigLine.MatchString(line) {
|
|
||||||
regex = quotedConfigLine
|
|
||||||
}
|
|
||||||
if currentSection == "" {
|
|
||||||
currentSection = DefaultSection
|
|
||||||
if !cfg.SectionInConfig(currentSection) {
|
|
||||||
cfg[currentSection] = map[string]string{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
key := regex.ReplaceAllString(line, "$1")
|
|
||||||
val := regex.ReplaceAllString(line, "$2")
|
|
||||||
if key == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
cfg[currentSection][key] = val
|
|
||||||
} else {
|
|
||||||
err = fmt.Errorf("invalid config file")
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// readConfigLine reads and assembles a complete configuration line, handling long lines.
|
||||||
|
func readConfigLine(buf *bufio.Reader, currentLine string, longLine bool) (line string, stillLong bool, err error) {
|
||||||
|
lineBytes, isPrefix, err := buf.ReadLine()
|
||||||
|
if err != nil {
|
||||||
|
return "", false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if isPrefix {
|
||||||
|
return currentLine + string(lineBytes), true, nil
|
||||||
|
} else if longLine {
|
||||||
|
return currentLine + string(lineBytes), false, nil
|
||||||
|
}
|
||||||
|
return string(lineBytes), false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// processConfigLine processes a single line and updates the configuration map.
|
||||||
|
func processConfigLine(cfg ConfigMap, line string, currentSection string) (string, error) {
|
||||||
|
if commentLine.MatchString(line) || blankLine.MatchString(line) {
|
||||||
|
return currentSection, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if configSection.MatchString(line) {
|
||||||
|
return handleSectionLine(cfg, line)
|
||||||
|
}
|
||||||
|
|
||||||
|
if configLine.MatchString(line) {
|
||||||
|
return handleConfigLine(cfg, line, currentSection)
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentSection, fmt.Errorf("invalid config file")
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleSectionLine processes a section header line.
|
||||||
|
func handleSectionLine(cfg ConfigMap, line string) (string, error) {
|
||||||
|
section := configSection.ReplaceAllString(line, "$1")
|
||||||
|
if section == "" {
|
||||||
|
return "", fmt.Errorf("invalid structure in file")
|
||||||
|
}
|
||||||
|
if !cfg.SectionInConfig(section) {
|
||||||
|
cfg[section] = make(map[string]string, 0)
|
||||||
|
}
|
||||||
|
return section, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleConfigLine processes a key=value configuration line.
|
||||||
|
func handleConfigLine(cfg ConfigMap, line string, currentSection string) (string, error) {
|
||||||
|
regex := configLine
|
||||||
|
if quotedConfigLine.MatchString(line) {
|
||||||
|
regex = quotedConfigLine
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentSection == "" {
|
||||||
|
currentSection = DefaultSection
|
||||||
|
if !cfg.SectionInConfig(currentSection) {
|
||||||
|
cfg[currentSection] = map[string]string{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
key := regex.ReplaceAllString(line, "$1")
|
||||||
|
val := regex.ReplaceAllString(line, "$2")
|
||||||
|
if key != "" {
|
||||||
|
cfg[currentSection][key] = val
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentSection, nil
|
||||||
|
}
|
||||||
|
|
||||||
// SectionInConfig determines whether a section is in the configuration.
|
// SectionInConfig determines whether a section is in the configuration.
|
||||||
func (c ConfigMap) SectionInConfig(section string) bool {
|
func (c ConfigMap) SectionInConfig(section string) bool {
|
||||||
_, ok := c[section]
|
_, ok := c[section]
|
||||||
|
|||||||
Reference in New Issue
Block a user