Compare commits

...

22 Commits

Author SHA1 Message Date
1d32a64dc0 add cert-revcheck 2025-11-14 22:56:10 -08:00
d70ca5ee87 adding golangci-lint 2025-11-14 22:54:06 -08:00
eca3a229a4 config: golangci-lint cleanups. 2025-11-14 22:53:02 -08:00
4c1eb03671 Cleaning up golangci-lint warnings. 2025-11-14 22:49:54 -08:00
f463eeed88 minor updates 2025-11-14 22:01:12 -08:00
289c9d2343 cmd: add cert-bundler
Builds certificate bundles.
2025-11-14 21:59:03 -08:00
e375963243 cmd: add tlsinfo. 2025-11-14 15:21:38 -08:00
31baa10b3b Update README. 2025-11-14 14:55:16 -08:00
0556c7c56d ca-signed: note self-signed certs. 2025-11-14 14:49:46 -08:00
83c95d9db8 cmd: add ca-signed tool.
Verify certificates are signed by a CA.
2025-11-14 14:48:34 -08:00
beccb551e2 add minmax 2025-04-10 01:17:11 -07:00
c761d98b82 additional debugging for basic constraints 2024-08-22 18:06:09 -07:00
e68d22337b cmd: add die roller 2024-06-14 20:27:00 -07:00
4cb6f5b6f0 fix minor issues in Print calls. 2024-05-19 20:51:35 -07:00
6d5708800f circleci: Some days I really hate computers. 2024-05-19 20:48:59 -07:00
fa3eb821e6 circleci: Update go container. 2024-05-19 20:48:25 -07:00
dd5ed403b9 circleci: Update go version. 2024-05-19 20:46:28 -07:00
b4fde22c31 circleci: don't use bazel 2024-05-19 20:45:32 -07:00
9715293773 Add zsearch and dumpbytes. 2024-05-19 20:41:50 -07:00
f6d227946b Get rid of bazel.
Good riddance. More of a headache than it's worth.
2024-05-19 20:24:38 -07:00
6f7a8fa4d4 Add host tool. 2024-04-26 14:15:08 -07:00
622f6a2638 seekbuf: add Bytes method. 2023-08-27 20:56:47 -07:00
93 changed files with 2369 additions and 1454 deletions

View File

@@ -2,22 +2,6 @@
# See: https://circleci.com/docs/2.0/configuration-reference
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.
# See: https://circleci.com/docs/2.0/configuration-reference/#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.
# See: https://circleci.com/docs/2.0/configuration-reference/#docker-machine-macos-windows-executor
docker:
- image: circleci/golang:1.15.8
- image: cimg/go:1.22.2
# Add steps to the job
# See: https://circleci.com/docs/2.0/configuration-reference/#steps
steps:
- checkout
- setup-bazel
- restore_cache:
keys:
- go-mod-v4-{{ checksum "go.sum" }}
@@ -44,10 +27,10 @@ jobs:
- "/go/pkg/mod"
- run:
name: Run tests
command: bazel test //...
command: go test ./...
- run:
name: Run build
command: bazel build //...
command: go build ./...
- store_test_results:
path: /tmp/test-reports

87
.golangci.yml Normal file
View 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

View File

@@ -1,22 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
load("@bazel_gazelle//:def.bzl", "gazelle")
# gazelle:prefix git.wntrmute.dev/kyle/goutils
gazelle(name = "gazelle")
go_library(
name = "goutils",
srcs = ["doc.go"],
importpath = "git.wntrmute.dev/kyle/goutils",
visibility = ["//visibility:public"],
)
gazelle(
name = "gazelle-update-repos",
args = [
"-from_file=go.mod",
"-to_macro=deps.bzl%go_dependencies",
"-prune",
],
command = "update-repos",
)

View File

@@ -1,32 +0,0 @@
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
### Go tooling, including Gazelle to generate and maintain BUILD files.
http_archive(
name = "io_bazel_rules_go",
sha256 = "6b65cb7917b4d1709f9410ffe00ecf3e160edf674b78c54a894471320862184f",
urls = [
"https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.39.0/rules_go-v0.39.0.zip",
"https://github.com/bazelbuild/rules_go/releases/download/v0.39.0/rules_go-v0.39.0.zip",
],
)
http_archive(
name = "bazel_gazelle",
sha256 = "ecba0f04f96b4960a5b250c8e8eeec42281035970aa8852dda73098274d14a1d",
urls = [
"https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.29.0/bazel-gazelle-v0.29.0.tar.gz",
"https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.29.0/bazel-gazelle-v0.29.0.tar.gz",
],
)
load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies")
load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies")
load("//:deps.bzl", "go_dependencies")
# gazelle:repository_macro deps.bzl%go_dependencies
go_dependencies()
go_rules_dependencies()
go_register_toolchains(version = "1.20.4")
gazelle_dependencies()

View File

@@ -1,24 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "ahash",
srcs = ["ahash.go"],
importpath = "git.wntrmute.dev/kyle/goutils/ahash",
visibility = ["//visibility:public"],
deps = [
"//assert",
"@org_golang_x_crypto//blake2b",
"@org_golang_x_crypto//blake2s",
"@org_golang_x_crypto//md4",
"@org_golang_x_crypto//ripemd160",
"@org_golang_x_crypto//sha3",
],
)
go_test(
name = "ahash_test",
size = "small",
srcs = ["ahash_test.go"],
embed = [":ahash"],
deps = ["//assert"],
)

View File

@@ -1,8 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "assert",
srcs = ["assert.go"],
importpath = "git.wntrmute.dev/kyle/goutils/assert",
visibility = ["//visibility:public"],
)

View File

@@ -1,30 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "certlib",
srcs = [
"certlib.go",
"der_helpers.go",
"ed25519.go",
"helpers.go",
],
importpath = "git.wntrmute.dev/kyle/goutils/certlib",
visibility = ["//visibility:public"],
deps = [
"//certlib/certerr",
"//certlib/pkcs7",
"@com_github_google_certificate_transparency_go//:certificate-transparency-go",
"@com_github_google_certificate_transparency_go//tls",
"@com_github_google_certificate_transparency_go//x509",
"@org_golang_x_crypto//ocsp",
"@org_golang_x_crypto//pkcs12",
],
)
go_test(
name = "certlib_test",
size = "small",
srcs = ["certlib_test.go"],
embed = [":certlib"],
deps = ["//assert"],
)

View File

@@ -1,8 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "certerr",
srcs = ["errors.go"],
importpath = "git.wntrmute.dev/kyle/goutils/certlib/certerr",
visibility = ["//visibility:public"],
)

82
certlib/hosts/hosts.go Normal file
View 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
}

View File

@@ -1,9 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "pkcs7",
srcs = ["pkcs7.go"],
importpath = "git.wntrmute.dev/kyle/goutils/certlib/pkcs7",
visibility = ["//visibility:public"],
deps = ["//certlib/certerr"],
)

View File

@@ -1,19 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "revoke",
srcs = ["revoke.go"],
importpath = "git.wntrmute.dev/kyle/goutils/certlib/revoke",
visibility = ["//visibility:public"],
deps = [
"//certlib",
"//log",
"@org_golang_x_crypto//ocsp",
],
)
go_test(
name = "revoke_test",
srcs = ["revoke_test.go"],
embed = [":revoke"],
)

View File

@@ -1,14 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "atping_lib",
srcs = ["main.go"],
importpath = "git.wntrmute.dev/kyle/goutils/cmd/atping",
visibility = ["//visibility:private"],
)
go_binary(
name = "atping",
embed = [":atping_lib"],
visibility = ["//visibility:public"],
)

40
cmd/ca-signed/README.txt Normal file
View 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 certificates 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 leafs 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
View 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
View File

@@ -0,0 +1,29 @@
-----BEGIN CERTIFICATE-----
MIIFCzCCAvOgAwIBAgIQf/AFoHxM3tEArZ1mpRB7mDANBgkqhkiG9w0BAQsFADBH
MQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExM
QzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMjMxMjEzMDkwMDAwWhcNMjkwMjIw
MTQwMDAwWjA7MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVR29vZ2xlIFRydXN0IFNl
cnZpY2VzMQwwCgYDVQQDEwNXUjIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
AoIBAQCp/5x/RR5wqFOfytnlDd5GV1d9vI+aWqxG8YSau5HbyfsvAfuSCQAWXqAc
+MGr+XgvSszYhaLYWTwO0xj7sfUkDSbutltkdnwUxy96zqhMt/TZCPzfhyM1IKji
aeKMTj+xWfpgoh6zySBTGYLKNlNtYE3pAJH8do1cCA8Kwtzxc2vFE24KT3rC8gIc
LrRjg9ox9i11MLL7q8Ju26nADrn5Z9TDJVd06wW06Y613ijNzHoU5HEDy01hLmFX
xRmpC5iEGuh5KdmyjS//V2pm4M6rlagplmNwEmceOuHbsCFx13ye/aoXbv4r+zgX
FNFmp6+atXDMyGOBOozAKql2N87jAgMBAAGjgf4wgfswDgYDVR0PAQH/BAQDAgGG
MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjASBgNVHRMBAf8ECDAGAQH/
AgEAMB0GA1UdDgQWBBTeGx7teRXUPjckwyG77DQ5bUKyMDAfBgNVHSMEGDAWgBTk
rysmcRorSCeFL1JmLO/wiRNxPjA0BggrBgEFBQcBAQQoMCYwJAYIKwYBBQUHMAKG
GGh0dHA6Ly9pLnBraS5nb29nL3IxLmNydDArBgNVHR8EJDAiMCCgHqAchhpodHRw
Oi8vYy5wa2kuZ29vZy9yL3IxLmNybDATBgNVHSAEDDAKMAgGBmeBDAECATANBgkq
hkiG9w0BAQsFAAOCAgEARXWL5R87RBOWGqtY8TXJbz3S0DNKhjO6V1FP7sQ02hYS
TL8Tnw3UVOlIecAwPJQl8hr0ujKUtjNyC4XuCRElNJThb0Lbgpt7fyqaqf9/qdLe
SiDLs/sDA7j4BwXaWZIvGEaYzq9yviQmsR4ATb0IrZNBRAq7x9UBhb+TV+PfdBJT
DhEl05vc3ssnbrPCuTNiOcLgNeFbpwkuGcuRKnZc8d/KI4RApW//mkHgte8y0YWu
ryUJ8GLFbsLIbjL9uNrizkqRSvOFVU6xddZIMy9vhNkSXJ/UcZhjJY1pXAprffJB
vei7j+Qi151lRehMCofa6WBmiA4fx+FOVsV2/7R6V2nyAiIJJkEd2nSi5SnzxJrl
Xdaqev3htytmOPvoKWa676ATL/hzfvDaQBEcXd2Ppvy+275W+DKcH0FBbX62xevG
iza3F4ydzxl6NJ8hk8R+dDXSqv1MbRT1ybB5W0k8878XSOjvmiYTDIfyc9acxVJr
Y/cykHipa+te1pOhv7wYPYtZ9orGBV5SGOJm4NrB3K1aJar0RfzxC3ikr7Dyc6Qw
qDTBU39CluVIQeuQRgwG3MuSxl7zRERDRilGoKb8uY45JzmxWuKxrfwT/478JuHU
/oTxUFqOl2stKnn7QGTq8z29W+GgBLCXSBxC9epaHM0myFH/FJlniXJfHeytWt0=
-----END CERTIFICATE-----

31
cmd/ca-signed/testdata/gts-r1.pem vendored Normal file
View File

@@ -0,0 +1,31 @@
-----BEGIN CERTIFICATE-----
MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQsw
CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU
MBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw
MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp
Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0GCSqGSIb3DQEBAQUA
A4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaMf/vo
27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7w
Cl7raKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7zUjw
TcLCeoiKu7rPWRnWr4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0Pfybl
qAj+lug8aJRT7oM6iCsVlgmy4HqMLnXWnOunVmSPlk9orj2XwoSPwLxAwAtcvfaH
szVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk9+aCEI3oncKKiPo4Zor8
Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zqkUspzBmk
MiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOORc92
wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70p
aDPvOmbsB4om3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrN
VjzRlwW5y0vtOUucxD/SVRNuJLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgFlQID
AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E
FgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQADggIBAJ+qQibb
C5u+/x6Wki4+omVKapi6Ist9wTrYggoGxval3sBOh2Z5ofmmWJyq+bXmYOfg6LEe
QkEzCzc9zolwFcq1JKjPa7XSQCGYzyI0zzvFIoTgxQ6KfF2I5DUkzps+GlQebtuy
h6f88/qBVRRiClmpIgUxPoLW7ttXNLwzldMXG+gnoot7TiYaelpkttGsN/H9oPM4
7HLwEXWdyzRSjeZ2axfG34arJ45JK3VmgRAhpuo+9K4l/3wV3s6MJT/KYnAK9y8J
ZgfIPxz88NtFMN9iiMG1D53Dn0reWVlHxYciNuaCp+0KueIHoI17eko8cdLiA6Ef
MgfdG+RCzgwARWGAtQsgWSl4vflVy2PFPEz0tv/bal8xa5meLMFrUKTX5hgUvYU/
Z6tGn6D/Qqc6f1zLXbBwHSs09dR2CQzreExZBfMzQsNhFRAbd03OIozUhfJFfbdT
6u9AWpQKXCBfTkBdYiJ23//OYb2MI3jSNwLgjt7RETeJ9r/tSQdirpLsQBqvFAnZ
0E6yove+7u7Y/9waLd64NnHi/Hm3lCXRSHNboTXns5lndcEZOitHTtNCjv0xyBZm
2tIMPNuzjsmhDYAPexZ3FL//2wmUspO8IFgV6dtxQ/PeEMMA3KgqlbbC1j+Qa3bb
bP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3gm3c
-----END CERTIFICATE-----

31
cmd/ca-signed/testdata/isrg-root-x1.pem vendored Normal file
View File

@@ -0,0 +1,31 @@
-----BEGIN CERTIFICATE-----
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
-----END CERTIFICATE-----

26
cmd/ca-signed/testdata/le-e7.pem vendored Normal file
View 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
View 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
View 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
View 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
View 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

View File

@@ -0,0 +1,29 @@
-----BEGIN CERTIFICATE-----
MIIFCzCCAvOgAwIBAgIQf/AFoHxM3tEArZ1mpRB7mDANBgkqhkiG9w0BAQsFADBH
MQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExM
QzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMjMxMjEzMDkwMDAwWhcNMjkwMjIw
MTQwMDAwWjA7MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVR29vZ2xlIFRydXN0IFNl
cnZpY2VzMQwwCgYDVQQDEwNXUjIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
AoIBAQCp/5x/RR5wqFOfytnlDd5GV1d9vI+aWqxG8YSau5HbyfsvAfuSCQAWXqAc
+MGr+XgvSszYhaLYWTwO0xj7sfUkDSbutltkdnwUxy96zqhMt/TZCPzfhyM1IKji
aeKMTj+xWfpgoh6zySBTGYLKNlNtYE3pAJH8do1cCA8Kwtzxc2vFE24KT3rC8gIc
LrRjg9ox9i11MLL7q8Ju26nADrn5Z9TDJVd06wW06Y613ijNzHoU5HEDy01hLmFX
xRmpC5iEGuh5KdmyjS//V2pm4M6rlagplmNwEmceOuHbsCFx13ye/aoXbv4r+zgX
FNFmp6+atXDMyGOBOozAKql2N87jAgMBAAGjgf4wgfswDgYDVR0PAQH/BAQDAgGG
MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjASBgNVHRMBAf8ECDAGAQH/
AgEAMB0GA1UdDgQWBBTeGx7teRXUPjckwyG77DQ5bUKyMDAfBgNVHSMEGDAWgBTk
rysmcRorSCeFL1JmLO/wiRNxPjA0BggrBgEFBQcBAQQoMCYwJAYIKwYBBQUHMAKG
GGh0dHA6Ly9pLnBraS5nb29nL3IxLmNydDArBgNVHR8EJDAiMCCgHqAchhpodHRw
Oi8vYy5wa2kuZ29vZy9yL3IxLmNybDATBgNVHSAEDDAKMAgGBmeBDAECATANBgkq
hkiG9w0BAQsFAAOCAgEARXWL5R87RBOWGqtY8TXJbz3S0DNKhjO6V1FP7sQ02hYS
TL8Tnw3UVOlIecAwPJQl8hr0ujKUtjNyC4XuCRElNJThb0Lbgpt7fyqaqf9/qdLe
SiDLs/sDA7j4BwXaWZIvGEaYzq9yviQmsR4ATb0IrZNBRAq7x9UBhb+TV+PfdBJT
DhEl05vc3ssnbrPCuTNiOcLgNeFbpwkuGcuRKnZc8d/KI4RApW//mkHgte8y0YWu
ryUJ8GLFbsLIbjL9uNrizkqRSvOFVU6xddZIMy9vhNkSXJ/UcZhjJY1pXAprffJB
vei7j+Qi151lRehMCofa6WBmiA4fx+FOVsV2/7R6V2nyAiIJJkEd2nSi5SnzxJrl
Xdaqev3htytmOPvoKWa676ATL/hzfvDaQBEcXd2Ppvy+275W+DKcH0FBbX62xevG
iza3F4ydzxl6NJ8hk8R+dDXSqv1MbRT1ybB5W0k8878XSOjvmiYTDIfyc9acxVJr
Y/cykHipa+te1pOhv7wYPYtZ9orGBV5SGOJm4NrB3K1aJar0RfzxC3ikr7Dyc6Qw
qDTBU39CluVIQeuQRgwG3MuSxl7zRERDRilGoKb8uY45JzmxWuKxrfwT/478JuHU
/oTxUFqOl2stKnn7QGTq8z29W+GgBLCXSBxC9epaHM0myFH/FJlniXJfHeytWt0=
-----END CERTIFICATE-----

View File

@@ -0,0 +1,31 @@
-----BEGIN CERTIFICATE-----
MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQsw
CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU
MBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw
MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp
Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0GCSqGSIb3DQEBAQUA
A4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaMf/vo
27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7w
Cl7raKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7zUjw
TcLCeoiKu7rPWRnWr4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0Pfybl
qAj+lug8aJRT7oM6iCsVlgmy4HqMLnXWnOunVmSPlk9orj2XwoSPwLxAwAtcvfaH
szVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk9+aCEI3oncKKiPo4Zor8
Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zqkUspzBmk
MiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOORc92
wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70p
aDPvOmbsB4om3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrN
VjzRlwW5y0vtOUucxD/SVRNuJLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgFlQID
AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E
FgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQADggIBAJ+qQibb
C5u+/x6Wki4+omVKapi6Ist9wTrYggoGxval3sBOh2Z5ofmmWJyq+bXmYOfg6LEe
QkEzCzc9zolwFcq1JKjPa7XSQCGYzyI0zzvFIoTgxQ6KfF2I5DUkzps+GlQebtuy
h6f88/qBVRRiClmpIgUxPoLW7ttXNLwzldMXG+gnoot7TiYaelpkttGsN/H9oPM4
7HLwEXWdyzRSjeZ2axfG34arJ45JK3VmgRAhpuo+9K4l/3wV3s6MJT/KYnAK9y8J
ZgfIPxz88NtFMN9iiMG1D53Dn0reWVlHxYciNuaCp+0KueIHoI17eko8cdLiA6Ef
MgfdG+RCzgwARWGAtQsgWSl4vflVy2PFPEz0tv/bal8xa5meLMFrUKTX5hgUvYU/
Z6tGn6D/Qqc6f1zLXbBwHSs09dR2CQzreExZBfMzQsNhFRAbd03OIozUhfJFfbdT
6u9AWpQKXCBfTkBdYiJ23//OYb2MI3jSNwLgjt7RETeJ9r/tSQdirpLsQBqvFAnZ
0E6yove+7u7Y/9waLd64NnHi/Hm3lCXRSHNboTXns5lndcEZOitHTtNCjv0xyBZm
2tIMPNuzjsmhDYAPexZ3FL//2wmUspO8IFgV6dtxQ/PeEMMA3KgqlbbC1j+Qa3bb
bP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3gm3c
-----END CERTIFICATE-----

View File

@@ -0,0 +1,31 @@
-----BEGIN CERTIFICATE-----
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
-----END CERTIFICATE-----

View 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-----

View File

@@ -0,0 +1,4 @@
5ed8bf9ed693045faa8a5cb0edc4a870052e56aef6291ce8b1604565affbc2a4 core_certs.zip
e59eddc590d2f7b790a87c5b56e81697088ab54be382c0e2c51b82034006d308 core_certs.tgz
51b9b63b1335118079e90700a3a5b847c363808e9116e576ca84f301bc433289 google_certs.tgz
3d1910ca8835c3ded1755a8c7d6c48083c2f3ff68b2bfbf932aaf27e29d0a232 lets_encrypt.zip

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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
View 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
}

View File

@@ -1,15 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "certchain_lib",
srcs = ["certchain.go"],
importpath = "git.wntrmute.dev/kyle/goutils/cmd/certchain",
visibility = ["//visibility:private"],
deps = ["//die"],
)
go_binary(
name = "certchain",
embed = [":certchain_lib"],
visibility = ["//visibility:public"],
)

View File

@@ -1,22 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "certdump_lib",
srcs = [
"certdump.go",
"util.go",
],
importpath = "git.wntrmute.dev/kyle/goutils/cmd/certdump",
visibility = ["//visibility:private"],
deps = [
"//certlib",
"//lib",
"@com_github_kr_text//:text",
],
)
go_binary(
name = "certdump",
embed = [":certdump_lib"],
visibility = ["//visibility:public"],
)

View File

@@ -110,6 +110,14 @@ func showBasicConstraints(cert *x509.Certificate) {
if cert.IsCA {
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) {

View File

@@ -1,19 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "certexpiry_lib",
srcs = ["main.go"],
importpath = "git.wntrmute.dev/kyle/goutils/cmd/certexpiry",
visibility = ["//visibility:private"],
deps = [
"//certlib",
"//die",
"//lib",
],
)
go_binary(
name = "certexpiry",
embed = [":certexpiry_lib"],
visibility = ["//visibility:public"],
)

View File

@@ -1,20 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "certverify_lib",
srcs = ["main.go"],
importpath = "git.wntrmute.dev/kyle/goutils/cmd/certverify",
visibility = ["//visibility:private"],
deps = [
"//certlib",
"//certlib/revoke",
"//die",
"//lib",
],
)
go_binary(
name = "certverify",
embed = [":certverify_lib"],
visibility = ["//visibility:public"],
)

View File

@@ -1,20 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "clustersh_lib",
srcs = ["main.go"],
importpath = "git.wntrmute.dev/kyle/goutils/cmd/clustersh",
visibility = ["//visibility:private"],
deps = [
"//lib",
"@com_github_pkg_sftp//:sftp",
"@org_golang_x_crypto//ssh",
"@org_golang_x_crypto//ssh/agent",
],
)
go_binary(
name = "clustersh",
embed = [":clustersh_lib"],
visibility = ["//visibility:public"],
)

View File

@@ -1,18 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "cruntar_lib",
srcs = ["main.go"],
importpath = "git.wntrmute.dev/kyle/goutils/cmd/cruntar",
visibility = ["//visibility:private"],
deps = [
"//die",
"//fileutil",
],
)
go_binary(
name = "cruntar",
embed = [":cruntar_lib"],
visibility = ["//visibility:public"],
)

View File

@@ -1,15 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "csrpubdump_lib",
srcs = ["pubdump.go"],
importpath = "git.wntrmute.dev/kyle/goutils/cmd/csrpubdump",
visibility = ["//visibility:private"],
deps = ["//die"],
)
go_binary(
name = "csrpubdump",
embed = [":csrpubdump_lib"],
visibility = ["//visibility:public"],
)

View File

@@ -1,19 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "diskimg_lib",
srcs = ["main.go"],
importpath = "git.wntrmute.dev/kyle/goutils/cmd/diskimg",
visibility = ["//visibility:private"],
deps = [
"//ahash",
"//dbg",
"//die",
],
)
go_binary(
name = "diskimg",
embed = [":diskimg_lib"],
visibility = ["//visibility:public"],
)

71
cmd/dumpbytes/main.go Normal file
View File

@@ -0,0 +1,71 @@
package main
import (
"flag"
"fmt"
"git.wntrmute.dev/kyle/goutils/die"
"io"
"os"
)
func usage(w io.Writer, exc int) {
fmt.Fprintln(w, `usage: dumpbytes <file>`)
os.Exit(exc)
}
func printBytes(buf []byte) {
fmt.Printf("\t")
for i := 0; i < len(buf); i++ {
fmt.Printf("0x%02x, ", buf[i])
}
fmt.Println()
}
func dumpFile(path string, indentLevel int) error {
indent := ""
for i := 0; i < indentLevel; i++ {
indent += "\t"
}
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
fmt.Printf("%svar buffer = []byte{\n", indent)
for {
buf := make([]byte, 8)
n, err := file.Read(buf)
if err == io.EOF {
if n > 0 {
fmt.Printf("%s", indent)
printBytes(buf[:n])
}
break
}
if err != nil {
return err
}
fmt.Printf("%s", indent)
printBytes(buf[:n])
}
fmt.Printf("%s}\n", indent)
return nil
}
func main() {
indent := 0
flag.Usage = func() { usage(os.Stderr, 0) }
flag.IntVar(&indent, "n", 0, "indent level")
flag.Parse()
for _, file := range flag.Args() {
err := dumpFile(file, indent)
die.If(err)
}
}

View File

@@ -1,15 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "eig_lib",
srcs = ["main.go"],
importpath = "git.wntrmute.dev/kyle/goutils/cmd/eig",
visibility = ["//visibility:private"],
deps = ["//die"],
)
go_binary(
name = "eig",
embed = [":eig_lib"],
visibility = ["//visibility:public"],
)

View File

@@ -1,15 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "fragment_lib",
srcs = ["fragment.go"],
importpath = "git.wntrmute.dev/kyle/goutils/cmd/fragment",
visibility = ["//visibility:private"],
deps = ["//die"],
)
go_binary(
name = "fragment",
embed = [":fragment_lib"],
visibility = ["//visibility:public"],
)

5
cmd/host/README Normal file
View File

@@ -0,0 +1,5 @@
host
This is a utility to display CNAME records and IPs for a hostname. It
was born of my frustration in trying to figure out how to get the host(1)
tool installed on Fedora.

41
cmd/host/main.go Normal file
View File

@@ -0,0 +1,41 @@
package main
import (
"flag"
"fmt"
"log"
"net"
)
func lookupHost(host string) error {
cname, err := net.LookupCNAME(host)
if err != nil {
return err
}
if cname != host {
fmt.Printf("%s is a CNAME for %s\n", host, cname)
host = cname
}
addrs, err := net.LookupHost(host)
if err != nil {
return err
}
for _, addr := range addrs {
fmt.Printf("\t%s\n", addr)
}
return nil
}
func main() {
flag.Parse()
for _, arg := range flag.Args() {
if err := lookupHost(arg); err != nil {
log.Printf("%s: %s", arg, err)
}
}
}

View File

@@ -1,15 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "jlp_lib",
srcs = ["jlp.go"],
importpath = "git.wntrmute.dev/kyle/goutils/cmd/jlp",
visibility = ["//visibility:private"],
deps = ["//lib"],
)
go_binary(
name = "jlp",
embed = [":jlp_lib"],
visibility = ["//visibility:public"],
)

View File

@@ -1,15 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "kgz_lib",
srcs = ["main.go"],
importpath = "git.wntrmute.dev/kyle/goutils/cmd/kgz",
visibility = ["//visibility:private"],
deps = ["@com_github_pkg_errors//:errors"],
)
go_binary(
name = "kgz",
embed = [":kgz_lib"],
visibility = ["//visibility:public"],
)

3
cmd/minmax/README Normal file
View File

@@ -0,0 +1,3 @@
minmax
A quick tool to calculate minmax codes if needed for uLisp.

53
cmd/minmax/minmax.go Normal file
View 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)
}

View File

@@ -1,15 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "parts_lib",
srcs = ["main.go"],
importpath = "git.wntrmute.dev/kyle/goutils/cmd/parts",
visibility = ["//visibility:private"],
deps = ["//die"],
)
go_binary(
name = "parts",
embed = [":parts_lib"],
visibility = ["//visibility:public"],
)

View File

@@ -1,14 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "pem2bin_lib",
srcs = ["main.go"],
importpath = "git.wntrmute.dev/kyle/goutils/cmd/pem2bin",
visibility = ["//visibility:private"],
)
go_binary(
name = "pem2bin",
embed = [":pem2bin_lib"],
visibility = ["//visibility:public"],
)

View File

@@ -1,15 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "pembody_lib",
srcs = ["pembody.go"],
importpath = "git.wntrmute.dev/kyle/goutils/cmd/pembody",
visibility = ["//visibility:private"],
deps = ["//lib"],
)
go_binary(
name = "pembody",
embed = [":pembody_lib"],
visibility = ["//visibility:public"],
)

View File

@@ -1,19 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "pemit_lib",
srcs = ["main.go"],
importpath = "git.wntrmute.dev/kyle/goutils/cmd/pemit",
visibility = ["//visibility:private"],
deps = [
"//assert",
"//die",
"//lib",
],
)
go_binary(
name = "pemit",
embed = [":pemit_lib"],
visibility = ["//visibility:public"],
)

View File

@@ -1,14 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "readchain_lib",
srcs = ["chain.go"],
importpath = "git.wntrmute.dev/kyle/goutils/cmd/readchain",
visibility = ["//visibility:private"],
)
go_binary(
name = "readchain",
embed = [":readchain_lib"],
visibility = ["//visibility:public"],
)

View File

@@ -1,18 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "renfnv_lib",
srcs = ["renfnv.go"],
importpath = "git.wntrmute.dev/kyle/goutils/cmd/renfnv",
visibility = ["//visibility:private"],
deps = [
"//fileutil",
"//lib",
],
)
go_binary(
name = "renfnv",
embed = [":renfnv_lib"],
visibility = ["//visibility:public"],
)

View File

@@ -1,19 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "rhash_lib",
srcs = ["main.go"],
importpath = "git.wntrmute.dev/kyle/goutils/cmd/rhash",
visibility = ["//visibility:private"],
deps = [
"//ahash",
"//die",
"//lib",
],
)
go_binary(
name = "rhash",
embed = [":rhash_lib"],
visibility = ["//visibility:public"],
)

48
cmd/rolldie/main.go Normal file
View 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))
}
}

View File

@@ -1,18 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "showimp_lib",
srcs = ["main.go"],
importpath = "git.wntrmute.dev/kyle/goutils/cmd/showimp",
visibility = ["//visibility:private"],
deps = [
"//dbg",
"//die",
],
)
go_binary(
name = "showimp",
embed = [":showimp_lib"],
visibility = ["//visibility:public"],
)

View File

@@ -1,18 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "ski_lib",
srcs = ["main.go"],
importpath = "git.wntrmute.dev/kyle/goutils/cmd/ski",
visibility = ["//visibility:private"],
deps = [
"//die",
"//lib",
],
)
go_binary(
name = "ski",
embed = [":ski_lib"],
visibility = ["//visibility:public"],
)

View File

@@ -1,15 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "sprox_lib",
srcs = ["main.go"],
importpath = "git.wntrmute.dev/kyle/goutils/cmd/sprox",
visibility = ["//visibility:private"],
deps = ["//die"],
)
go_binary(
name = "sprox",
embed = [":sprox_lib"],
visibility = ["//visibility:public"],
)

View File

@@ -1,15 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "stealchain-server_lib",
srcs = ["main.go"],
importpath = "git.wntrmute.dev/kyle/goutils/cmd/stealchain-server",
visibility = ["//visibility:private"],
deps = ["//die"],
)
go_binary(
name = "stealchain-server",
embed = [":stealchain-server_lib"],
visibility = ["//visibility:public"],
)

View File

@@ -1,15 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "stealchain_lib",
srcs = ["thief.go"],
importpath = "git.wntrmute.dev/kyle/goutils/cmd/stealchain",
visibility = ["//visibility:private"],
deps = ["//die"],
)
go_binary(
name = "stealchain",
embed = [":stealchain_lib"],
visibility = ["//visibility:public"],
)

View File

@@ -1,19 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "subjhash_lib",
srcs = ["main.go"],
importpath = "git.wntrmute.dev/kyle/goutils/cmd/subjhash",
visibility = ["//visibility:private"],
deps = [
"//certlib",
"//die",
"//lib",
],
)
go_binary(
name = "subjhash",
embed = [":subjhash_lib"],
visibility = ["//visibility:public"],
)

34
cmd/tlsinfo/README.txt Normal file
View 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 servers
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
View 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)
}
}

View File

@@ -1,15 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "tlskeypair_lib",
srcs = ["main.go"],
importpath = "git.wntrmute.dev/kyle/goutils/cmd/tlskeypair",
visibility = ["//visibility:private"],
deps = ["//die"],
)
go_binary(
name = "tlskeypair",
embed = [":tlskeypair_lib"],
visibility = ["//visibility:public"],
)

View File

@@ -1,14 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "utc_lib",
srcs = ["main.go"],
importpath = "git.wntrmute.dev/kyle/goutils/cmd/utc",
visibility = ["//visibility:private"],
)
go_binary(
name = "utc",
embed = [":utc_lib"],
visibility = ["//visibility:public"],
)

View File

@@ -1,15 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "yamll_lib",
srcs = ["main.go"],
importpath = "git.wntrmute.dev/kyle/goutils/cmd/yamll",
visibility = ["//visibility:private"],
deps = ["@in_gopkg_yaml_v2//:yaml_v2"],
)
go_binary(
name = "yamll",
embed = [":yamll_lib"],
visibility = ["//visibility:public"],
)

136
cmd/zsearch/main.go Normal file
View File

@@ -0,0 +1,136 @@
// zsearch is a utility for searching zlib-compressed files for a
// search string. It was really designed for use with the Git object
// store, i.e. to aid in the recovery of files after Git does what Git
// do.
package main
import (
"bufio"
"bytes"
"compress/zlib"
"flag"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
)
const defaultDirectory = ".git/objects"
func errorf(format string, a ...interface{}) {
fmt.Fprintf(os.Stderr, format, a...)
if format[len(format)-1] != '\n' {
fmt.Fprintf(os.Stderr, "\n")
}
}
func isDir(path string) bool {
fi, err := os.Stat(path)
if err != nil {
return false
}
return fi.IsDir()
}
func loadFile(path string) ([]byte, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
buf := new(bytes.Buffer)
zread, err := zlib.NewReader(file)
if err != nil {
return nil, err
}
defer zread.Close()
_, err = io.Copy(buf, zread)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func showFile(path string) {
fileData, err := loadFile(path)
if err != nil {
errorf("%v", err)
return
}
fmt.Printf("%s\n", fileData)
}
func searchFile(path string, search *regexp.Regexp) error {
file, err := os.Open(path)
if err != nil {
errorf("%v", err)
return err
}
defer file.Close()
zread, err := zlib.NewReader(file)
if err != nil {
errorf("%v", err)
return err
}
defer zread.Close()
zbuf := bufio.NewReader(zread)
if search.MatchReader(zbuf) {
fileData, err := loadFile(path)
if err != nil {
errorf("%v", err)
return err
}
fmt.Printf("%s:\n%s\n", path, fileData)
}
return nil
}
func buildWalker(searchExpr *regexp.Regexp) filepath.WalkFunc {
return func(path string, info os.FileInfo, err error) error {
if info.Mode().IsRegular() {
return searchFile(path, searchExpr)
}
return nil
}
}
func main() {
flSearch := flag.String("s", "", "search string (should be an RE2 regular expression)")
flag.Parse()
if *flSearch == "" {
for _, path := range flag.Args() {
showFile(path)
}
} else {
search, err := regexp.Compile(*flSearch)
if err != nil {
errorf("Bad regexp: %v", err)
return
}
pathList := flag.Args()
if len(pathList) == 0 {
pathList = []string{defaultDirectory}
}
for _, path := range pathList {
if isDir(path) {
err := filepath.Walk(path, buildWalker(search))
if err != nil {
errorf("%v", err)
return
}
} else {
searchFile(path, search)
}
}
}
}

View File

@@ -1,24 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "config",
srcs = [
"config.go",
"path.go",
"path_linux.go",
],
importpath = "git.wntrmute.dev/kyle/goutils/config",
visibility = ["//visibility:public"],
deps = ["//config/iniconf"],
)
go_test(
name = "config_test",
size = "small",
srcs = [
"config_test.go",
"path_test.go",
],
data = glob(["testdata/**"]),
embed = [":config"],
)

View File

@@ -64,11 +64,7 @@ func LoadFile(path string) error {
addLine(line)
}
if err = scanner.Err(); err != nil {
return err
}
return nil
return scanner.Err()
}
// LoadFileFor scans the ini file at path, loading the default section

View File

@@ -1,16 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "iniconf",
srcs = ["iniconf.go"],
importpath = "git.wntrmute.dev/kyle/goutils/config/iniconf",
visibility = ["//visibility:public"],
)
go_test(
name = "iniconf_test",
size = "small",
srcs = ["iniconf_test.go"],
data = glob(["testdata/**"]),
embed = [":iniconf"],
)

View File

@@ -42,69 +42,96 @@ func ParseReader(r io.Reader) (cfg ConfigMap, err error) {
line string
longLine bool
currentSection string
lineBytes []byte
isPrefix bool
)
for {
err = nil
lineBytes, isPrefix, err = buf.ReadLine()
if io.EOF == err {
line, longLine, err = readConfigLine(buf, line, longLine)
if err == io.EOF {
err = nil
break
} else if err != nil {
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
} else if blankLine.MatchString(line) {
continue
} else if configSection.MatchString(line) {
section := configSection.ReplaceAllString(line,
"$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")
}
currentSection, err = processConfigLine(cfg, line, currentSection)
if err != nil {
break
}
}
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.
func (c ConfigMap) SectionInConfig(section string) bool {
_, ok := c[section]

View File

@@ -1,19 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "dbg",
srcs = ["dbg.go"],
importpath = "git.wntrmute.dev/kyle/goutils/dbg",
visibility = ["//visibility:public"],
)
go_test(
name = "dbg_test",
size = "small",
srcs = ["dbg_test.go"],
embed = [":dbg"],
deps = [
"//assert",
"//testio",
],
)

346
deps.bzl
View File

@@ -1,346 +0,0 @@
load("@bazel_gazelle//:deps.bzl", "go_repository")
def go_dependencies():
go_repository(
name = "com_github_akavel_rsrc",
importpath = "github.com/akavel/rsrc",
sum = "h1:zjWn7ukO9Kc5Q62DOJCcxGpXC18RawVtYAGdz2aLlfw=",
version = "v0.8.0",
)
go_repository(
name = "com_github_certifi_gocertifi",
importpath = "github.com/certifi/gocertifi",
sum = "h1:6/yVvBsKeAw05IUj4AzvrxaCnDjN4nUqKjW9+w5wixg=",
version = "v0.0.0-20180118203423-deb3ae2ef261",
)
go_repository(
name = "com_github_cloudflare_backoff",
importpath = "github.com/cloudflare/backoff",
sum = "h1:8d1CEOF1xldesKds5tRG3tExBsMOgWYownMHNCsev54=",
version = "v0.0.0-20161212185259-647f3cdfc87a",
)
go_repository(
name = "com_github_cloudflare_cfssl",
importpath = "github.com/cloudflare/cfssl",
sum = "h1:vFJDAvQgFSRbCn9zg8KpSrrEZrBAQ4KO5oNK7SXEyb0=",
version = "v1.5.0",
)
go_repository(
name = "com_github_cloudflare_go_metrics",
importpath = "github.com/cloudflare/go-metrics",
sum = "h1:/8sZyuGTAU2+fYv0Sz9lBcipqX0b7i4eUl8pSStk/4g=",
version = "v0.0.0-20151117154305-6a9aea36fb41",
)
go_repository(
name = "com_github_cloudflare_redoctober",
importpath = "github.com/cloudflare/redoctober",
sum = "h1:p0Q1GvgWtVf46XpMMibupKiE7aQxPYUIb+/jLTTK2kM=",
version = "v0.0.0-20171127175943-746a508df14c",
)
go_repository(
name = "com_github_creack_pty",
importpath = "github.com/creack/pty",
sum = "h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w=",
version = "v1.1.9",
)
go_repository(
name = "com_github_daaku_go_zipexe",
importpath = "github.com/daaku/go.zipexe",
sum = "h1:VSOgZtH418pH9L16hC/JrgSNJbbAL26pj7lmD1+CGdY=",
version = "v1.0.0",
)
go_repository(
name = "com_github_davecgh_go_spew",
importpath = "github.com/davecgh/go-spew",
sum = "h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=",
version = "v1.1.1",
)
go_repository(
name = "com_github_geertjohan_go_incremental",
importpath = "github.com/GeertJohan/go.incremental",
sum = "h1:7AH+pY1XUgQE4Y1HcXYaMqAI0m9yrFqo/jt0CW30vsg=",
version = "v1.0.0",
)
go_repository(
name = "com_github_geertjohan_go_rice",
importpath = "github.com/GeertJohan/go.rice",
sum = "h1:KkI6O9uMaQU3VEKaj01ulavtF7o1fWT7+pk/4voiMLQ=",
version = "v1.0.0",
)
go_repository(
name = "com_github_getsentry_raven_go",
importpath = "github.com/getsentry/raven-go",
sum = "h1:ELaJ1cjF2nEJeIlHXahGme22yG7TK+3jB6IGCq0Cdrc=",
version = "v0.0.0-20180121060056-563b81fc02b7",
)
go_repository(
name = "com_github_go_sql_driver_mysql",
importpath = "github.com/go-sql-driver/mysql",
sum = "h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk=",
version = "v1.4.0",
)
go_repository(
name = "com_github_golang_protobuf",
importpath = "github.com/golang/protobuf",
sum = "h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=",
version = "v1.3.1",
)
go_repository(
name = "com_github_google_certificate_transparency_go",
importpath = "github.com/google/certificate-transparency-go",
sum = "h1:Yf1aXowfZ2nuboBsg7iYGLmwsOARdV86pfH3g95wXmE=",
version = "v1.0.21",
)
go_repository(
name = "com_github_hashicorp_go_syslog",
importpath = "github.com/hashicorp/go-syslog",
sum = "h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE=",
version = "v1.0.0",
)
go_repository(
name = "com_github_jessevdk_go_flags",
importpath = "github.com/jessevdk/go-flags",
sum = "h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=",
version = "v1.4.0",
)
go_repository(
name = "com_github_jmhodges_clock",
importpath = "github.com/jmhodges/clock",
sum = "h1:dYTbLf4m0a5u0KLmPfB6mgxbcV7588bOCx79hxa5Sr4=",
version = "v0.0.0-20160418191101-880ee4c33548",
)
go_repository(
name = "com_github_jmoiron_sqlx",
importpath = "github.com/jmoiron/sqlx",
sum = "h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA=",
version = "v1.2.0",
)
go_repository(
name = "com_github_kisielk_sqlstruct",
importpath = "github.com/kisielk/sqlstruct",
sum = "h1:o/c0aWEP/m6n61xlYW2QP4t9424qlJOsxugn5Zds2Rg=",
version = "v0.0.0-20150923205031-648daed35d49",
)
go_repository(
name = "com_github_kisom_goutils",
importpath = "github.com/kisom/goutils",
sum = "h1:z4HEOgAnFq+e1+O4QdVsyDPatJDu5Ei/7w7DRbYjsIA=",
version = "v1.1.0",
)
go_repository(
name = "com_github_konsorten_go_windows_terminal_sequences",
importpath = "github.com/konsorten/go-windows-terminal-sequences",
sum = "h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=",
version = "v1.0.1",
)
go_repository(
name = "com_github_kr_fs",
importpath = "github.com/kr/fs",
sum = "h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=",
version = "v0.1.0",
)
go_repository(
name = "com_github_kr_pretty",
importpath = "github.com/kr/pretty",
sum = "h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=",
version = "v0.1.0",
)
go_repository(
name = "com_github_kr_pty",
importpath = "github.com/kr/pty",
sum = "h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=",
version = "v1.1.1",
)
go_repository(
name = "com_github_kr_text",
importpath = "github.com/kr/text",
sum = "h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=",
version = "v0.2.0",
)
go_repository(
name = "com_github_kylelemons_go_gypsy",
importpath = "github.com/kylelemons/go-gypsy",
sum = "h1:mkl3tvPHIuPaWsLtmHTybJeoVEW7cbePK73Ir8VtruA=",
version = "v0.0.0-20160905020020-08cad365cd28",
)
go_repository(
name = "com_github_lib_pq",
importpath = "github.com/lib/pq",
sum = "h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU=",
version = "v1.3.0",
)
go_repository(
name = "com_github_mattn_go_sqlite3",
importpath = "github.com/mattn/go-sqlite3",
sum = "h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o=",
version = "v1.10.0",
)
go_repository(
name = "com_github_mreiferson_go_httpclient",
importpath = "github.com/mreiferson/go-httpclient",
sum = "h1:oKIteTqeSpenyTrOVj5zkiyCaflLa8B+CD0324otT+o=",
version = "v0.0.0-20160630210159-31f0106b4474",
)
go_repository(
name = "com_github_nkovacs_streamquote",
importpath = "github.com/nkovacs/streamquote",
sum = "h1:E2B8qYyeSgv5MXpmzZXRNp8IAQ4vjxIjhpAf5hv/tAg=",
version = "v0.0.0-20170412213628-49af9bddb229",
)
go_repository(
name = "com_github_op_go_logging",
importpath = "github.com/op/go-logging",
sum = "h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=",
version = "v0.0.0-20160315200505-970db520ece7",
)
go_repository(
name = "com_github_pkg_errors",
importpath = "github.com/pkg/errors",
sum = "h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=",
version = "v0.9.1",
)
go_repository(
name = "com_github_pkg_sftp",
importpath = "github.com/pkg/sftp",
sum = "h1:/f3b24xrDhkhddlaobPe2JgBqfdt+gC/NYl0QY9IOuI=",
version = "v1.12.0",
)
go_repository(
name = "com_github_pmezard_go_difflib",
importpath = "github.com/pmezard/go-difflib",
sum = "h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=",
version = "v1.0.0",
)
go_repository(
name = "com_github_sirupsen_logrus",
importpath = "github.com/sirupsen/logrus",
sum = "h1:hI/7Q+DtNZ2kINb6qt/lS+IyXnHQe9e90POfeewL/ME=",
version = "v1.3.0",
)
go_repository(
name = "com_github_stretchr_objx",
importpath = "github.com/stretchr/objx",
sum = "h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=",
version = "v0.1.0",
)
go_repository(
name = "com_github_stretchr_testify",
importpath = "github.com/stretchr/testify",
sum = "h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=",
version = "v1.6.1",
)
go_repository(
name = "com_github_valyala_bytebufferpool",
importpath = "github.com/valyala/bytebufferpool",
sum = "h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=",
version = "v1.0.0",
)
go_repository(
name = "com_github_valyala_fasttemplate",
importpath = "github.com/valyala/fasttemplate",
sum = "h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=",
version = "v1.0.1",
)
go_repository(
name = "com_github_weppos_publicsuffix_go",
importpath = "github.com/weppos/publicsuffix-go",
sum = "h1:0Tu1uzLBd1jPn4k6OnMmOPZH/l/9bj9kUOMMkoRs6Gg=",
version = "v0.13.0",
)
go_repository(
name = "com_github_ziutek_mymysql",
importpath = "github.com/ziutek/mymysql",
sum = "h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs=",
version = "v1.5.4",
)
go_repository(
name = "com_github_zmap_rc2",
importpath = "github.com/zmap/rc2",
sum = "h1:kKCF7VX/wTmdg2ZjEaqlq99Bjsoiz7vH6sFniF/vI4M=",
version = "v0.0.0-20131011165748-24b9757f5521",
)
go_repository(
name = "com_github_zmap_zcertificate",
importpath = "github.com/zmap/zcertificate",
sum = "h1:17HHAgFKlLcZsDOjBOUrd5hDihb1ggf+1a5dTbkgkIY=",
version = "v0.0.0-20180516150559-0e3d58b1bac4",
)
go_repository(
name = "com_github_zmap_zcrypto",
importpath = "github.com/zmap/zcrypto",
sum = "h1:PIpcdSOg3pMdFJUBg5yR9xxcj5rm/SGAyaWT/wK6Kco=",
version = "v0.0.0-20200911161511-43ff0ea04f21",
)
go_repository(
name = "com_github_zmap_zlint_v2",
importpath = "github.com/zmap/zlint/v2",
sum = "h1:b2kI/ToXX16h2wjV2c6Da65eT6aTMtkLHKetXuM9EtI=",
version = "v2.2.1",
)
go_repository(
name = "in_gopkg_check_v1",
importpath = "gopkg.in/check.v1",
sum = "h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=",
version = "v1.0.0-20180628173108-788fd7840127",
)
go_repository(
name = "in_gopkg_yaml_v2",
importpath = "gopkg.in/yaml.v2",
sum = "h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=",
version = "v2.4.0",
)
go_repository(
name = "in_gopkg_yaml_v3",
importpath = "gopkg.in/yaml.v3",
sum = "h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=",
version = "v3.0.0-20200313102051-9f266ea9e77c",
)
go_repository(
name = "org_bitbucket_liamstask_goose",
importpath = "bitbucket.org/liamstask/goose",
sum = "h1:bkb2NMGo3/Du52wvYj9Whth5KZfMV6d3O0Vbr3nz/UE=",
version = "v0.0.0-20150115234039-8488cc47d90c",
)
go_repository(
name = "org_golang_x_crypto",
importpath = "golang.org/x/crypto",
sum = "h1:Qwe1rC8PSniVfAFPFJeyUkB+zcysC3RgJBAGk7eqBEU=",
version = "v0.0.0-20220314234659-1baeb1ce4c0b",
)
go_repository(
name = "org_golang_x_lint",
importpath = "golang.org/x/lint",
sum = "h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=",
version = "v0.0.0-20190930215403-16217165b5de",
)
go_repository(
name = "org_golang_x_net",
importpath = "golang.org/x/net",
sum = "h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=",
version = "v0.0.0-20211112202133-69e39bad7dc2",
)
go_repository(
name = "org_golang_x_sys",
importpath = "golang.org/x/sys",
sum = "h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0=",
version = "v0.0.0-20220412211240-33da011f77ad",
)
go_repository(
name = "org_golang_x_term",
importpath = "golang.org/x/term",
sum = "h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=",
version = "v0.0.0-20201126162022-7de9c90e9dd1",
)
go_repository(
name = "org_golang_x_text",
importpath = "golang.org/x/text",
sum = "h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=",
version = "v0.3.6",
)
go_repository(
name = "org_golang_x_tools",
importpath = "golang.org/x/tools",
sum = "h1:/e+gpKk9r3dJobndpTytxS2gOy6m5uvpg+ISQoEcusQ=",
version = "v0.0.0-20190311212946-11955173bddd",
)

View File

@@ -1,8 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "die",
srcs = ["die.go"],
importpath = "git.wntrmute.dev/kyle/goutils/die",
visibility = ["//visibility:public"],
)

View File

@@ -1,54 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "fileutil",
srcs = [
"fileutil.go",
"fileutil_windows.go",
"symlinks.go",
],
importpath = "git.wntrmute.dev/kyle/goutils/fileutil",
visibility = ["//visibility:public"],
deps = select({
"@io_bazel_rules_go//go/platform:aix": [
"@org_golang_x_sys//unix",
],
"@io_bazel_rules_go//go/platform:android": [
"@org_golang_x_sys//unix",
],
"@io_bazel_rules_go//go/platform:darwin": [
"@org_golang_x_sys//unix",
],
"@io_bazel_rules_go//go/platform:dragonfly": [
"@org_golang_x_sys//unix",
],
"@io_bazel_rules_go//go/platform:freebsd": [
"@org_golang_x_sys//unix",
],
"@io_bazel_rules_go//go/platform:illumos": [
"@org_golang_x_sys//unix",
],
"@io_bazel_rules_go//go/platform:ios": [
"@org_golang_x_sys//unix",
],
"@io_bazel_rules_go//go/platform:js": [
"@org_golang_x_sys//unix",
],
"@io_bazel_rules_go//go/platform:linux": [
"@org_golang_x_sys//unix",
],
"@io_bazel_rules_go//go/platform:netbsd": [
"@org_golang_x_sys//unix",
],
"@io_bazel_rules_go//go/platform:openbsd": [
"@org_golang_x_sys//unix",
],
"@io_bazel_rules_go//go/platform:plan9": [
"@org_golang_x_sys//unix",
],
"@io_bazel_rules_go//go/platform:solaris": [
"@org_golang_x_sys//unix",
],
"//conditions:default": [],
}),
)

View File

@@ -1,14 +0,0 @@
#!/bin/bash
set -euxo pipefail
BAZEL="bazel"
if [ -z "$(command -v ${BAZEL})" ]
then
BAZEL="bazelisk"
fi
$BAZEL run //:gazelle
$BAZEL run //:gazelle -- update-repos -from_file=go.mod -to_macro=deps.bzl%go_dependencies
$BAZEL run //:gazelle

2
go.mod
View File

@@ -1,6 +1,6 @@
module git.wntrmute.dev/kyle/goutils
go 1.20
go 1.22
require (
github.com/hashicorp/go-syslog v1.0.0

View File

@@ -1,109 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "lib",
srcs = [
"defs.go",
"ftime_bsd.go",
"ftime_unix.go",
"lib.go",
],
importpath = "git.wntrmute.dev/kyle/goutils/lib",
visibility = ["//visibility:public"],
deps = select({
"@io_bazel_rules_go//go/platform:android_386": [
"@org_golang_x_sys//unix",
],
"@io_bazel_rules_go//go/platform:android_amd64": [
"@org_golang_x_sys//unix",
],
"@io_bazel_rules_go//go/platform:android_arm": [
"@org_golang_x_sys//unix",
],
"@io_bazel_rules_go//go/platform:android_arm64": [
"@org_golang_x_sys//unix",
],
"@io_bazel_rules_go//go/platform:darwin_386": [
"@org_golang_x_sys//unix",
],
"@io_bazel_rules_go//go/platform:darwin_amd64": [
"@org_golang_x_sys//unix",
],
"@io_bazel_rules_go//go/platform:freebsd_386": [
"@org_golang_x_sys//unix",
],
"@io_bazel_rules_go//go/platform:freebsd_amd64": [
"@org_golang_x_sys//unix",
],
"@io_bazel_rules_go//go/platform:freebsd_arm": [
"@org_golang_x_sys//unix",
],
"@io_bazel_rules_go//go/platform:freebsd_arm64": [
"@org_golang_x_sys//unix",
],
"@io_bazel_rules_go//go/platform:ios_amd64": [
"@org_golang_x_sys//unix",
],
"@io_bazel_rules_go//go/platform:linux_386": [
"@org_golang_x_sys//unix",
],
"@io_bazel_rules_go//go/platform:linux_amd64": [
"@org_golang_x_sys//unix",
],
"@io_bazel_rules_go//go/platform:linux_arm": [
"@org_golang_x_sys//unix",
],
"@io_bazel_rules_go//go/platform:linux_arm64": [
"@org_golang_x_sys//unix",
],
"@io_bazel_rules_go//go/platform:linux_mips": [
"@org_golang_x_sys//unix",
],
"@io_bazel_rules_go//go/platform:linux_mips64": [
"@org_golang_x_sys//unix",
],
"@io_bazel_rules_go//go/platform:linux_mips64le": [
"@org_golang_x_sys//unix",
],
"@io_bazel_rules_go//go/platform:linux_mipsle": [
"@org_golang_x_sys//unix",
],
"@io_bazel_rules_go//go/platform:linux_ppc64": [
"@org_golang_x_sys//unix",
],
"@io_bazel_rules_go//go/platform:linux_ppc64le": [
"@org_golang_x_sys//unix",
],
"@io_bazel_rules_go//go/platform:linux_riscv64": [
"@org_golang_x_sys//unix",
],
"@io_bazel_rules_go//go/platform:linux_s390x": [
"@org_golang_x_sys//unix",
],
"@io_bazel_rules_go//go/platform:netbsd_386": [
"@org_golang_x_sys//unix",
],
"@io_bazel_rules_go//go/platform:netbsd_amd64": [
"@org_golang_x_sys//unix",
],
"@io_bazel_rules_go//go/platform:netbsd_arm": [
"@org_golang_x_sys//unix",
],
"@io_bazel_rules_go//go/platform:netbsd_arm64": [
"@org_golang_x_sys//unix",
],
"@io_bazel_rules_go//go/platform:openbsd_386": [
"@org_golang_x_sys//unix",
],
"@io_bazel_rules_go//go/platform:openbsd_amd64": [
"@org_golang_x_sys//unix",
],
"@io_bazel_rules_go//go/platform:openbsd_arm": [
"@org_golang_x_sys//unix",
],
"@io_bazel_rules_go//go/platform:openbsd_arm64": [
"@org_golang_x_sys//unix",
],
"//conditions:default": [],
}),
)

View File

@@ -1,23 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "syslog",
srcs = ["logger.go"],
importpath = "git.wntrmute.dev/kyle/goutils/syslog",
visibility = ["//visibility:public"],
deps = [
"@com_github_davecgh_go_spew//spew",
"@com_github_hashicorp_go_syslog//:go-syslog",
],
)
go_library(
name = "log",
srcs = ["logger.go"],
importpath = "git.wntrmute.dev/kyle/goutils/log",
visibility = ["//visibility:public"],
deps = [
"@com_github_davecgh_go_spew//spew",
"@com_github_hashicorp_go_syslog//:go-syslog",
],
)

View File

@@ -1,24 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "logging",
srcs = [
"console_logger.go",
"doc.go",
"file.go",
"levels.go",
"log.go",
],
importpath = "git.wntrmute.dev/kyle/goutils/logging",
visibility = ["//visibility:public"],
)
go_test(
name = "logging_test",
size = "small",
srcs = [
"example_test.go",
"log_test.go",
],
embed = [":logging"],
)

View File

@@ -1,15 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "example_lib",
srcs = ["example.go"],
importpath = "git.wntrmute.dev/kyle/goutils/logging/example",
visibility = ["//visibility:private"],
deps = ["//logging"],
)
go_binary(
name = "example",
embed = [":example_lib"],
visibility = ["//visibility:public"],
)

View File

@@ -1,19 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "mwc",
srcs = ["mwc.go"],
importpath = "git.wntrmute.dev/kyle/goutils/mwc",
visibility = ["//visibility:public"],
)
go_test(
name = "mwc_test",
size = "small",
srcs = ["mwc_test.go"],
embed = [":mwc"],
deps = [
"//assert",
"//testio",
],
)

View File

@@ -1,15 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "rand",
srcs = ["rand.go"],
importpath = "git.wntrmute.dev/kyle/goutils/rand",
visibility = ["//visibility:public"],
)
go_test(
name = "rand_test",
size = "small",
srcs = ["rand_test.go"],
embed = [":rand"],
)

View File

@@ -1,16 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "sbuf",
srcs = ["sbuf.go"],
importpath = "git.wntrmute.dev/kyle/goutils/sbuf",
visibility = ["//visibility:public"],
)
go_test(
name = "sbuf_test",
size = "small",
srcs = ["sbuf_test.go"],
embed = [":sbuf"],
deps = ["@org_golang_x_crypto//nacl/box"],
)

View File

@@ -1,16 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "seekbuf",
srcs = ["seekbuf.go"],
importpath = "git.wntrmute.dev/kyle/goutils/seekbuf",
visibility = ["//visibility:public"],
)
go_test(
name = "seekbuf_test",
size = "small",
srcs = ["seekbuf_test.go"],
embed = [":seekbuf"],
deps = ["//assert"],
)

View File

@@ -55,3 +55,8 @@ func (b *Buffer) Close() error {
func (b *Buffer) Len() int {
return len(b.data[b.pos:])
}
// Bytes returns the underlying bytes from the current position.
func (b *Buffer) Bytes() []byte {
return b.data[b.pos:]
}

View File

@@ -1,8 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "tee",
srcs = ["tee.go"],
importpath = "git.wntrmute.dev/kyle/goutils/tee",
visibility = ["//visibility:public"],
)

View File

@@ -1,15 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "testio",
srcs = ["testio.go"],
importpath = "git.wntrmute.dev/kyle/goutils/testio",
visibility = ["//visibility:public"],
)
go_test(
name = "testio_test",
size = "small",
srcs = ["testio_test.go"],
embed = [":testio"],
)

View File

@@ -1,8 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "testutil",
srcs = ["testutil.go"],
importpath = "git.wntrmute.dev/kyle/goutils/testutil",
visibility = ["//visibility:public"],
)