Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d32a64dc0 | |||
| d70ca5ee87 | |||
| eca3a229a4 | |||
| 4c1eb03671 | |||
| f463eeed88 | |||
| 289c9d2343 | |||
| e375963243 | |||
| 31baa10b3b | |||
| 0556c7c56d | |||
| 83c95d9db8 | |||
| beccb551e2 | |||
| c761d98b82 | |||
| e68d22337b | |||
| 4cb6f5b6f0 | |||
| 6d5708800f | |||
| fa3eb821e6 | |||
| dd5ed403b9 | |||
| b4fde22c31 |
@@ -2,22 +2,6 @@
|
|||||||
# See: https://circleci.com/docs/2.0/configuration-reference
|
# See: https://circleci.com/docs/2.0/configuration-reference
|
||||||
version: 2.1
|
version: 2.1
|
||||||
|
|
||||||
commands:
|
|
||||||
setup-bazel:
|
|
||||||
description: |
|
|
||||||
Setup the Bazel build system used for building the repo
|
|
||||||
steps:
|
|
||||||
- run:
|
|
||||||
name: Add Bazel Apt repository
|
|
||||||
command: |
|
|
||||||
sudo apt install curl gnupg
|
|
||||||
curl -fsSL https://bazel.build/bazel-release.pub.gpg | gpg --dearmor > bazel.gpg
|
|
||||||
sudo mv bazel.gpg /etc/apt/trusted.gpg.d/
|
|
||||||
echo "deb [arch=amd64] https://storage.googleapis.com/bazel-apt stable jdk1.8" | sudo tee /etc/apt/sources.list.d/bazel.list
|
|
||||||
- run:
|
|
||||||
name: Install Bazel from Apt
|
|
||||||
command: sudo apt update && sudo apt install bazel
|
|
||||||
|
|
||||||
# Define a job to be invoked later in a workflow.
|
# Define a job to be invoked later in a workflow.
|
||||||
# See: https://circleci.com/docs/2.0/configuration-reference/#jobs
|
# See: https://circleci.com/docs/2.0/configuration-reference/#jobs
|
||||||
jobs:
|
jobs:
|
||||||
@@ -26,12 +10,11 @@ jobs:
|
|||||||
# Specify the execution environment. You can specify an image from Dockerhub or use one of our Convenience Images from CircleCI's Developer Hub.
|
# Specify the execution environment. You can specify an image from Dockerhub or use one of our Convenience Images from CircleCI's Developer Hub.
|
||||||
# See: https://circleci.com/docs/2.0/configuration-reference/#docker-machine-macos-windows-executor
|
# See: https://circleci.com/docs/2.0/configuration-reference/#docker-machine-macos-windows-executor
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/golang:1.15.8
|
- image: cimg/go:1.22.2
|
||||||
# Add steps to the job
|
# Add steps to the job
|
||||||
# See: https://circleci.com/docs/2.0/configuration-reference/#steps
|
# See: https://circleci.com/docs/2.0/configuration-reference/#steps
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- setup-bazel
|
|
||||||
- restore_cache:
|
- restore_cache:
|
||||||
keys:
|
keys:
|
||||||
- go-mod-v4-{{ checksum "go.sum" }}
|
- go-mod-v4-{{ checksum "go.sum" }}
|
||||||
@@ -44,10 +27,10 @@ jobs:
|
|||||||
- "/go/pkg/mod"
|
- "/go/pkg/mod"
|
||||||
- run:
|
- run:
|
||||||
name: Run tests
|
name: Run tests
|
||||||
command: bazel test //...
|
command: go test ./...
|
||||||
- run:
|
- run:
|
||||||
name: Run build
|
name: Run build
|
||||||
command: bazel build //...
|
command: go build ./...
|
||||||
- store_test_results:
|
- store_test_results:
|
||||||
path: /tmp/test-reports
|
path: /tmp/test-reports
|
||||||
|
|
||||||
|
|||||||
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
|
||||||
82
certlib/hosts/hosts.go
Normal file
82
certlib/hosts/hosts.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package hosts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Target struct {
|
||||||
|
Host string
|
||||||
|
Port int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Target) String() string {
|
||||||
|
return fmt.Sprintf("%s:%d", t.Host, t.Port)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseURL(host string) (string, int, error) {
|
||||||
|
url, err := url.Parse(host)
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, fmt.Errorf("certlib/hosts: invalid host: %s", host)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.ToLower(url.Scheme) != "https" {
|
||||||
|
return "", 0, errors.New("certlib/hosts: only https scheme supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
if url.Port() == "" {
|
||||||
|
return url.Hostname(), 443, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
port, err := strconv.ParseInt(url.Port(), 10, 16)
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, fmt.Errorf("certlib/hosts: invalid port: %s", url.Port())
|
||||||
|
}
|
||||||
|
|
||||||
|
return url.Hostname(), int(port), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseHostPort(host string) (string, int, error) {
|
||||||
|
host, sport, err := net.SplitHostPort(host)
|
||||||
|
if err == nil {
|
||||||
|
port, err := strconv.ParseInt(sport, 10, 16)
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, fmt.Errorf("certlib/hosts: invalid port: %s", sport)
|
||||||
|
}
|
||||||
|
|
||||||
|
return host, int(port), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return host, 443, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseHost(host string) (*Target, error) {
|
||||||
|
host, port, err := parseURL(host)
|
||||||
|
if err == nil {
|
||||||
|
return &Target{Host: host, Port: port}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
host, port, err = parseHostPort(host)
|
||||||
|
if err == nil {
|
||||||
|
return &Target{Host: host, Port: port}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("certlib/hosts: invalid host: %s", host)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseHosts(hosts ...string) ([]*Target, error) {
|
||||||
|
targets := make([]*Target, 0, len(hosts))
|
||||||
|
for _, host := range hosts {
|
||||||
|
target, err := ParseHost(host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
targets = append(targets, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
return targets, nil
|
||||||
|
}
|
||||||
40
cmd/ca-signed/README.txt
Normal file
40
cmd/ca-signed/README.txt
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
ca-signed: verify certificates against a CA
|
||||||
|
-------------------------------------------
|
||||||
|
|
||||||
|
Description
|
||||||
|
ca-signed verifies whether one or more certificates are signed by a given
|
||||||
|
Certificate Authority (CA). It prints a concise status per input certificate
|
||||||
|
along with the certificate’s expiration date when validation succeeds.
|
||||||
|
|
||||||
|
Usage
|
||||||
|
ca-signed CA.pem cert1.pem [cert2.pem ...]
|
||||||
|
|
||||||
|
- CA.pem: A file containing one or more CA certificates in PEM, DER, or PKCS#7/PKCS#12 formats.
|
||||||
|
- certN.pem: A file containing the end-entity (leaf) certificate to verify. If the file contains a chain,
|
||||||
|
the first certificate is treated as the leaf and the remaining ones are used as intermediates.
|
||||||
|
|
||||||
|
Output format
|
||||||
|
For each input certificate file, one line is printed:
|
||||||
|
<filename>: OK (expires YYYY-MM-DD)
|
||||||
|
<filename>: INVALID
|
||||||
|
|
||||||
|
Special self-test mode
|
||||||
|
ca-signed selftest
|
||||||
|
|
||||||
|
Runs a built-in test suite using embedded certificates. This mode requires no
|
||||||
|
external files or network access. The program exits with code 0 if all tests
|
||||||
|
pass, or a non-zero exit code if any test fails. Example output lines include
|
||||||
|
whether validation succeeds and the leaf’s expiration when applicable.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
# Verify a server certificate against a root CA
|
||||||
|
ca-signed isrg-root-x1.pem le-e7.pem
|
||||||
|
|
||||||
|
# Run the embedded self-test suite
|
||||||
|
ca-signed selftest
|
||||||
|
|
||||||
|
Notes
|
||||||
|
- The tool attempts to parse certificates in PEM first, then falls back to
|
||||||
|
DER/PKCS#7/PKCS#12 (with an empty password) where applicable.
|
||||||
|
- Expiration is shown for the leaf certificate only.
|
||||||
|
- In selftest mode, test certificates are compiled into the binary using go:embed.
|
||||||
287
cmd/ca-signed/main.go
Normal file
287
cmd/ca-signed/main.go
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/x509"
|
||||||
|
"embed"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/kyle/goutils/certlib"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try PEM first
|
||||||
|
if certs, err := certlib.ParseCertificatesPEM(data); err == nil {
|
||||||
|
return certs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try DER/PKCS7/PKCS12 (with no password)
|
||||||
|
if certs, _, err := certlib.ParseCertificatesDER(data, ""); err == nil {
|
||||||
|
return certs, nil
|
||||||
|
} else {
|
||||||
|
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) {
|
||||||
|
// Try PEM first
|
||||||
|
if certs, err := certlib.ParseCertificatesPEM(data); err == nil {
|
||||||
|
return certs, nil
|
||||||
|
}
|
||||||
|
// Try DER/PKCS7/PKCS12 (with no password)
|
||||||
|
if certs, _, err := certlib.ParseCertificatesDER(data, ""); err == nil {
|
||||||
|
return certs, nil
|
||||||
|
} else {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makePoolFromBytes(data []byte) (*x509.CertPool, error) {
|
||||||
|
certs, err := loadCertsFromBytes(data)
|
||||||
|
if err != nil || len(certs) == 0 {
|
||||||
|
return nil, fmt.Errorf("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) (ok bool, expiry 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) (ok bool, expiry 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
// selftest runs built-in validation using embedded certificates.
|
||||||
|
func selftest() int {
|
||||||
|
type testCase struct {
|
||||||
|
name string
|
||||||
|
caFile string
|
||||||
|
certFile string
|
||||||
|
expectOK bool
|
||||||
|
}
|
||||||
|
|
||||||
|
cases := []testCase{
|
||||||
|
{name: "ISRG Root X1 validates LE E7", caFile: "testdata/isrg-root-x1.pem", certFile: "testdata/le-e7.pem", expectOK: true},
|
||||||
|
{name: "ISRG Root X1 does NOT validate Google WR2", caFile: "testdata/isrg-root-x1.pem", certFile: "testdata/goog-wr2.pem", expectOK: false},
|
||||||
|
{name: "GTS R1 validates Google WR2", caFile: "testdata/gts-r1.pem", certFile: "testdata/goog-wr2.pem", expectOK: true},
|
||||||
|
{name: "GTS R1 does NOT validate LE E7", caFile: "testdata/gts-r1.pem", certFile: "testdata/le-e7.pem", expectOK: false},
|
||||||
|
}
|
||||||
|
|
||||||
|
failures := 0
|
||||||
|
for _, tc := range cases {
|
||||||
|
caBytes, err := embeddedTestdata.ReadFile(tc.caFile)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "selftest: failed to read embedded %s: %v\n", tc.caFile, err)
|
||||||
|
failures++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
certBytes, err := embeddedTestdata.ReadFile(tc.certFile)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "selftest: failed to read embedded %s: %v\n", tc.certFile, err)
|
||||||
|
failures++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pool, err := makePoolFromBytes(caBytes)
|
||||||
|
if err != nil || pool == nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "selftest: failed to build CA pool for %s: %v\n", tc.caFile, err)
|
||||||
|
failures++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ok, exp := verifyAgainstCABytes(pool, certBytes)
|
||||||
|
if ok != tc.expectOK {
|
||||||
|
fmt.Printf("%s: unexpected result: got %v, want %v\n", tc.name, ok, tc.expectOK)
|
||||||
|
failures++
|
||||||
|
} else {
|
||||||
|
if ok {
|
||||||
|
fmt.Printf("%s: OK (expires %s)\n", tc.name, exp)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("%s: INVALID (as expected)\n", tc.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that both embedded root CAs are detected as self-signed
|
||||||
|
roots := []string{"testdata/gts-r1.pem", "testdata/isrg-root-x1.pem"}
|
||||||
|
for _, root := range roots {
|
||||||
|
b, err := embeddedTestdata.ReadFile(root)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "selftest: failed to read embedded %s: %v\n", root, err)
|
||||||
|
failures++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
certs, err := loadCertsFromBytes(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) {
|
||||||
|
fmt.Printf("%s: SELF-SIGNED (as expected)\n", root)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("%s: expected SELF-SIGNED, but was not detected as such\n", root)
|
||||||
|
failures++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if failures == 0 {
|
||||||
|
fmt.Println("selftest: PASS")
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
fmt.Fprintf(os.Stderr, "selftest: FAIL (%d failure(s))\n", failures)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Special selftest mode: single argument "selftest"
|
||||||
|
if len(os.Args) == 2 && os.Args[1] == "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)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, certPath := range os.Args[2:] {
|
||||||
|
ok, exp := verifyAgainstCA(caPool, certPath)
|
||||||
|
name := filepath.Base(certPath)
|
||||||
|
// Load the leaf once for self-signed detection and potential expiry fallback
|
||||||
|
var leaf *x509.Certificate
|
||||||
|
if certs, err := loadCertsFromFile(certPath); err == nil && len(certs) > 0 {
|
||||||
|
leaf = certs[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the certificate is self-signed, prefer the SELF-SIGNED label
|
||||||
|
if isSelfSigned(leaf) {
|
||||||
|
fmt.Printf("%s: SELF-SIGNED\n", name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
// Display with the requested format
|
||||||
|
// Example: file: OK (expires 2031-01-01)
|
||||||
|
// Ensure deterministic date formatting
|
||||||
|
// Note: no timezone displayed; date only as per example
|
||||||
|
// If exp ended up empty for some reason, recompute safely
|
||||||
|
if exp == "" {
|
||||||
|
if leaf != nil {
|
||||||
|
exp = leaf.NotAfter.Format("2006-01-02")
|
||||||
|
} else {
|
||||||
|
// fallback to the current date to avoid empty; though shouldn't happen
|
||||||
|
exp = time.Now().Format("2006-01-02")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Printf("%s: OK (expires %s)\n", name, exp)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("%s: INVALID\n", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
cmd/ca-signed/testdata/goog-wr2.pem
vendored
Normal file
29
cmd/ca-signed/testdata/goog-wr2.pem
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
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
|
||||||
|
-----END CERTIFICATE-----
|
||||||
31
cmd/ca-signed/testdata/gts-r1.pem
vendored
Normal file
31
cmd/ca-signed/testdata/gts-r1.pem
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
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
|
||||||
|
bP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3gm3c
|
||||||
|
-----END CERTIFICATE-----
|
||||||
31
cmd/ca-signed/testdata/isrg-root-x1.pem
vendored
Normal file
31
cmd/ca-signed/testdata/isrg-root-x1.pem
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
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
|
||||||
|
-----END CERTIFICATE-----
|
||||||
26
cmd/ca-signed/testdata/le-e7.pem
vendored
Normal file
26
cmd/ca-signed/testdata/le-e7.pem
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIEVzCCAj+gAwIBAgIRAKp18eYrjwoiCWbTi7/UuqEwDQYJKoZIhvcNAQELBQAw
|
||||||
|
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
|
||||||
|
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjQwMzEzMDAwMDAw
|
||||||
|
WhcNMjcwMzEyMjM1OTU5WjAyMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg
|
||||||
|
RW5jcnlwdDELMAkGA1UEAxMCRTcwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARB6AST
|
||||||
|
CFh/vjcwDMCgQer+VtqEkz7JANurZxLP+U9TCeioL6sp5Z8VRvRbYk4P1INBmbef
|
||||||
|
QHJFHCxcSjKmwtvGBWpl/9ra8HW0QDsUaJW2qOJqceJ0ZVFT3hbUHifBM/2jgfgw
|
||||||
|
gfUwDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcD
|
||||||
|
ATASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBSuSJ7chx1EoG/aouVgdAR4
|
||||||
|
wpwAgDAfBgNVHSMEGDAWgBR5tFnme7bl5AFzgAiIyBpY9umbbjAyBggrBgEFBQcB
|
||||||
|
AQQmMCQwIgYIKwYBBQUHMAKGFmh0dHA6Ly94MS5pLmxlbmNyLm9yZy8wEwYDVR0g
|
||||||
|
BAwwCjAIBgZngQwBAgEwJwYDVR0fBCAwHjAcoBqgGIYWaHR0cDovL3gxLmMubGVu
|
||||||
|
Y3Iub3JnLzANBgkqhkiG9w0BAQsFAAOCAgEAjx66fDdLk5ywFn3CzA1w1qfylHUD
|
||||||
|
aEf0QZpXcJseddJGSfbUUOvbNR9N/QQ16K1lXl4VFyhmGXDT5Kdfcr0RvIIVrNxF
|
||||||
|
h4lqHtRRCP6RBRstqbZ2zURgqakn/Xip0iaQL0IdfHBZr396FgknniRYFckKORPG
|
||||||
|
yM3QKnd66gtMst8I5nkRQlAg/Jb+Gc3egIvuGKWboE1G89NTsN9LTDD3PLj0dUMr
|
||||||
|
OIuqVjLB8pEC6yk9enrlrqjXQgkLEYhXzq7dLafv5Vkig6Gl0nuuqjqfp0Q1bi1o
|
||||||
|
yVNAlXe6aUXw92CcghC9bNsKEO1+M52YY5+ofIXlS/SEQbvVYYBLZ5yeiglV6t3S
|
||||||
|
M6H+vTG0aP9YHzLn/KVOHzGQfXDP7qM5tkf+7diZe7o2fw6O7IvN6fsQXEQQj8TJ
|
||||||
|
UXJxv2/uJhcuy/tSDgXwHM8Uk34WNbRT7zGTGkQRX0gsbjAea/jYAoWv0ZvQRwpq
|
||||||
|
Pe79D/i7Cep8qWnA+7AE/3B3S/3dEEYmc0lpe1366A/6GEgk3ktr9PEoQrLChs6I
|
||||||
|
tu3wnNLB2euC8IKGLQFpGtOO/2/hiAKjyajaBP25w1jF0Wl8Bbqne3uZ2q1GyPFJ
|
||||||
|
YRmT7/OXpmOH/FVLtwS+8ng1cAmpCujPwteJZNcDG0sF2n/sc0+SQf49fdyUK0ty
|
||||||
|
+VUwFj9tmWxyR/M=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
148
cmd/cert-bundler/README.txt
Normal file
148
cmd/cert-bundler/README.txt
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
cert-bundler: create certificate chain archives
|
||||||
|
------------------------------------------------
|
||||||
|
|
||||||
|
Description
|
||||||
|
cert-bundler creates archives of certificate chains from a YAML configuration
|
||||||
|
file. It validates certificates, checks expiration dates, and generates
|
||||||
|
archives in multiple formats (zip, tar.gz) with optional manifest files
|
||||||
|
containing SHA256 checksums.
|
||||||
|
|
||||||
|
Usage
|
||||||
|
cert-bundler [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-c <file> Path to YAML configuration file (default: bundle.yaml)
|
||||||
|
-o <dir> Output directory for archives (default: pkg)
|
||||||
|
|
||||||
|
YAML Configuration Format
|
||||||
|
|
||||||
|
The configuration file uses the following structure:
|
||||||
|
|
||||||
|
config:
|
||||||
|
hashes: <filename>
|
||||||
|
expiry: <duration>
|
||||||
|
chains:
|
||||||
|
<group_name>:
|
||||||
|
certs:
|
||||||
|
- root: <path>
|
||||||
|
intermediates:
|
||||||
|
- <path>
|
||||||
|
- <path>
|
||||||
|
- root: <path>
|
||||||
|
intermediates:
|
||||||
|
- <path>
|
||||||
|
outputs:
|
||||||
|
include_single: <bool>
|
||||||
|
include_individual: <bool>
|
||||||
|
manifest: <bool>
|
||||||
|
encoding: <encoding>
|
||||||
|
formats:
|
||||||
|
- <format>
|
||||||
|
- <format>
|
||||||
|
|
||||||
|
Configuration Fields
|
||||||
|
|
||||||
|
config:
|
||||||
|
hashes: (optional) Name of the file to write SHA256 checksums of all
|
||||||
|
generated archives. If omitted, no hash file is created.
|
||||||
|
expiry: (optional) Expiration warning threshold. Certificates expiring
|
||||||
|
within this period will trigger a warning. Supports formats like
|
||||||
|
"1y" (year), "6m" (month), "30d" (day). Default: 1y
|
||||||
|
|
||||||
|
chains:
|
||||||
|
Each key under "chains" defines a named certificate group. All certificates
|
||||||
|
in a group are bundled together into archives with the group name.
|
||||||
|
|
||||||
|
certs:
|
||||||
|
List of certificate chains. Each chain has:
|
||||||
|
root: Path to root CA certificate (PEM or DER format)
|
||||||
|
intermediates: List of paths to intermediate certificates
|
||||||
|
|
||||||
|
All intermediates are validated against their root CA. An error is
|
||||||
|
reported if signature verification fails.
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
Defines output formats and content for the group's archives:
|
||||||
|
|
||||||
|
include_single: (bool) If true, all certificates in the group are
|
||||||
|
concatenated into a single file named "bundle.pem"
|
||||||
|
(or "bundle.crt" for DER encoding).
|
||||||
|
|
||||||
|
include_individual: (bool) If true, each certificate is included as
|
||||||
|
a separate file in the archive, named after the
|
||||||
|
original file (e.g., "int/cca2.pem" becomes
|
||||||
|
"cca2.pem").
|
||||||
|
|
||||||
|
manifest: (bool) If true, a MANIFEST file is included containing
|
||||||
|
SHA256 checksums of all files in the archive.
|
||||||
|
|
||||||
|
encoding: Specifies certificate encoding in the archive:
|
||||||
|
- "pem": PEM format with .pem extension (default)
|
||||||
|
- "der": DER format with .crt extension
|
||||||
|
- "both": Both PEM and DER versions are included
|
||||||
|
|
||||||
|
formats: List of archive formats to generate:
|
||||||
|
- "zip": Creates a .zip archive
|
||||||
|
- "tgz": Creates a .tar.gz archive
|
||||||
|
|
||||||
|
Output Files
|
||||||
|
|
||||||
|
For each group and format combination, an archive is created:
|
||||||
|
<group_name>.zip or <group_name>.tar.gz
|
||||||
|
|
||||||
|
If config.hashes is specified, a hash file is created in the output directory
|
||||||
|
containing SHA256 checksums of all generated archives.
|
||||||
|
|
||||||
|
Example Configuration
|
||||||
|
|
||||||
|
config:
|
||||||
|
hashes: bundle.sha256
|
||||||
|
expiry: 1y
|
||||||
|
chains:
|
||||||
|
core_certs:
|
||||||
|
certs:
|
||||||
|
- root: roots/core-ca.pem
|
||||||
|
intermediates:
|
||||||
|
- int/cca1.pem
|
||||||
|
- int/cca2.pem
|
||||||
|
- int/cca3.pem
|
||||||
|
- root: roots/ssh-ca.pem
|
||||||
|
intermediates:
|
||||||
|
- ssh/ssh_dmz1.pem
|
||||||
|
- ssh/ssh_internal.pem
|
||||||
|
outputs:
|
||||||
|
include_single: true
|
||||||
|
include_individual: true
|
||||||
|
manifest: true
|
||||||
|
encoding: pem
|
||||||
|
formats:
|
||||||
|
- zip
|
||||||
|
- tgz
|
||||||
|
|
||||||
|
This configuration:
|
||||||
|
- Creates core_certs.zip and core_certs.tar.gz in the output directory
|
||||||
|
- Each archive contains bundle.pem (all certificates concatenated)
|
||||||
|
- Each archive contains individual certificates (core-ca.pem, cca1.pem, etc.)
|
||||||
|
- Each archive includes a MANIFEST file with SHA256 checksums
|
||||||
|
- Creates bundle.sha256 with checksums of the two archives
|
||||||
|
- Warns if any certificate expires within 1 year
|
||||||
|
|
||||||
|
Examples
|
||||||
|
|
||||||
|
# Create bundles using default configuration (bundle.yaml -> pkg/)
|
||||||
|
cert-bundler
|
||||||
|
|
||||||
|
# Use custom configuration and output directory
|
||||||
|
cert-bundler -c myconfig.yaml -o output
|
||||||
|
|
||||||
|
# Create bundles from testdata configuration
|
||||||
|
cert-bundler -c testdata/bundle.yaml -o testdata/pkg
|
||||||
|
|
||||||
|
Notes
|
||||||
|
- Certificate paths in the YAML are relative to the current working directory
|
||||||
|
- All intermediates must be properly signed by their specified root CA
|
||||||
|
- Certificates are checked for expiration; warnings are printed to stderr
|
||||||
|
- Expired certificates do not prevent archive creation but generate warnings
|
||||||
|
- Both PEM and DER certificate formats are supported as input
|
||||||
|
- Archive filenames use the group name, not individual chain names
|
||||||
|
- If both include_single and include_individual are true, archives contain both
|
||||||
528
cmd/cert-bundler/main.go
Normal file
528
cmd/cert-bundler/main.go
Normal file
@@ -0,0 +1,528 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"archive/zip"
|
||||||
|
"compress/gzip"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/x509"
|
||||||
|
_ "embed"
|
||||||
|
"encoding/pem"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/kyle/goutils/certlib"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config represents the top-level YAML configuration
|
||||||
|
type Config struct {
|
||||||
|
Config struct {
|
||||||
|
Hashes string `yaml:"hashes"`
|
||||||
|
Expiry string `yaml:"expiry"`
|
||||||
|
} `yaml:"config"`
|
||||||
|
Chains map[string]ChainGroup `yaml:"chains"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChainGroup represents a named group of certificate chains
|
||||||
|
type ChainGroup struct {
|
||||||
|
Certs []CertChain `yaml:"certs"`
|
||||||
|
Outputs Outputs `yaml:"outputs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CertChain represents a root certificate and its intermediates
|
||||||
|
type CertChain struct {
|
||||||
|
Root string `yaml:"root"`
|
||||||
|
Intermediates []string `yaml:"intermediates"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Outputs defines output format options
|
||||||
|
type Outputs struct {
|
||||||
|
IncludeSingle bool `yaml:"include_single"`
|
||||||
|
IncludeIndividual bool `yaml:"include_individual"`
|
||||||
|
Manifest bool `yaml:"manifest"`
|
||||||
|
Formats []string `yaml:"formats"`
|
||||||
|
Encoding string `yaml:"encoding"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
configFile string
|
||||||
|
outputDir string
|
||||||
|
)
|
||||||
|
|
||||||
|
var formatExtensions = map[string]string{
|
||||||
|
"zip": ".zip",
|
||||||
|
"tgz": ".tar.gz",
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:embed README.txt
|
||||||
|
var readmeContent string
|
||||||
|
|
||||||
|
func usage() {
|
||||||
|
fmt.Fprint(os.Stderr, readmeContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Usage = usage
|
||||||
|
flag.StringVar(&configFile, "c", "bundle.yaml", "path to YAML configuration file")
|
||||||
|
flag.StringVar(&outputDir, "o", "pkg", "output directory for archives")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if configFile == "" {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: configuration file required (-c flag)\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load and parse configuration
|
||||||
|
cfg, err := loadConfig(configFile)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse expiry duration (default 1 year)
|
||||||
|
expiryDuration := 365 * 24 * time.Hour
|
||||||
|
if cfg.Config.Expiry != "" {
|
||||||
|
expiryDuration, err = parseDuration(cfg.Config.Expiry)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error parsing expiry: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create output directory if it doesn't exist
|
||||||
|
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error creating output directory: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process each chain group
|
||||||
|
// 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 {
|
||||||
|
files, err := processChainGroup(groupName, group, expiryDuration)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error processing chain group %s: %v\n", groupName, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
createdFiles = append(createdFiles, files...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate hash file for all created archives
|
||||||
|
if cfg.Config.Hashes != "" {
|
||||||
|
hashFile := filepath.Join(outputDir, cfg.Config.Hashes)
|
||||||
|
if err := generateHashFile(hashFile, createdFiles); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error generating hash file: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Certificate bundling completed successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadConfig(path string) (*Config, error) {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfg Config
|
||||||
|
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseDuration(s string) (time.Duration, error) {
|
||||||
|
// Support simple formats like "1y", "6m", "30d"
|
||||||
|
if len(s) < 2 {
|
||||||
|
return 0, fmt.Errorf("invalid duration format: %s", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
unit := s[len(s)-1]
|
||||||
|
value := s[:len(s)-1]
|
||||||
|
|
||||||
|
var multiplier time.Duration
|
||||||
|
switch unit {
|
||||||
|
case 'y', 'Y':
|
||||||
|
multiplier = 365 * 24 * time.Hour
|
||||||
|
case 'm', 'M':
|
||||||
|
multiplier = 30 * 24 * time.Hour
|
||||||
|
case 'd', 'D':
|
||||||
|
multiplier = 24 * time.Hour
|
||||||
|
default:
|
||||||
|
return time.ParseDuration(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
var num int
|
||||||
|
_, err := fmt.Sscanf(value, "%d", &num)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("invalid duration value: %s", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.Duration(num) * multiplier, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func processChainGroup(groupName string, group ChainGroup, expiryDuration time.Duration) ([]string, error) {
|
||||||
|
// Default encoding to "pem" if not specified
|
||||||
|
encoding := group.Outputs.Encoding
|
||||||
|
if encoding == "" {
|
||||||
|
encoding = "pem"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 individualCerts []certWithPath
|
||||||
|
|
||||||
|
for _, chain := range chains {
|
||||||
|
// Load root certificate
|
||||||
|
rootCert, err := certlib.LoadCertificate(chain.Root)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to load root certificate %s: %v", chain.Root, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check expiry for root
|
||||||
|
checkExpiry(chain.Root, rootCert, expiryDuration)
|
||||||
|
|
||||||
|
// Add root to collections if needed
|
||||||
|
if outputs.IncludeSingle {
|
||||||
|
singleFileCerts = append(singleFileCerts, rootCert)
|
||||||
|
}
|
||||||
|
if outputs.IncludeIndividual {
|
||||||
|
individualCerts = append(individualCerts, certWithPath{
|
||||||
|
cert: rootCert,
|
||||||
|
path: chain.Root,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load and validate intermediates
|
||||||
|
for _, intPath := range chain.Intermediates {
|
||||||
|
intCert, err := certlib.LoadCertificate(intPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to load intermediate certificate %s: %v", intPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that intermediate is signed by root
|
||||||
|
if err := intCert.CheckSignatureFrom(rootCert); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("intermediate %s is not properly signed by root %s: %v", intPath, chain.Root, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check expiry for intermediate
|
||||||
|
checkExpiry(intPath, intCert, expiryDuration)
|
||||||
|
|
||||||
|
// Add intermediate to collections if needed
|
||||||
|
if outputs.IncludeSingle {
|
||||||
|
singleFileCerts = append(singleFileCerts, intCert)
|
||||||
|
}
|
||||||
|
if outputs.IncludeIndividual {
|
||||||
|
individualCerts = append(individualCerts, certWithPath{
|
||||||
|
cert: intCert,
|
||||||
|
path: intPath,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
// Handle a single bundle file
|
||||||
|
if outputs.IncludeSingle && len(singleFileCerts) > 0 {
|
||||||
|
files, err := encodeCertsToFiles(singleFileCerts, "bundle", encoding, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to encode single bundle: %v", err)
|
||||||
|
}
|
||||||
|
archiveFiles = append(archiveFiles, files...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle individual files
|
||||||
|
if outputs.IncludeIndividual {
|
||||||
|
for _, cp := range individualCerts {
|
||||||
|
baseName := strings.TrimSuffix(filepath.Base(cp.path), filepath.Ext(cp.path))
|
||||||
|
files, err := encodeCertsToFiles([]*x509.Certificate{cp.cert}, baseName, encoding, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to encode individual cert %s: %v", cp.path, err)
|
||||||
|
}
|
||||||
|
archiveFiles = append(archiveFiles, files...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate manifest if requested
|
||||||
|
if outputs.Manifest {
|
||||||
|
manifestContent := generateManifest(archiveFiles)
|
||||||
|
archiveFiles = append(archiveFiles, fileEntry{
|
||||||
|
name: "MANIFEST",
|
||||||
|
content: manifestContent,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return archiveFiles, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unsupported format: %s", format)
|
||||||
|
}
|
||||||
|
archivePath := filepath.Join(outputDir, groupName+ext)
|
||||||
|
switch format {
|
||||||
|
case "zip":
|
||||||
|
if err := createZipArchive(archivePath, archiveFiles); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create zip archive: %v", err)
|
||||||
|
}
|
||||||
|
case "tgz":
|
||||||
|
if err := createTarGzArchive(archivePath, archiveFiles); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create tar.gz archive: %v", err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported format: %s", format)
|
||||||
|
}
|
||||||
|
createdFiles = append(createdFiles, archivePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return createdFiles, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkExpiry(path string, cert *x509.Certificate, expiryDuration time.Duration) {
|
||||||
|
now := time.Now()
|
||||||
|
expiryThreshold := now.Add(expiryDuration)
|
||||||
|
|
||||||
|
if cert.NotAfter.Before(expiryThreshold) {
|
||||||
|
daysUntilExpiry := int(cert.NotAfter.Sub(now).Hours() / 24)
|
||||||
|
if daysUntilExpiry < 0 {
|
||||||
|
fmt.Fprintf(os.Stderr, "WARNING: Certificate %s has EXPIRED (expired %d days ago)\n", path, -daysUntilExpiry)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(os.Stderr, "WARNING: Certificate %s will expire in %d days (on %s)\n", path, daysUntilExpiry, cert.NotAfter.Format("2006-01-02"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type fileEntry struct {
|
||||||
|
name string
|
||||||
|
content []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type certWithPath struct {
|
||||||
|
cert *x509.Certificate
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeCertsToFiles converts certificates to file entries based on encoding type
|
||||||
|
// If isSingle is true, certs are concatenated into a single file; otherwise one cert per file
|
||||||
|
func encodeCertsToFiles(certs []*x509.Certificate, baseName string, encoding string, isSingle bool) ([]fileEntry, error) {
|
||||||
|
var files []fileEntry
|
||||||
|
|
||||||
|
switch encoding {
|
||||||
|
case "pem":
|
||||||
|
pemContent := encodeCertsToPEM(certs)
|
||||||
|
files = append(files, fileEntry{
|
||||||
|
name: baseName + ".pem",
|
||||||
|
content: pemContent,
|
||||||
|
})
|
||||||
|
case "der":
|
||||||
|
if isSingle {
|
||||||
|
// For single file in DER, concatenate all cert DER bytes
|
||||||
|
var derContent []byte
|
||||||
|
for _, cert := range certs {
|
||||||
|
derContent = append(derContent, cert.Raw...)
|
||||||
|
}
|
||||||
|
files = append(files, fileEntry{
|
||||||
|
name: baseName + ".crt",
|
||||||
|
content: derContent,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Individual DER file (should only have one cert)
|
||||||
|
if len(certs) > 0 {
|
||||||
|
files = append(files, fileEntry{
|
||||||
|
name: baseName + ".crt",
|
||||||
|
content: certs[0].Raw,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "both":
|
||||||
|
// Add PEM version
|
||||||
|
pemContent := encodeCertsToPEM(certs)
|
||||||
|
files = append(files, fileEntry{
|
||||||
|
name: baseName + ".pem",
|
||||||
|
content: pemContent,
|
||||||
|
})
|
||||||
|
// Add DER version
|
||||||
|
if isSingle {
|
||||||
|
var derContent []byte
|
||||||
|
for _, cert := range certs {
|
||||||
|
derContent = append(derContent, cert.Raw...)
|
||||||
|
}
|
||||||
|
files = append(files, fileEntry{
|
||||||
|
name: baseName + ".crt",
|
||||||
|
content: derContent,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
if len(certs) > 0 {
|
||||||
|
files = append(files, fileEntry{
|
||||||
|
name: baseName + ".crt",
|
||||||
|
content: certs[0].Raw,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported encoding: %s (must be 'pem', 'der', or 'both')", encoding)
|
||||||
|
}
|
||||||
|
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeCertsToPEM encodes certificates to PEM format
|
||||||
|
func encodeCertsToPEM(certs []*x509.Certificate) []byte {
|
||||||
|
var pemContent []byte
|
||||||
|
for _, cert := range certs {
|
||||||
|
pemBlock := &pem.Block{
|
||||||
|
Type: "CERTIFICATE",
|
||||||
|
Bytes: cert.Raw,
|
||||||
|
}
|
||||||
|
pemContent = append(pemContent, pem.EncodeToMemory(pemBlock)...)
|
||||||
|
}
|
||||||
|
return pemContent
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateManifest(files []fileEntry) []byte {
|
||||||
|
var manifest strings.Builder
|
||||||
|
for _, file := range files {
|
||||||
|
if file.name == "MANIFEST" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
hash := sha256.Sum256(file.content)
|
||||||
|
manifest.WriteString(fmt.Sprintf("%x %s\n", hash, file.name))
|
||||||
|
}
|
||||||
|
return []byte(manifest.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func createZipArchive(path string, files []fileEntry) error {
|
||||||
|
f, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
w := zip.NewWriter(f)
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
fw, err := w.Create(file.name)
|
||||||
|
if err != nil {
|
||||||
|
w.Close()
|
||||||
|
f.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := fw.Write(file.content); err != nil {
|
||||||
|
w.Close()
|
||||||
|
f.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
f, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
gw := gzip.NewWriter(f)
|
||||||
|
tw := tar.NewWriter(gw)
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
hdr := &tar.Header{
|
||||||
|
Name: file.name,
|
||||||
|
Mode: 0644,
|
||||||
|
Size: int64(len(file.content)),
|
||||||
|
}
|
||||||
|
if err := tw.WriteHeader(hdr); err != nil {
|
||||||
|
tw.Close()
|
||||||
|
gw.Close()
|
||||||
|
f.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := tw.Write(file.content); err != nil {
|
||||||
|
tw.Close()
|
||||||
|
gw.Close()
|
||||||
|
f.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
f, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
data, err := os.ReadFile(file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
hash := sha256.Sum256(data)
|
||||||
|
fmt.Fprintf(f, "%x %s\n", hash, filepath.Base(file))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
194
cmd/cert-bundler/prompt.txt
Normal file
194
cmd/cert-bundler/prompt.txt
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
Task: build a certificate bundling tool in cmd/cert-bundler. It
|
||||||
|
creates archives of certificates chains.
|
||||||
|
|
||||||
|
A YAML file for this looks something like:
|
||||||
|
|
||||||
|
``` yaml
|
||||||
|
config:
|
||||||
|
hashes: bundle.sha256
|
||||||
|
expiry: 1y
|
||||||
|
chains:
|
||||||
|
core_certs:
|
||||||
|
certs:
|
||||||
|
- root: roots/core-ca.pem
|
||||||
|
intermediates:
|
||||||
|
- int/cca1.pem
|
||||||
|
- int/cca2.pem
|
||||||
|
- int/cca3.pem
|
||||||
|
- root: roots/ssh-ca.pem
|
||||||
|
intermediates:
|
||||||
|
- ssh/ssh_dmz1.pem
|
||||||
|
- ssh/ssh_internal.pem
|
||||||
|
outputs:
|
||||||
|
include_single: true
|
||||||
|
include_individual: true
|
||||||
|
manifest: true
|
||||||
|
formats:
|
||||||
|
- zip
|
||||||
|
- tgz
|
||||||
|
```
|
||||||
|
|
||||||
|
Some requirements:
|
||||||
|
|
||||||
|
1. First, all the certificates should be loaded.
|
||||||
|
2. For each root, each of the indivudal intermediates should be
|
||||||
|
checked to make sure they are properly signed by the root CA.
|
||||||
|
3. The program should optionally take an expiration period (defaulting
|
||||||
|
to one year), specified in config.expiration, and if any certificate
|
||||||
|
is within that expiration period, a warning should be printed.
|
||||||
|
4. If outputs.include_single is true, all certificates under chains
|
||||||
|
should be concatenated into a single file.
|
||||||
|
5. If outputs.include_individual is true, all certificates under
|
||||||
|
chains should be included at the root level (e.g. int/cca2.pem
|
||||||
|
would be cca2.pem in the archive).
|
||||||
|
6. If bundle.manifest is true, a "MANIFEST" file is created with
|
||||||
|
SHA256 sums of each file included in the archive.
|
||||||
|
7. For each of the formats, create an archive file in the output
|
||||||
|
directory (specified with `-o`) with that format.
|
||||||
|
- If zip is included, create a .zip file.
|
||||||
|
- If tgz is included, create a .tar.gz file with default compression
|
||||||
|
levels.
|
||||||
|
- All archive files should include any generated files (single
|
||||||
|
and/or individual) in the top-level directory.
|
||||||
|
8. In the output directory, create a file with the same name as
|
||||||
|
config.hashes that contains the SHA256 sum of all files created.
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
|
The outputs.include_single and outputs.include_individual describe
|
||||||
|
what should go in the final archive. If both are specified, the output
|
||||||
|
archive should include both a single bundle.pem and each individual
|
||||||
|
certificate, for example.
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
|
As it stands, given the following `bundle.yaml`:
|
||||||
|
|
||||||
|
``` yaml
|
||||||
|
config:
|
||||||
|
hashes: bundle.sha256
|
||||||
|
expiry: 1y
|
||||||
|
chains:
|
||||||
|
core_certs:
|
||||||
|
certs:
|
||||||
|
- root: pems/gts-r1.pem
|
||||||
|
intermediates:
|
||||||
|
- pems/goog-wr2.pem
|
||||||
|
outputs:
|
||||||
|
include_single: true
|
||||||
|
include_individual: true
|
||||||
|
manifest: true
|
||||||
|
formats:
|
||||||
|
- zip
|
||||||
|
- tgz
|
||||||
|
- root: pems/isrg-root-x1.pem
|
||||||
|
intermediates:
|
||||||
|
- pems/le-e7.pem
|
||||||
|
outputs:
|
||||||
|
include_single: true
|
||||||
|
include_individual: false
|
||||||
|
manifest: true
|
||||||
|
formats:
|
||||||
|
- zip
|
||||||
|
- tgz
|
||||||
|
google_certs:
|
||||||
|
certs:
|
||||||
|
- root: pems/gts-r1.pem
|
||||||
|
intermediates:
|
||||||
|
- pems/goog-wr2.pem
|
||||||
|
outputs:
|
||||||
|
include_single: true
|
||||||
|
include_individual: false
|
||||||
|
manifest: true
|
||||||
|
formats:
|
||||||
|
- tgz
|
||||||
|
lets_encrypt:
|
||||||
|
certs:
|
||||||
|
- root: pems/isrg-root-x1.pem
|
||||||
|
intermediates:
|
||||||
|
- pems/le-e7.pem
|
||||||
|
outputs:
|
||||||
|
include_single: false
|
||||||
|
include_individual: true
|
||||||
|
manifest: false
|
||||||
|
formats:
|
||||||
|
- zip
|
||||||
|
```
|
||||||
|
|
||||||
|
The program outputs the following files:
|
||||||
|
|
||||||
|
- bundle.sha256
|
||||||
|
- core_certs_0.tgz (contains individual certs)
|
||||||
|
- core_certs_0.zip (contains individual certs)
|
||||||
|
- core_certs_1.tgz (contains core_certs.pem)
|
||||||
|
- core_certs_1.zip (contains core_certs.pem)
|
||||||
|
- google_certs_0.tgz
|
||||||
|
- lets_encrypt_0.zip
|
||||||
|
|
||||||
|
It should output
|
||||||
|
|
||||||
|
- bundle.sha256
|
||||||
|
- core_certs.tgz
|
||||||
|
- core_certs.zip
|
||||||
|
- google_certs.tgz
|
||||||
|
- lets_encrypt.zip
|
||||||
|
|
||||||
|
core_certs.* should contain `bundle.pem` and all the individual
|
||||||
|
certs. There should be no _$n$ variants of archives.
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
|
Add an additional field to outputs: encoding. It should accept one of
|
||||||
|
`der`, `pem`, or `both`. If `der`, certificates should be output as a
|
||||||
|
`.crt` file containing a DER-encoded certificate. If `pem`, certificates
|
||||||
|
should be output as a `.pem` file containing a PEM-encoded certificate.
|
||||||
|
If both, both the `.crt` and `.pem` certificate should be included.
|
||||||
|
|
||||||
|
For example, given the previous config, if `encoding` is der, the
|
||||||
|
google_certs.tgz archive should contain
|
||||||
|
|
||||||
|
- bundle.crt
|
||||||
|
- MANIFEST
|
||||||
|
|
||||||
|
Or with lets_encrypt.zip:
|
||||||
|
|
||||||
|
- isrg-root-x1.crt
|
||||||
|
- le-e7.crt
|
||||||
|
|
||||||
|
However, if `encoding` is pem, the lets_encrypt.zip archive should contain:
|
||||||
|
|
||||||
|
- isrg-root-x1.pem
|
||||||
|
- le-e7.pem
|
||||||
|
|
||||||
|
And if it `encoding` is both, the lets_encrypt.zip archive should contain:
|
||||||
|
|
||||||
|
- isrg-root-x1.crt
|
||||||
|
- isrg-root-x1.pem
|
||||||
|
- le-e7.crt
|
||||||
|
- le-e7.pem
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
|
The tgz format should output a `.tar.gz` file instead of a `.tgz` file.
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
|
Move the format extensions to a global variable.
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
|
Write a README.txt with a description of the bundle.yaml format.
|
||||||
|
|
||||||
|
Additionally, update the help text for the program (e.g. with `-h`)
|
||||||
|
to provide the same detailed information.
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
|
||||||
43
cmd/cert-bundler/testdata/bundle.yaml
vendored
Normal file
43
cmd/cert-bundler/testdata/bundle.yaml
vendored
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
config:
|
||||||
|
hashes: bundle.sha256
|
||||||
|
expiry: 1y
|
||||||
|
chains:
|
||||||
|
core_certs:
|
||||||
|
certs:
|
||||||
|
- root: pems/gts-r1.pem
|
||||||
|
intermediates:
|
||||||
|
- pems/goog-wr2.pem
|
||||||
|
- root: pems/isrg-root-x1.pem
|
||||||
|
intermediates:
|
||||||
|
- pems/le-e7.pem
|
||||||
|
outputs:
|
||||||
|
include_single: true
|
||||||
|
include_individual: true
|
||||||
|
manifest: true
|
||||||
|
formats:
|
||||||
|
- zip
|
||||||
|
- tgz
|
||||||
|
google_certs:
|
||||||
|
certs:
|
||||||
|
- root: pems/gts-r1.pem
|
||||||
|
intermediates:
|
||||||
|
- pems/goog-wr2.pem
|
||||||
|
outputs:
|
||||||
|
include_single: true
|
||||||
|
include_individual: false
|
||||||
|
manifest: true
|
||||||
|
encoding: der
|
||||||
|
formats:
|
||||||
|
- tgz
|
||||||
|
lets_encrypt:
|
||||||
|
certs:
|
||||||
|
- root: pems/isrg-root-x1.pem
|
||||||
|
intermediates:
|
||||||
|
- pems/le-e7.pem
|
||||||
|
outputs:
|
||||||
|
include_single: false
|
||||||
|
include_individual: true
|
||||||
|
manifest: false
|
||||||
|
encoding: both
|
||||||
|
formats:
|
||||||
|
- zip
|
||||||
29
cmd/cert-bundler/testdata/pems/goog-wr2.pem
vendored
Normal file
29
cmd/cert-bundler/testdata/pems/goog-wr2.pem
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
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
|
||||||
|
-----END CERTIFICATE-----
|
||||||
31
cmd/cert-bundler/testdata/pems/gts-r1.pem
vendored
Normal file
31
cmd/cert-bundler/testdata/pems/gts-r1.pem
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
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
|
||||||
|
bP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3gm3c
|
||||||
|
-----END CERTIFICATE-----
|
||||||
31
cmd/cert-bundler/testdata/pems/isrg-root-x1.pem
vendored
Normal file
31
cmd/cert-bundler/testdata/pems/isrg-root-x1.pem
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
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
|
||||||
|
-----END CERTIFICATE-----
|
||||||
26
cmd/cert-bundler/testdata/pems/le-e7.pem
vendored
Normal file
26
cmd/cert-bundler/testdata/pems/le-e7.pem
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIEVzCCAj+gAwIBAgIRAKp18eYrjwoiCWbTi7/UuqEwDQYJKoZIhvcNAQELBQAw
|
||||||
|
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
|
||||||
|
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjQwMzEzMDAwMDAw
|
||||||
|
WhcNMjcwMzEyMjM1OTU5WjAyMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg
|
||||||
|
RW5jcnlwdDELMAkGA1UEAxMCRTcwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARB6AST
|
||||||
|
CFh/vjcwDMCgQer+VtqEkz7JANurZxLP+U9TCeioL6sp5Z8VRvRbYk4P1INBmbef
|
||||||
|
QHJFHCxcSjKmwtvGBWpl/9ra8HW0QDsUaJW2qOJqceJ0ZVFT3hbUHifBM/2jgfgw
|
||||||
|
gfUwDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcD
|
||||||
|
ATASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBSuSJ7chx1EoG/aouVgdAR4
|
||||||
|
wpwAgDAfBgNVHSMEGDAWgBR5tFnme7bl5AFzgAiIyBpY9umbbjAyBggrBgEFBQcB
|
||||||
|
AQQmMCQwIgYIKwYBBQUHMAKGFmh0dHA6Ly94MS5pLmxlbmNyLm9yZy8wEwYDVR0g
|
||||||
|
BAwwCjAIBgZngQwBAgEwJwYDVR0fBCAwHjAcoBqgGIYWaHR0cDovL3gxLmMubGVu
|
||||||
|
Y3Iub3JnLzANBgkqhkiG9w0BAQsFAAOCAgEAjx66fDdLk5ywFn3CzA1w1qfylHUD
|
||||||
|
aEf0QZpXcJseddJGSfbUUOvbNR9N/QQ16K1lXl4VFyhmGXDT5Kdfcr0RvIIVrNxF
|
||||||
|
h4lqHtRRCP6RBRstqbZ2zURgqakn/Xip0iaQL0IdfHBZr396FgknniRYFckKORPG
|
||||||
|
yM3QKnd66gtMst8I5nkRQlAg/Jb+Gc3egIvuGKWboE1G89NTsN9LTDD3PLj0dUMr
|
||||||
|
OIuqVjLB8pEC6yk9enrlrqjXQgkLEYhXzq7dLafv5Vkig6Gl0nuuqjqfp0Q1bi1o
|
||||||
|
yVNAlXe6aUXw92CcghC9bNsKEO1+M52YY5+ofIXlS/SEQbvVYYBLZ5yeiglV6t3S
|
||||||
|
M6H+vTG0aP9YHzLn/KVOHzGQfXDP7qM5tkf+7diZe7o2fw6O7IvN6fsQXEQQj8TJ
|
||||||
|
UXJxv2/uJhcuy/tSDgXwHM8Uk34WNbRT7zGTGkQRX0gsbjAea/jYAoWv0ZvQRwpq
|
||||||
|
Pe79D/i7Cep8qWnA+7AE/3B3S/3dEEYmc0lpe1366A/6GEgk3ktr9PEoQrLChs6I
|
||||||
|
tu3wnNLB2euC8IKGLQFpGtOO/2/hiAKjyajaBP25w1jF0Wl8Bbqne3uZ2q1GyPFJ
|
||||||
|
YRmT7/OXpmOH/FVLtwS+8ng1cAmpCujPwteJZNcDG0sF2n/sc0+SQf49fdyUK0ty
|
||||||
|
+VUwFj9tmWxyR/M=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
4
cmd/cert-bundler/testdata/pkg/bundle.sha256
vendored
Normal file
4
cmd/cert-bundler/testdata/pkg/bundle.sha256
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
5ed8bf9ed693045faa8a5cb0edc4a870052e56aef6291ce8b1604565affbc2a4 core_certs.zip
|
||||||
|
e59eddc590d2f7b790a87c5b56e81697088ab54be382c0e2c51b82034006d308 core_certs.tgz
|
||||||
|
51b9b63b1335118079e90700a3a5b847c363808e9116e576ca84f301bc433289 google_certs.tgz
|
||||||
|
3d1910ca8835c3ded1755a8c7d6c48083c2f3ff68b2bfbf932aaf27e29d0a232 lets_encrypt.zip
|
||||||
BIN
cmd/cert-bundler/testdata/pkg/core_certs.tgz
vendored
Normal file
BIN
cmd/cert-bundler/testdata/pkg/core_certs.tgz
vendored
Normal file
Binary file not shown.
BIN
cmd/cert-bundler/testdata/pkg/core_certs.zip
vendored
Normal file
BIN
cmd/cert-bundler/testdata/pkg/core_certs.zip
vendored
Normal file
Binary file not shown.
BIN
cmd/cert-bundler/testdata/pkg/google_certs.tgz
vendored
Normal file
BIN
cmd/cert-bundler/testdata/pkg/google_certs.tgz
vendored
Normal file
Binary file not shown.
BIN
cmd/cert-bundler/testdata/pkg/lets_encrypt.zip
vendored
Normal file
BIN
cmd/cert-bundler/testdata/pkg/lets_encrypt.zip
vendored
Normal file
Binary file not shown.
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
|
||||||
|
}
|
||||||
@@ -110,6 +110,14 @@ func showBasicConstraints(cert *x509.Certificate) {
|
|||||||
|
|
||||||
if cert.IsCA {
|
if cert.IsCA {
|
||||||
fmt.Printf(", is a CA certificate")
|
fmt.Printf(", is a CA certificate")
|
||||||
|
if !cert.BasicConstraintsValid {
|
||||||
|
fmt.Printf(" (basic constraint failure)")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Printf("is not a CA certificate")
|
||||||
|
if cert.KeyUsage&x509.KeyUsageKeyEncipherment != 0 {
|
||||||
|
fmt.Printf(" (key encipherment usage enabled!)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cert.MaxPathLen == 0 && cert.MaxPathLenZero) || (cert.MaxPathLen > 0) {
|
if (cert.MaxPathLen == 0 && cert.MaxPathLenZero) || (cert.MaxPathLen > 0) {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ func main() {
|
|||||||
|
|
||||||
for _, arg := range flag.Args() {
|
for _, arg := range flag.Args() {
|
||||||
if err := lookupHost(arg); err != nil {
|
if err := lookupHost(arg); err != nil {
|
||||||
log.Println("%s: %s", arg, err)
|
log.Printf("%s: %s", arg, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3
cmd/minmax/README
Normal file
3
cmd/minmax/README
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
minmax
|
||||||
|
|
||||||
|
A quick tool to calculate minmax codes if needed for uLisp.
|
||||||
53
cmd/minmax/minmax.go
Normal file
53
cmd/minmax/minmax.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
var kinds = map[string]int{
|
||||||
|
"sym": 0,
|
||||||
|
"tf": 1,
|
||||||
|
"fn": 2,
|
||||||
|
"sp": 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
func dieIf(err error) {
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Fprintf(os.Stderr, "[!] %s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func usage() {
|
||||||
|
fmt.Fprintf(os.Stderr, "usage: minmax type min max\n")
|
||||||
|
fmt.Fprintf(os.Stderr, " type is one of fn, sp, sym, tf\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if flag.NArg() != 3 {
|
||||||
|
usage()
|
||||||
|
}
|
||||||
|
|
||||||
|
kind, ok := kinds[flag.Arg(0)]
|
||||||
|
if !ok {
|
||||||
|
usage()
|
||||||
|
}
|
||||||
|
|
||||||
|
min, err := strconv.Atoi(flag.Arg(1))
|
||||||
|
dieIf(err)
|
||||||
|
|
||||||
|
max, err := strconv.Atoi(flag.Arg(2))
|
||||||
|
dieIf(err)
|
||||||
|
|
||||||
|
code := kind << 6
|
||||||
|
code += (min << 3)
|
||||||
|
code += max
|
||||||
|
fmt.Printf("%0o\n", code)
|
||||||
|
}
|
||||||
48
cmd/rolldie/main.go
Normal file
48
cmd/rolldie/main.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/kyle/goutils/die"
|
||||||
|
)
|
||||||
|
|
||||||
|
var dieRollFormat = regexp.MustCompile(`^(\d+)[dD](\d+)$`)
|
||||||
|
|
||||||
|
func rollDie(count, sides int) []int {
|
||||||
|
sum := 0
|
||||||
|
var rolls []int
|
||||||
|
|
||||||
|
for i := 0; i < count; i++ {
|
||||||
|
roll := rand.Intn(sides) + 1
|
||||||
|
sum += roll
|
||||||
|
rolls = append(rolls, roll)
|
||||||
|
}
|
||||||
|
|
||||||
|
rolls = append(rolls, sum)
|
||||||
|
return rolls
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
for _, arg := range flag.Args() {
|
||||||
|
if !dieRollFormat.MatchString(arg) {
|
||||||
|
fmt.Fprintf(os.Stderr, "invalid die format %s: should be XdY\n", arg)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
dieRoll := dieRollFormat.FindAllStringSubmatch(arg, -1)
|
||||||
|
count, err := strconv.Atoi(dieRoll[0][1])
|
||||||
|
die.If(err)
|
||||||
|
|
||||||
|
sides, err := strconv.Atoi(dieRoll[0][2])
|
||||||
|
die.If(err)
|
||||||
|
|
||||||
|
fmt.Println(rollDie(count, sides))
|
||||||
|
}
|
||||||
|
}
|
||||||
34
cmd/tlsinfo/README.txt
Normal file
34
cmd/tlsinfo/README.txt
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
tlsinfo: show TLS version, cipher, and peer certificates
|
||||||
|
---------------------------------------------------------
|
||||||
|
|
||||||
|
Description
|
||||||
|
tlsinfo connects to a TLS server and prints the negotiated TLS version and
|
||||||
|
cipher suite, followed by details for each certificate in the server’s
|
||||||
|
presented chain (as provided by the server).
|
||||||
|
|
||||||
|
Usage
|
||||||
|
tlsinfo <hostname:port>
|
||||||
|
|
||||||
|
Output
|
||||||
|
The program prints the negotiated protocol and cipher, then one section per
|
||||||
|
certificate in the order received from the server. Example fields:
|
||||||
|
TLS Version: TLS 1.3
|
||||||
|
Cipher Suite: TLS_AES_128_GCM_SHA256
|
||||||
|
Certificate 1
|
||||||
|
Subject: CN=example.com, O=Example Corp, C=US
|
||||||
|
Issuer: CN=Example Root CA, O=Example Corp, C=US
|
||||||
|
DNS Names: [example.com www.example.com]
|
||||||
|
Not Before: 2025-01-01 00:00:00 +0000 UTC
|
||||||
|
Not After: 2026-01-01 23:59:59 +0000 UTC
|
||||||
|
|
||||||
|
Examples
|
||||||
|
# Inspect a public HTTPS endpoint
|
||||||
|
tlsinfo example.com:443
|
||||||
|
|
||||||
|
Notes
|
||||||
|
- Verification is intentionally disabled (InsecureSkipVerify=true). The tool
|
||||||
|
does not validate the server certificate or hostname; it is for inspection
|
||||||
|
only.
|
||||||
|
- The SNI/ServerName is inferred from <hostname> when applicable.
|
||||||
|
- You must specify a port (e.g., 443 for HTTPS).
|
||||||
|
- The entire certificate chain is printed exactly as presented by the server.
|
||||||
64
cmd/tlsinfo/main.go
Normal file
64
cmd/tlsinfo/main.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) != 2 {
|
||||||
|
fmt.Printf("Usage: %s ‹hostname:port>\n", os.Args[0])
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
hostPort := os.Args[1]
|
||||||
|
conn, err := tls.Dial("tcp", hostPort, &tls.Config{
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Failed to connect to the TLS server: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
state := conn.ConnectionState()
|
||||||
|
printConnectionDetails(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printConnectionDetails(state tls.ConnectionState) {
|
||||||
|
version := tlsVersion(state.Version)
|
||||||
|
cipherSuite := tls.CipherSuiteName(state.CipherSuite)
|
||||||
|
fmt.Printf("TLS Version: %s\n", version)
|
||||||
|
fmt.Printf("Cipher Suite: %s\n", cipherSuite)
|
||||||
|
printPeerCertificates(state.PeerCertificates)
|
||||||
|
}
|
||||||
|
|
||||||
|
func tlsVersion(version uint16) string {
|
||||||
|
switch version {
|
||||||
|
|
||||||
|
case tls.VersionTLS13:
|
||||||
|
return "TLS 1.3"
|
||||||
|
case tls.VersionTLS12:
|
||||||
|
|
||||||
|
return "TLS 1.2"
|
||||||
|
case tls.VersionTLS11:
|
||||||
|
return "TLS 1.1"
|
||||||
|
case tls.VersionTLS10:
|
||||||
|
return "TLS 1.0"
|
||||||
|
default:
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printPeerCertificates(certificates []*x509.Certificate) {
|
||||||
|
for i, cert := range certificates {
|
||||||
|
fmt.Printf("Certificate %d\n", i+1)
|
||||||
|
fmt.Printf("\tSubject: %s\n", cert.Subject)
|
||||||
|
fmt.Printf("\tIssuer: %s\n", cert.Issuer)
|
||||||
|
fmt.Printf("\tDNS Names: %v\n", cert.DNSNames)
|
||||||
|
fmt.Printf("\tNot Before: %s\n:", cert.NotBefore)
|
||||||
|
fmt.Printf("\tNot After: %s\n", cert.NotAfter)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -68,7 +68,7 @@ func showFile(path string) {
|
|||||||
func searchFile(path string, search *regexp.Regexp) error {
|
func searchFile(path string, search *regexp.Regexp) error {
|
||||||
file, err := os.Open(path)
|
file, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorf("%v")
|
errorf("%v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|||||||
@@ -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,67 +42,94 @@ 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
|
break
|
||||||
} else if !cfg.SectionInConfig(section) {
|
}
|
||||||
|
}
|
||||||
|
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)
|
cfg[section] = make(map[string]string, 0)
|
||||||
}
|
}
|
||||||
currentSection = section
|
return section, nil
|
||||||
} else if configLine.MatchString(line) {
|
}
|
||||||
|
|
||||||
|
// handleConfigLine processes a key=value configuration line.
|
||||||
|
func handleConfigLine(cfg ConfigMap, line string, currentSection string) (string, error) {
|
||||||
regex := configLine
|
regex := configLine
|
||||||
if quotedConfigLine.MatchString(line) {
|
if quotedConfigLine.MatchString(line) {
|
||||||
regex = quotedConfigLine
|
regex = quotedConfigLine
|
||||||
}
|
}
|
||||||
|
|
||||||
if currentSection == "" {
|
if currentSection == "" {
|
||||||
currentSection = DefaultSection
|
currentSection = DefaultSection
|
||||||
if !cfg.SectionInConfig(currentSection) {
|
if !cfg.SectionInConfig(currentSection) {
|
||||||
cfg[currentSection] = map[string]string{}
|
cfg[currentSection] = map[string]string{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
key := regex.ReplaceAllString(line, "$1")
|
key := regex.ReplaceAllString(line, "$1")
|
||||||
val := regex.ReplaceAllString(line, "$2")
|
val := regex.ReplaceAllString(line, "$2")
|
||||||
if key == "" {
|
if key != "" {
|
||||||
continue
|
|
||||||
}
|
|
||||||
cfg[currentSection][key] = val
|
cfg[currentSection][key] = val
|
||||||
} else {
|
|
||||||
err = fmt.Errorf("invalid config file")
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return
|
return currentSection, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SectionInConfig determines whether a section is in the configuration.
|
// SectionInConfig determines whether a section is in the configuration.
|
||||||
|
|||||||
Reference in New Issue
Block a user