Compare commits

..

20 Commits

Author SHA1 Message Date
e639df78ec Add certgen.TestCA for in-memory test certificate infrastructure
Provides a P-256 CA that issues leaf certificates for TLS testing
with full verification enabled. No files written to disk.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 10:44:36 -07:00
5dbb46c3ee Add AIA fields (OCSPServer, IssuingCertificateURL) to certgen.Profile
The Profile struct now supports optional OCSPServer and
IssuingCertificateURL fields. When populated, these are set on the
x509.Certificate template as Authority Information Access extensions
before signing. Empty slices are omitted.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 08:49:28 -07:00
30b5a6699a Ensure CN is included as a DNS SAN when FQDN. 2026-03-15 14:06:36 -07:00
d5cee37433 Add ccfind - locates c/c++ source files. 2026-03-06 10:02:10 -08:00
e1cb7efbf1 DisplayCSR and MatchKeysCSR. 2026-02-12 13:51:20 -08:00
925e0a7124 Update CHANGELOG for v1.18.0. 2025-11-21 18:59:08 -08:00
659f636d01 Update golangci to disable unconvert on cmd/kgz. 2025-11-21 18:58:37 -08:00
e43c677fba Update CHANGELOG for 1.17.2. 2025-11-21 18:52:53 -08:00
94c55af888 Update testdata yaml files. 2025-11-21 18:51:20 -08:00
ee8e48cd56 Update CHANGELOG for v1.17.1. 2025-11-21 18:49:35 -08:00
11866a3b29 Cleaning certlib code. 2025-11-21 18:49:30 -08:00
08a411bccf Update CHANGELOG for v1.17.0. 2025-11-21 16:58:02 -08:00
91f954391e certlib and other updates 2025-11-21 16:56:39 -08:00
d6efbd22fd add bcuz 2025-11-21 16:45:01 -08:00
3cf80ad127 Update CHANGELOG for v1.16.3. 2025-11-20 19:38:12 -08:00
17e9649d1e msg: fixes and tests added. 2025-11-20 19:35:17 -08:00
1fceb0e0da Update CHANGELOG for v1.16.2. 2025-11-20 19:15:34 -08:00
b7bd30b550 msg: fix null pointer deref. 2025-11-20 19:15:08 -08:00
45d011e114 Update CHANGELOG for v1.16.1. 2025-11-20 19:11:05 -08:00
31fa136b49 msg: rename functions for ergonomics. 2025-11-20 19:10:15 -08:00
20 changed files with 1225 additions and 44 deletions

View File

@@ -488,6 +488,8 @@ linters:
linters: [ exhaustive, mnd, revive ] linters: [ exhaustive, mnd, revive ]
- path: 'backoff/backoff_test.go' - path: 'backoff/backoff_test.go'
linters: [ testpackage ] linters: [ testpackage ]
- path: "cmd/kgz/main.go"
linters: [ unconvert ]
- path: 'dbg/dbg_test.go' - path: 'dbg/dbg_test.go'
linters: [ testpackage ] linters: [ testpackage ]
- path: 'log/logger.go' - path: 'log/logger.go'

View File

@@ -1,5 +1,51 @@
CHANGELOG CHANGELOG
v1.19.0 - 2026-02-12
Added:
- certlib/dump: DisplayCSR function for dumping Certificate Signing Request information.
- certlib: MatchKeysCSR function for testing if a CSR's public key matches a private key.
v1.18.0 - 2025-11-21
Changed:
- disable unconvert for kgz, as various platforms complain about it.
v1.17.2 - 2025-11-21
Note: 1.17.2 was a mangled release.
Changed:
- certlib: fix request configs in testdata.
v1.17.1 - 2025-11-21
Changed:
- certlib: various code cleanups.
v1.17.0 - 2025-11-21
Added:
- cmd/bcuz: unzips bandcamp archives.
Changed:
- certlib: ergonomic improvements.
v1.16.3 - 2025-11-21
Changed:
- msg: fixups and testing.
v1.16.2 - 2025-11-21
Changed:
- msg: fill debug null pointer deref.
v1.16.1 - 2025-11-21
Changed:
- msg: rename functions for ergonomics.
v1.16.0 - 2025-11-20 v1.16.0 - 2025-11-20
Added: Added:

View File

@@ -448,13 +448,13 @@ func encodeCertsToFiles(
derContent = append(derContent, cert.Raw...) derContent = append(derContent, cert.Raw...)
} }
files = append(files, fileEntry{ files = append(files, fileEntry{
name: baseName + ".crt", name: baseName + ".cer",
content: derContent, content: derContent,
}) })
} else if len(certs) > 0 { } else if len(certs) > 0 {
// Individual DER file (should only have one cert) // Individual DER file (should only have one cert)
files = append(files, fileEntry{ files = append(files, fileEntry{
name: baseName + ".crt", name: baseName + ".cer",
content: certs[0].Raw, content: certs[0].Raw,
}) })
} }
@@ -472,17 +472,17 @@ func encodeCertsToFiles(
derContent = append(derContent, cert.Raw...) derContent = append(derContent, cert.Raw...)
} }
files = append(files, fileEntry{ files = append(files, fileEntry{
name: baseName + ".crt", name: baseName + ".cer",
content: derContent, content: derContent,
}) })
} else if len(certs) > 0 { } else if len(certs) > 0 {
files = append(files, fileEntry{ files = append(files, fileEntry{
name: baseName + ".crt", name: baseName + ".cer",
content: certs[0].Raw, content: certs[0].Raw,
}) })
} }
default: default:
return nil, fmt.Errorf("unsupported encoding: %s (must be 'pem', 'der', or 'both')", encoding) return nil, fmt.Errorf("unsupported encoding: %s (must be 'pem', 'der', 'both', 'crt', 'pemcrt')", encoding)
} }
return files, nil return files, nil

View File

@@ -8,6 +8,7 @@ import (
"fmt" "fmt"
"math/big" "math/big"
"net" "net"
"slices"
"strings" "strings"
"time" "time"
@@ -19,13 +20,21 @@ type KeySpec struct {
Size int `yaml:"size"` Size int `yaml:"size"`
} }
func (ks KeySpec) String() string {
if strings.ToLower(ks.Algorithm) == nameEd25519 {
return nameEd25519
}
return fmt.Sprintf("%s-%d", ks.Algorithm, ks.Size)
}
func (ks KeySpec) Generate() (crypto.PublicKey, crypto.PrivateKey, error) { func (ks KeySpec) Generate() (crypto.PublicKey, crypto.PrivateKey, error) {
switch strings.ToLower(ks.Algorithm) { switch strings.ToLower(ks.Algorithm) {
case "rsa": case "rsa":
return GenerateKey(x509.RSA, ks.Size) return GenerateKey(x509.RSA, ks.Size)
case "ecdsa": case "ecdsa":
return GenerateKey(x509.ECDSA, ks.Size) return GenerateKey(x509.ECDSA, ks.Size)
case "ed25519": case nameEd25519:
return GenerateKey(x509.Ed25519, 0) return GenerateKey(x509.Ed25519, 0)
default: default:
return nil, nil, fmt.Errorf("unknown key algorithm: %s", ks.Algorithm) return nil, nil, fmt.Errorf("unknown key algorithm: %s", ks.Algorithm)
@@ -38,7 +47,7 @@ func (ks KeySpec) SigningAlgorithm() (x509.SignatureAlgorithm, error) {
return x509.SHA512WithRSAPSS, nil return x509.SHA512WithRSAPSS, nil
case "ecdsa": case "ecdsa":
return x509.ECDSAWithSHA512, nil return x509.ECDSAWithSHA512, nil
case "ed25519": case nameEd25519:
return x509.PureEd25519, nil return x509.PureEd25519, nil
default: default:
return 0, fmt.Errorf("unknown key algorithm: %s", ks.Algorithm) return 0, fmt.Errorf("unknown key algorithm: %s", ks.Algorithm)
@@ -52,7 +61,7 @@ type Subject struct {
Province string `yaml:"province"` Province string `yaml:"province"`
Organization string `yaml:"organization"` Organization string `yaml:"organization"`
OrganizationalUnit string `yaml:"organizational_unit"` OrganizationalUnit string `yaml:"organizational_unit"`
Email string `yaml:"email"` Email []string `yaml:"email"`
DNSNames []string `yaml:"dns"` DNSNames []string `yaml:"dns"`
IPAddresses []string `yaml:"ips"` IPAddresses []string `yaml:"ips"`
} }
@@ -80,11 +89,17 @@ func (cs CertificateRequest) Request(priv crypto.PrivateKey) (*x509.CertificateR
} }
} }
dnsNames := cs.Subject.DNSNames
if isFQDN(cs.Subject.CommonName) && !slices.Contains(dnsNames, cs.Subject.CommonName) {
dnsNames = append(dnsNames, cs.Subject.CommonName)
}
req := &x509.CertificateRequest{ req := &x509.CertificateRequest{
PublicKeyAlgorithm: 0, PublicKeyAlgorithm: 0,
PublicKey: getPublic(priv), PublicKey: getPublic(priv),
Subject: subject, Subject: subject,
DNSNames: cs.Subject.DNSNames, EmailAddresses: cs.Subject.Email,
DNSNames: dnsNames,
IPAddresses: ipAddresses, IPAddresses: ipAddresses,
} }
@@ -118,9 +133,11 @@ func (cs CertificateRequest) Generate() (crypto.PrivateKey, *x509.CertificateReq
type Profile struct { type Profile struct {
IsCA bool `yaml:"is_ca"` IsCA bool `yaml:"is_ca"`
PathLen int `yaml:"path_len"` PathLen int `yaml:"path_len"`
KeyUse string `yaml:"key_uses"` KeyUse []string `yaml:"key_uses"`
ExtKeyUsages []string `yaml:"ext_key_usages"` ExtKeyUsages []string `yaml:"ext_key_usages"`
Expiry string `yaml:"expiry"` Expiry string `yaml:"expiry"`
OCSPServer []string `yaml:"ocsp_server,omitempty"`
IssuingCertificateURL []string `yaml:"issuing_certificate_url,omitempty"`
} }
func (p Profile) templateFromRequest(req *x509.CertificateRequest) (*x509.Certificate, error) { func (p Profile) templateFromRequest(req *x509.CertificateRequest) (*x509.Certificate, error) {
@@ -149,21 +166,30 @@ func (p Profile) templateFromRequest(req *x509.CertificateRequest) (*x509.Certif
IPAddresses: req.IPAddresses, IPAddresses: req.IPAddresses,
} }
var ok bool for _, sku := range p.KeyUse {
certTemplate.KeyUsage, ok = keyUsageStrings[p.KeyUse] ku, ok := keyUsageStrings[sku]
if !ok { if !ok {
return nil, fmt.Errorf("invalid key usage: %s", p.KeyUse) return nil, fmt.Errorf("invalid key usage: %s", p.KeyUse)
} }
var eku x509.ExtKeyUsage certTemplate.KeyUsage |= ku
}
for _, extKeyUsage := range p.ExtKeyUsages { for _, extKeyUsage := range p.ExtKeyUsages {
eku, ok = extKeyUsageStrings[extKeyUsage] eku, ok := extKeyUsageStrings[extKeyUsage]
if !ok { if !ok {
return nil, fmt.Errorf("invalid extended key usage: %s", extKeyUsage) return nil, fmt.Errorf("invalid extended key usage: %s", extKeyUsage)
} }
certTemplate.ExtKeyUsage = append(certTemplate.ExtKeyUsage, eku) certTemplate.ExtKeyUsage = append(certTemplate.ExtKeyUsage, eku)
} }
if len(p.OCSPServer) > 0 {
certTemplate.OCSPServer = p.OCSPServer
}
if len(p.IssuingCertificateURL) > 0 {
certTemplate.IssuingCertificateURL = p.IssuingCertificateURL
}
return certTemplate, nil return certTemplate, nil
} }
@@ -199,6 +225,32 @@ func (p Profile) SelfSign(req *x509.CertificateRequest, priv crypto.PrivateKey)
return p.SignRequest(certTemplate, req, priv) return p.SignRequest(certTemplate, req, priv)
} }
// isFQDN returns true if s looks like a fully-qualified domain name.
func isFQDN(s string) bool {
if s == "" {
return false
}
// Must contain at least one dot and no spaces.
if !strings.Contains(s, ".") || strings.ContainsAny(s, " \t") {
return false
}
// Each label must be non-empty and consist of letters, digits, or hyphens.
for label := range strings.SplitSeq(strings.TrimSuffix(s, "."), ".") {
if label == "" {
return false
}
for _, c := range label {
if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-') {
return false
}
}
if label[0] == '-' || label[len(label)-1] == '-' {
return false
}
}
return true
}
func SerialNumber() (*big.Int, error) { func SerialNumber() (*big.Int, error) {
serialNumberBytes := make([]byte, 20) serialNumberBytes := make([]byte, 20)
_, err := rand.Read(serialNumberBytes) _, err := rand.Read(serialNumberBytes)

View File

@@ -0,0 +1,188 @@
package certgen
import (
"slices"
"testing"
)
func TestIsFQDN(t *testing.T) {
tests := []struct {
input string
want bool
}{
{"example.com", true},
{"sub.example.com", true},
{"example.com.", true}, // trailing dot
{"localhost", false}, // no dot
{"", false},
{"foo bar.com", false}, // space
{"-bad.com", false}, // leading hyphen
{"bad-.com", false}, // trailing hyphen
{"a..b.com", false}, // empty label
}
for _, tt := range tests {
got := isFQDN(tt.input)
if got != tt.want {
t.Errorf("isFQDN(%q) = %v, want %v", tt.input, got, tt.want)
}
}
}
func TestRequestAddsFQDNAsDNSSAN(t *testing.T) {
creq := &CertificateRequest{
KeySpec: KeySpec{Algorithm: "ecdsa", Size: 256},
Subject: Subject{
CommonName: "example.com",
Organization: "Test Org",
},
Profile: Profile{
Expiry: "1h",
},
}
_, req, err := creq.Generate()
if err != nil {
t.Fatalf("Generate() error: %v", err)
}
if !slices.Contains(req.DNSNames, "example.com") {
t.Errorf("expected DNS SAN to contain %q, got %v", "example.com", req.DNSNames)
}
}
func TestRequestFQDNNotDuplicated(t *testing.T) {
creq := &CertificateRequest{
KeySpec: KeySpec{Algorithm: "ecdsa", Size: 256},
Subject: Subject{
CommonName: "example.com",
Organization: "Test Org",
DNSNames: []string{"example.com", "www.example.com"},
},
Profile: Profile{
Expiry: "1h",
},
}
_, req, err := creq.Generate()
if err != nil {
t.Fatalf("Generate() error: %v", err)
}
count := 0
for _, name := range req.DNSNames {
if name == "example.com" {
count++
}
}
if count != 1 {
t.Errorf("expected exactly 1 occurrence of %q in DNS SANs, got %d: %v", "example.com", count, req.DNSNames)
}
}
func TestProfileAIAFieldsInCertificate(t *testing.T) {
caKey := KeySpec{Algorithm: "ecdsa", Size: 256}
_, caPriv, err := caKey.Generate()
if err != nil {
t.Fatalf("generate CA key: %v", err)
}
caProfile := Profile{
IsCA: true,
PathLen: 1,
KeyUse: []string{"cert sign", "crl sign"},
Expiry: "8760h",
}
caReq := &CertificateRequest{
KeySpec: caKey,
Subject: Subject{CommonName: "Test CA", Organization: "Test"},
Profile: caProfile,
}
caCSR, err := caReq.Request(caPriv)
if err != nil {
t.Fatalf("generate CA CSR: %v", err)
}
caCert, err := caProfile.SelfSign(caCSR, caPriv)
if err != nil {
t.Fatalf("self-sign CA: %v", err)
}
leafProfile := Profile{
KeyUse: []string{"digital signature"},
ExtKeyUsages: []string{"server auth"},
Expiry: "24h",
OCSPServer: []string{"https://ocsp.example.com"},
IssuingCertificateURL: []string{"https://pki.example.com/ca.pem"},
}
leafReq := &CertificateRequest{
KeySpec: KeySpec{Algorithm: "ecdsa", Size: 256},
Subject: Subject{CommonName: "leaf.example.com", Organization: "Test"},
Profile: leafProfile,
}
_, leafCSR, err := leafReq.Generate()
if err != nil {
t.Fatalf("generate leaf CSR: %v", err)
}
leafCert, err := leafProfile.SignRequest(caCert, leafCSR, caPriv)
if err != nil {
t.Fatalf("sign leaf: %v", err)
}
if len(leafCert.OCSPServer) != 1 || leafCert.OCSPServer[0] != "https://ocsp.example.com" {
t.Errorf("OCSPServer = %v, want [https://ocsp.example.com]", leafCert.OCSPServer)
}
if len(leafCert.IssuingCertificateURL) != 1 || leafCert.IssuingCertificateURL[0] != "https://pki.example.com/ca.pem" {
t.Errorf("IssuingCertificateURL = %v, want [https://pki.example.com/ca.pem]", leafCert.IssuingCertificateURL)
}
}
func TestProfileWithoutAIAOmitsExtension(t *testing.T) {
profile := Profile{
KeyUse: []string{"digital signature"},
ExtKeyUsages: []string{"server auth"},
Expiry: "24h",
}
creq := &CertificateRequest{
KeySpec: KeySpec{Algorithm: "ecdsa", Size: 256},
Subject: Subject{CommonName: "noaia.example.com", Organization: "Test"},
Profile: profile,
}
cert, _, err := GenerateSelfSigned(creq)
if err != nil {
t.Fatalf("generate: %v", err)
}
if len(cert.OCSPServer) != 0 {
t.Errorf("OCSPServer = %v, want empty", cert.OCSPServer)
}
if len(cert.IssuingCertificateURL) != 0 {
t.Errorf("IssuingCertificateURL = %v, want empty", cert.IssuingCertificateURL)
}
}
func TestRequestNonFQDNCommonNameNotAdded(t *testing.T) {
creq := &CertificateRequest{
KeySpec: KeySpec{Algorithm: "ecdsa", Size: 256},
Subject: Subject{
CommonName: "localhost",
Organization: "Test Org",
},
Profile: Profile{
Expiry: "1h",
},
}
_, req, err := creq.Generate()
if err != nil {
t.Fatalf("Generate() error: %v", err)
}
if slices.Contains(req.DNSNames, "localhost") {
t.Errorf("expected DNS SANs to not contain %q, got %v", "localhost", req.DNSNames)
}
}

View File

@@ -16,6 +16,10 @@ import (
// oidEd25519 = asn1.ObjectIdentifier{1, 3, 101, 110} // oidEd25519 = asn1.ObjectIdentifier{1, 3, 101, 110}
//) //)
const (
nameEd25519 = "ed25519"
)
func GenerateKey(algorithm x509.PublicKeyAlgorithm, bitSize int) (crypto.PublicKey, crypto.PrivateKey, error) { func GenerateKey(algorithm x509.PublicKeyAlgorithm, bitSize int) (crypto.PublicKey, crypto.PrivateKey, error) {
var key crypto.PrivateKey var key crypto.PrivateKey
var pub crypto.PublicKey var pub crypto.PublicKey

213
certlib/certgen/testca.go Normal file
View File

@@ -0,0 +1,213 @@
package certgen
import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"net"
"time"
)
// TestCA is an in-memory certificate authority for use in tests. It
// provides a root CA certificate and the ability to issue leaf
// certificates for TLS testing with full verification enabled.
type TestCA struct {
cert *x509.Certificate
key *ecdsa.PrivateKey
}
// NewTestCA creates a new TestCA with a self-signed P-256 root
// certificate. The CA is valid for 1 hour.
func NewTestCA() (*TestCA, error) {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, fmt.Errorf("certgen: generating CA key: %w", err)
}
serial, err := SerialNumber()
if err != nil {
return nil, fmt.Errorf("certgen: generating serial: %w", err)
}
template := &x509.Certificate{
SerialNumber: serial,
Subject: pkix.Name{
CommonName: "Test CA",
Organization: []string{"Test"},
},
NotBefore: time.Now().Add(-1 * time.Minute),
NotAfter: time.Now().Add(1 * time.Hour),
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
BasicConstraintsValid: true,
IsCA: true,
MaxPathLen: 1,
}
certDER, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
if err != nil {
return nil, fmt.Errorf("certgen: creating CA certificate: %w", err)
}
cert, err := x509.ParseCertificate(certDER)
if err != nil {
return nil, fmt.Errorf("certgen: parsing CA certificate: %w", err)
}
return &TestCA{cert: cert, key: key}, nil
}
// Certificate returns the root CA certificate.
func (ca *TestCA) Certificate() *x509.Certificate {
return ca.cert
}
// CertificatePEM returns the root CA certificate as a PEM-encoded
// byte slice.
func (ca *TestCA) CertificatePEM() []byte {
return pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: ca.cert.Raw,
})
}
// CertPool returns a certificate pool containing the root CA
// certificate, suitable for use as a TLS root CA pool.
func (ca *TestCA) CertPool() *x509.CertPool {
pool := x509.NewCertPool()
pool.AddCert(ca.cert)
return pool
}
// Issue creates a new leaf certificate signed by the CA for the given
// DNS names and IP addresses. It returns the leaf private key and
// certificate. The leaf certificate is valid for 1 hour with key
// usage appropriate for a TLS server.
func (ca *TestCA) Issue(dnsNames []string, ips []net.IP) (crypto.Signer, *x509.Certificate, error) {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, nil, fmt.Errorf("certgen: generating leaf key: %w", err)
}
serial, err := SerialNumber()
if err != nil {
return nil, nil, fmt.Errorf("certgen: generating serial: %w", err)
}
cn := "localhost"
if len(dnsNames) > 0 {
cn = dnsNames[0]
}
template := &x509.Certificate{
SerialNumber: serial,
Subject: pkix.Name{
CommonName: cn,
Organization: []string{"Test"},
},
NotBefore: time.Now().Add(-1 * time.Minute),
NotAfter: time.Now().Add(1 * time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
BasicConstraintsValid: true,
DNSNames: dnsNames,
IPAddresses: ips,
}
certDER, err := x509.CreateCertificate(rand.Reader, template, ca.cert, &key.PublicKey, ca.key)
if err != nil {
return nil, nil, fmt.Errorf("certgen: creating leaf certificate: %w", err)
}
cert, err := x509.ParseCertificate(certDER)
if err != nil {
return nil, nil, fmt.Errorf("certgen: parsing leaf certificate: %w", err)
}
return key, cert, nil
}
// IssueServer is a convenience wrapper around Issue for the common
// case of a server certificate for localhost (both DNS and IP).
func (ca *TestCA) IssueServer() (crypto.Signer, *x509.Certificate, error) {
return ca.Issue(
[]string{"localhost"},
[]net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback},
)
}
// TLSConfig returns a tls.Config (by value) configured with the CA's
// root pool for verification. The caller can set additional fields
// (e.g., Certificates) or modify the returned config safely.
func (ca *TestCA) TLSConfig() tls.Config {
return tls.Config{
RootCAs: ca.CertPool(),
MinVersion: tls.VersionTLS13,
}
}
// ServerTLSConfig returns a tls.Config (by value) for a TLS server
// using the given leaf key and certificate, with client verification
// against the CA root pool. Pass key and cert from Issue or
// IssueServer.
func (ca *TestCA) ServerTLSConfig(key crypto.Signer, cert *x509.Certificate) tls.Config {
return tls.Config{
Certificates: []tls.Certificate{
{
Certificate: [][]byte{cert.Raw},
PrivateKey: key,
Leaf: cert,
},
},
ClientCAs: ca.CertPool(),
MinVersion: tls.VersionTLS13,
}
}
// TLSKeyPair returns a tls.Certificate from the given key and
// certificate, suitable for use in a tls.Config.Certificates slice.
func TLSKeyPair(key crypto.Signer, cert *x509.Certificate) tls.Certificate {
return tls.Certificate{
Certificate: [][]byte{cert.Raw},
PrivateKey: key,
Leaf: cert,
}
}
// MustTestCA calls NewTestCA and panics on error. Intended for use
// in TestMain or test helpers where error handling is impractical.
func MustTestCA() *TestCA {
ca, err := NewTestCA()
if err != nil {
panic("certgen: " + err.Error())
}
return ca
}
// CertificatePEM returns a PEM-encoded byte slice for the given
// certificate.
func CertificatePEM(cert *x509.Certificate) []byte {
return pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: cert.Raw,
})
}
// PrivateKeyPEM returns a PEM-encoded PKCS#8 byte slice for the
// given private key.
func PrivateKeyPEM(key crypto.Signer) ([]byte, error) {
der, err := x509.MarshalPKCS8PrivateKey(key)
if err != nil {
return nil, fmt.Errorf("certgen: marshaling private key: %w", err)
}
return pem.EncodeToMemory(&pem.Block{
Type: "PRIVATE KEY",
Bytes: der,
}), nil
}

View File

@@ -0,0 +1,223 @@
package certgen
import (
"crypto/tls"
"crypto/x509"
"encoding/pem"
"net"
"net/http"
"net/http/httptest"
"testing"
)
func TestNewTestCA(t *testing.T) {
ca, err := NewTestCA()
if err != nil {
t.Fatalf("NewTestCA: %v", err)
}
cert := ca.Certificate()
if !cert.IsCA {
t.Fatal("expected CA certificate")
}
if cert.Subject.CommonName != "Test CA" {
t.Fatalf("got CN %q, want %q", cert.Subject.CommonName, "Test CA")
}
}
func TestCertificatePEMRoundtrip(t *testing.T) {
ca, err := NewTestCA()
if err != nil {
t.Fatalf("NewTestCA: %v", err)
}
pemBytes := ca.CertificatePEM()
block, _ := pem.Decode(pemBytes)
if block == nil {
t.Fatal("failed to decode PEM")
}
if block.Type != "CERTIFICATE" {
t.Fatalf("got PEM type %q, want CERTIFICATE", block.Type)
}
parsed, err := x509.ParseCertificate(block.Bytes)
if err != nil {
t.Fatalf("parse certificate: %v", err)
}
if !parsed.Equal(ca.Certificate()) {
t.Fatal("parsed certificate does not match original")
}
}
func TestCertPool(t *testing.T) {
ca, err := NewTestCA()
if err != nil {
t.Fatalf("NewTestCA: %v", err)
}
pool := ca.CertPool()
// Verify the CA cert validates against its own pool.
chains, err := ca.Certificate().Verify(x509.VerifyOptions{
Roots: pool,
})
if err != nil {
t.Fatalf("verify CA cert against its own pool: %v", err)
}
if len(chains) == 0 {
t.Fatal("expected at least one chain")
}
}
func TestIssue(t *testing.T) {
ca, err := NewTestCA()
if err != nil {
t.Fatalf("NewTestCA: %v", err)
}
dnsNames := []string{"example.test", "www.example.test"}
ips := []net.IP{net.IPv4(10, 0, 0, 1)}
key, cert, err := ca.Issue(dnsNames, ips)
if err != nil {
t.Fatalf("Issue: %v", err)
}
if key == nil {
t.Fatal("expected non-nil key")
}
if cert.IsCA {
t.Fatal("leaf cert should not be CA")
}
if cert.Subject.CommonName != "example.test" {
t.Fatalf("got CN %q, want %q", cert.Subject.CommonName, "example.test")
}
// Verify the leaf cert chains to the CA.
_, err = cert.Verify(x509.VerifyOptions{
Roots: ca.CertPool(),
DNSName: "example.test",
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
})
if err != nil {
t.Fatalf("verify leaf cert: %v", err)
}
}
func TestIssueServerTLS(t *testing.T) {
ca, err := NewTestCA()
if err != nil {
t.Fatalf("NewTestCA: %v", err)
}
key, cert, err := ca.IssueServer()
if err != nil {
t.Fatalf("IssueServer: %v", err)
}
// Start a TLS server with the issued cert.
serverCfg := ca.ServerTLSConfig(key, cert)
srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
srv.TLS = &serverCfg
srv.StartTLS()
defer srv.Close()
// Create a client that verifies the server cert against the CA.
clientCfg := ca.TLSConfig()
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &clientCfg,
},
}
resp, err := client.Get(srv.URL)
if err != nil {
t.Fatalf("GET: %v", err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("got status %d, want 200", resp.StatusCode)
}
}
func TestTLSConfigReturnsByValue(t *testing.T) {
ca, err := NewTestCA()
if err != nil {
t.Fatalf("NewTestCA: %v", err)
}
cfg1 := ca.TLSConfig()
cfg2 := ca.TLSConfig()
// Modifying one should not affect the other.
cfg1.ServerName = "modified"
if cfg2.ServerName == "modified" {
t.Fatal("TLSConfig should return independent values")
}
}
func TestTLSConfigEnforcesTLS13(t *testing.T) {
ca, err := NewTestCA()
if err != nil {
t.Fatalf("NewTestCA: %v", err)
}
cfg := ca.TLSConfig()
if cfg.MinVersion != tls.VersionTLS13 {
t.Fatalf("got MinVersion %d, want TLS 1.3 (%d)", cfg.MinVersion, tls.VersionTLS13)
}
}
func TestMustTestCA(t *testing.T) {
// Should not panic.
ca := MustTestCA()
if ca.Certificate() == nil {
t.Fatal("expected non-nil certificate")
}
}
func TestPrivateKeyPEM(t *testing.T) {
ca, err := NewTestCA()
if err != nil {
t.Fatalf("NewTestCA: %v", err)
}
key, _, err := ca.IssueServer()
if err != nil {
t.Fatalf("IssueServer: %v", err)
}
pemBytes, err := PrivateKeyPEM(key)
if err != nil {
t.Fatalf("PrivateKeyPEM: %v", err)
}
block, _ := pem.Decode(pemBytes)
if block == nil {
t.Fatal("failed to decode PEM")
}
if block.Type != "PRIVATE KEY" {
t.Fatalf("got PEM type %q, want PRIVATE KEY", block.Type)
}
}
func TestUntrustedCAFails(t *testing.T) {
ca1 := MustTestCA()
ca2 := MustTestCA()
// Issue a cert from ca1, try to verify against ca2's pool.
_, cert, err := ca1.IssueServer()
if err != nil {
t.Fatalf("IssueServer: %v", err)
}
_, err = cert.Verify(x509.VerifyOptions{
Roots: ca2.CertPool(),
DNSName: "localhost",
})
if err == nil {
t.Fatal("expected verification to fail with wrong CA")
}
}

View File

@@ -165,6 +165,28 @@ func certPublic(cert *x509.Certificate) string {
} }
} }
func csrPublic(csr *x509.CertificateRequest) string {
switch pub := csr.PublicKey.(type) {
case *rsa.PublicKey:
return fmt.Sprintf("RSA-%d", pub.N.BitLen())
case *ecdsa.PublicKey:
switch pub.Curve {
case elliptic.P256():
return "ECDSA-prime256v1"
case elliptic.P384():
return "ECDSA-secp384r1"
case elliptic.P521():
return "ECDSA-secp521r1"
default:
return "ECDSA (unknown curve)"
}
case *dsa.PublicKey:
return "DSA"
default:
return "Unknown"
}
}
func DisplayName(name pkix.Name) string { func DisplayName(name pkix.Name) string {
var ns []string var ns []string
@@ -333,3 +355,36 @@ func DisplayCert(w io.Writer, cert *x509.Certificate, showHash bool) {
} }
} }
} }
func DisplayCSR(w io.Writer, csr *x509.CertificateRequest, showHash bool) {
fmt.Fprintln(w, "CERTIFICATE REQUEST")
if showHash {
fmt.Fprintln(w, wrap(fmt.Sprintf("SHA256: %x", sha256.Sum256(csr.Raw)), 0))
}
fmt.Fprintln(w, wrap("Subject: "+DisplayName(csr.Subject), 0))
fmt.Fprintf(w, "\tSignature algorithm: %s / %s\n", sigAlgoPK(csr.SignatureAlgorithm),
sigAlgoHash(csr.SignatureAlgorithm))
fmt.Fprintln(w, "Details:")
wrapPrint("Public key: "+csrPublic(csr), 1)
validNames := make([]string, 0, len(csr.DNSNames)+len(csr.EmailAddresses)+len(csr.IPAddresses)+len(csr.URIs))
for i := range csr.DNSNames {
validNames = append(validNames, "dns:"+csr.DNSNames[i])
}
for i := range csr.EmailAddresses {
validNames = append(validNames, "email:"+csr.EmailAddresses[i])
}
for i := range csr.IPAddresses {
validNames = append(validNames, "ip:"+csr.IPAddresses[i].String())
}
for i := range csr.URIs {
validNames = append(validNames, "uri:"+csr.URIs[i].String())
}
sans := fmt.Sprintf("SANs (%d): %s\n", len(validNames), strings.Join(validNames, ", "))
wrapPrint(sans, 1)
}

View File

@@ -133,3 +133,48 @@ func MatchKeys(cert *x509.Certificate, priv crypto.Signer) (bool, string) {
return false, fmt.Sprintf("unrecognised private key type: %T", priv.Public()) return false, fmt.Sprintf("unrecognised private key type: %T", priv.Public())
} }
} }
// MatchKeysCSR determines whether the CSR's public key matches the given private key.
// It returns true if they match; otherwise, it returns false and a human-friendly reason.
func MatchKeysCSR(csr *x509.CertificateRequest, priv crypto.Signer) (bool, string) {
switch keyPub := priv.Public().(type) {
case *rsa.PublicKey:
switch csrPub := csr.PublicKey.(type) {
case *rsa.PublicKey:
if matchRSA(csrPub, keyPub) {
return true, ""
}
return false, "public keys don't match"
case *ecdsa.PublicKey:
return false, "RSA private key, EC public key"
default:
return false, fmt.Sprintf("unsupported CSR public key type: %T", csr.PublicKey)
}
case *ecdsa.PublicKey:
switch csrPub := csr.PublicKey.(type) {
case *ecdsa.PublicKey:
if matchECDSA(csrPub, keyPub) {
return true, ""
}
// Determine a more precise reason
kc := getECCurve(keyPub)
cc := getECCurve(csrPub)
if kc == curveInvalid {
return false, "invalid private key curve"
}
if cc == curveRSA {
return false, "private key is EC, CSR is RSA"
}
if kc != cc {
return false, "EC curves don't match"
}
return false, "public keys don't match"
case *rsa.PublicKey:
return false, "private key is EC, CSR is RSA"
default:
return false, fmt.Sprintf("unsupported CSR public key type: %T", csr.PublicKey)
}
default:
return false, fmt.Sprintf("unrecognised private key type: %T", priv.Public())
}
}

View File

@@ -6,6 +6,7 @@ import (
"crypto/ed25519" "crypto/ed25519"
"crypto/rsa" "crypto/rsa"
"crypto/sha1" // #nosec G505 this is the standard "crypto/sha1" // #nosec G505 this is the standard
"crypto/tls"
"crypto/x509" "crypto/x509"
"crypto/x509/pkix" "crypto/x509/pkix"
"encoding/asn1" "encoding/asn1"
@@ -15,7 +16,9 @@ import (
"git.wntrmute.dev/kyle/goutils/certlib" "git.wntrmute.dev/kyle/goutils/certlib"
"git.wntrmute.dev/kyle/goutils/die" "git.wntrmute.dev/kyle/goutils/die"
"git.wntrmute.dev/kyle/goutils/fileutil"
"git.wntrmute.dev/kyle/goutils/lib" "git.wntrmute.dev/kyle/goutils/lib"
"git.wntrmute.dev/kyle/goutils/lib/fetch"
) )
const ( const (
@@ -53,6 +56,30 @@ func (k *KeyInfo) SKI(displayMode lib.HexEncodeMode) (string, error) {
return pubHashString, nil return pubHashString, nil
} }
func Lookup(path string, tcfg *tls.Config) (*KeyInfo, error) {
if fileutil.FileDoesExist(path) {
return ParsePEM(path)
}
server, err := fetch.ParseServer(path, tcfg)
if err != nil {
return nil, err
}
cert, err := server.Get()
if err != nil {
return nil, err
}
material := &KeyInfo{
FileType: "certificate",
}
material.PublicKey, material.KeyType = parseCertificate(cert)
return material, nil
}
// ParsePEM parses a PEM file and returns the public key and its type. // ParsePEM parses a PEM file and returns the public key and its type.
func ParsePEM(path string) (*KeyInfo, error) { func ParsePEM(path string) (*KeyInfo, error) {
material := &KeyInfo{} material := &KeyInfo{}
@@ -79,7 +106,7 @@ func ParsePEM(path string) (*KeyInfo, error) {
material.PublicKey, material.KeyType = parseKey(data) material.PublicKey, material.KeyType = parseKey(data)
material.FileType = "private key" material.FileType = "private key"
case "CERTIFICATE": case "CERTIFICATE":
material.PublicKey, material.KeyType = parseCertificate(data) material.PublicKey, material.KeyType = parseCertificateFile(data)
material.FileType = "certificate" material.FileType = "certificate"
case "CERTIFICATE REQUEST": case "CERTIFICATE REQUEST":
material.PublicKey, material.KeyType = parseCSR(data) material.PublicKey, material.KeyType = parseCSR(data)
@@ -113,12 +140,17 @@ func parseKey(data []byte) ([]byte, string) {
return public, kt return public, kt
} }
func parseCertificate(data []byte) ([]byte, string) { func parseCertificateFile(data []byte) ([]byte, string) {
cert, err := x509.ParseCertificate(data) cert, err := x509.ParseCertificate(data)
die.If(err) die.If(err)
return parseCertificate(cert)
}
func parseCertificate(cert *x509.Certificate) ([]byte, string) {
pub := cert.PublicKey pub := cert.PublicKey
var kt string var kt string
switch pub.(type) { switch pub.(type) {
case *rsa.PublicKey: case *rsa.PublicKey:
kt = keyTypeRSA kt = keyTypeRSA

View File

@@ -9,5 +9,6 @@ subject:
profile: profile:
is_ca: true is_ca: true
path_len: 3 path_len: 3
key_uses: cert sign key_uses:
- cert sign
expiry: 20y expiry: 20y

View File

@@ -9,5 +9,6 @@ subject:
profile: profile:
is_ca: true is_ca: true
path_len: 3 path_len: 3
key_uses: cert sign key_uses:
- cert sign
expiry: 20y expiry: 20y

8
cmd/bcuz/README Normal file
View File

@@ -0,0 +1,8 @@
bcuz: bandcamp unzip
When you download stuff from bandcamp, it gives you a zip file that
extracts files into the current directory. This is a quick hack tries
to parse the filename as "artist - album", unpack the contents to
"artist/album/*", and remove the zip file (which can be kept with
-k). Works on my machine™, good enough for me.

110
cmd/bcuz/main.go Normal file
View File

@@ -0,0 +1,110 @@
package main
import (
"archive/zip"
"errors"
"flag"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
var unrestrictedDecompression bool
var keepArchive bool
func removedir(dir string, existed bool) {
if !existed {
os.RemoveAll(dir)
}
}
func unpackFile(path string) error {
var dir string
var existed bool
fmt.Printf("[+] processing %s:\n", path)
base := filepath.Base(path[:len(path)-4])
pieces := strings.SplitN(base, "-", 2)
if len(pieces) == 2 {
artist := strings.TrimSpace(pieces[0])
album := strings.TrimSpace(pieces[1])
dir = filepath.Join(artist, album)
} else {
dir = base
}
_, err := os.Stat(dir)
if err == nil {
existed = true
}
fmt.Printf("\tunpack directory: %s\n", dir)
err = os.MkdirAll(dir, 0755)
if err != nil {
return err
}
r, err := zip.OpenReader(path)
if err != nil {
removedir(dir, existed)
return err
}
defer r.Close()
var rc io.ReadCloser
for _, f := range r.File {
fmt.Printf("\tunpacking %s\n", f.FileHeader.Name)
rc, err = f.Open()
if err != nil {
rc.Close()
removedir(dir, existed)
return err
}
if f.UncompressedSize64 > (f.CompressedSize64*32) && !unrestrictedDecompression {
rc.Close()
removedir(dir, existed)
return errors.New("file is too large to decompress (maybe a zip bomb)")
}
var out *os.File
out, err = os.Create(filepath.Join(dir, f.FileHeader.Name))
if err != nil {
rc.Close()
removedir(dir, existed)
return err
}
_, err = io.Copy(out, rc) // #nosec G110: handled with size check above
if err != nil {
rc.Close()
removedir(dir, existed)
return err
}
out.Close()
rc.Close()
}
if !keepArchive {
return os.Remove(path)
}
return nil
}
func main() {
flag.BoolVar(&keepArchive, "k", false, "don't remove the archive file after unpacking")
flag.BoolVar(&unrestrictedDecompression, "u", false, "allow unrestricted decompression")
flag.Parse()
for _, path := range flag.Args() {
err := unpackFile(path)
if err != nil {
fmt.Fprintf(os.Stderr, "[!] failed to process %s: %s\n", path, err)
}
}
}

46
cmd/ccfind/main.go Normal file
View File

@@ -0,0 +1,46 @@
package main
// Prompt:
// The current main.go should accept a list of paths to search. In each
// of those paths, without recursing, it should find all files ending in
// C/C++ source extensions and print them one per line.
import (
"fmt"
"os"
"path/filepath"
"slices"
"strings"
)
var extensions = []string{
".c", ".cpp", ".cc", ".cxx",
".h", ".hpp", ".hh", ".hxx",
}
func main() {
if len(os.Args) < 2 {
fmt.Fprintf(os.Stderr, "Usage: %s <path> [path...]\n", os.Args[0])
os.Exit(1)
}
for _, path := range os.Args[1:] {
entries, err := os.ReadDir(path)
if err != nil {
fmt.Fprintf(os.Stderr, "Error reading %s: %v\n", path, err)
continue
}
for _, entry := range entries {
if entry.IsDir() {
continue
}
name := entry.Name()
ext := filepath.Ext(name)
if slices.Contains(extensions, strings.ToLower(ext)) {
fmt.Println(filepath.Join(path, name))
}
}
}
}

View File

@@ -111,7 +111,7 @@ func buildExtraForPath(st unix.Stat_t, path string, setUID, setGID int) []byte {
ctns = clampToInt32(ft.Changed.Nanosecond()) ctns = clampToInt32(ft.Changed.Nanosecond())
} }
return buildKGExtra(uid, gid, mode, cts, ctns) return buildKGExtra(uid, gid, uint32(mode), cts, ctns)
} }
// parseKGExtra scans a gzip Extra blob and returns kgz metadata if present. // parseKGExtra scans a gzip Extra blob and returns kgz metadata if present.

6
go.sum
View File

@@ -29,19 +29,15 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=

View File

@@ -2,17 +2,18 @@
// flags for quiet, verbose, and debug modes. The default is to // flags for quiet, verbose, and debug modes. The default is to
// have all modes disabled. // have all modes disabled.
// //
// The QPrint messages will only output messages if quiet mode is // The Qprint messages will only output messages if quiet mode is
// disabled // disabled
// The VPrint messages will only output messages if verbose mode // The Vprint messages will only output messages if verbose mode
// is enabled. // is enabled.
// The DPrint messages will only output messages if debug mode // The Dprint messages will only output messages if debug mode
// is enabled. // is enabled.
package msg package msg
import ( import (
"fmt" "fmt"
"io" "io"
"os"
"git.wntrmute.dev/kyle/goutils/lib" "git.wntrmute.dev/kyle/goutils/lib"
@@ -22,10 +23,19 @@ import (
var ( var (
enableQuiet bool enableQuiet bool
enableVerbose bool enableVerbose bool
debug *dbg.DebugPrinter
w io.Writer debug = dbg.New()
w io.Writer = os.Stdout
) )
func Reset() {
enableQuiet = false
enableVerbose = false
debug = dbg.New()
w = os.Stdout
}
func SetQuiet(q bool) { func SetQuiet(q bool) {
enableQuiet = q enableQuiet = q
} }
@@ -44,7 +54,7 @@ func Set(q, v, d bool) {
SetDebug(d) SetDebug(d)
} }
func QPrint(a ...any) { func Qprint(a ...any) {
if enableQuiet { if enableQuiet {
return return
} }
@@ -52,7 +62,7 @@ func QPrint(a ...any) {
fmt.Fprint(w, a...) fmt.Fprint(w, a...)
} }
func QPrintf(format string, a ...any) { func Qprintf(format string, a ...any) {
if enableQuiet { if enableQuiet {
return return
} }
@@ -60,7 +70,7 @@ func QPrintf(format string, a ...any) {
fmt.Fprintf(w, format, a...) fmt.Fprintf(w, format, a...)
} }
func QPrintln(a ...any) { func Qprintln(a ...any) {
if enableQuiet { if enableQuiet {
return return
} }
@@ -68,15 +78,15 @@ func QPrintln(a ...any) {
fmt.Fprintln(w, a...) fmt.Fprintln(w, a...)
} }
func DPrint(a ...any) { func Dprint(a ...any) {
debug.Print(a...) debug.Print(a...)
} }
func DPrintf(format string, a ...any) { func Dprintf(format string, a ...any) {
debug.Printf(format, a...) debug.Printf(format, a...)
} }
func DPrintln(a ...any) { func Dprintln(a ...any) {
debug.Println(a...) debug.Println(a...)
} }
@@ -84,7 +94,7 @@ func StackTrace() {
debug.StackTrace() debug.StackTrace()
} }
func VPrint(a ...any) { func Vprint(a ...any) {
if !enableVerbose { if !enableVerbose {
return return
} }
@@ -92,7 +102,7 @@ func VPrint(a ...any) {
fmt.Fprint(w, a...) fmt.Fprint(w, a...)
} }
func VPrintf(format string, a ...any) { func Vprintf(format string, a ...any) {
if !enableVerbose { if !enableVerbose {
return return
} }
@@ -100,7 +110,7 @@ func VPrintf(format string, a ...any) {
fmt.Fprintf(w, format, a...) fmt.Fprintf(w, format, a...)
} }
func VPrintln(a ...any) { func Vprintln(a ...any) {
if !enableVerbose { if !enableVerbose {
return return
} }
@@ -123,5 +133,7 @@ func Println(a ...any) {
// SetWriter changes the output for messages. // SetWriter changes the output for messages.
func SetWriter(dst io.Writer) { func SetWriter(dst io.Writer) {
w = dst w = dst
dbgEnabled := debug.Enabled
debug = dbg.To(lib.WithCloser(w)) debug = dbg.To(lib.WithCloser(w))
debug.Enabled = dbgEnabled
} }

147
msg/msg_test.go Normal file
View File

@@ -0,0 +1,147 @@
package msg_test
import (
"bytes"
"testing"
"git.wntrmute.dev/kyle/goutils/msg"
)
func checkExpected(buf *bytes.Buffer, expected string) bool {
return buf.String() == expected
}
func resetBuf() *bytes.Buffer {
buf := &bytes.Buffer{}
msg.SetWriter(buf)
return buf
}
func TestVerbosePrint(t *testing.T) {
buf := resetBuf()
msg.SetVerbose(false) // ensure verbose is explicitly not set
msg.Vprint("hello, world")
if buf.Len() != 0 {
t.Fatalf("expected no output, have %s", buf.String())
}
msg.Vprintf("hello, %s", "world")
if buf.Len() != 0 {
t.Fatalf("expected no output, have %s", buf.String())
}
msg.Vprintln("hello, world")
if buf.Len() != 0 {
t.Fatalf("expected no output, have %s", buf.String())
}
msg.SetVerbose(true)
msg.Vprint("hello, world")
if !checkExpected(buf, "hello, world") {
t.Fatalf("expected output %q, have %q", "hello, world", buf.String())
}
buf.Reset()
msg.Vprintf("hello, %s", "world")
if !checkExpected(buf, "hello, world") {
t.Fatalf("expected output %q, have %q", "hello, world", buf.String())
}
buf.Reset()
msg.Vprintln("hello, world")
if !checkExpected(buf, "hello, world\n") {
t.Fatalf("expected output %q, have %q", "hello, world\n", buf.String())
}
}
func TestQuietPrint(t *testing.T) {
buf := resetBuf()
msg.SetQuiet(true)
msg.Qprint("hello, world")
if buf.Len() != 0 {
t.Fatalf("expected no output, have %s", buf.String())
}
msg.Qprintf("hello, %s", "world")
if buf.Len() != 0 {
t.Fatalf("expected no output, have %s", buf.String())
}
msg.Qprintln("hello, world")
if buf.Len() != 0 {
t.Fatalf("expected no output, have %s", buf.String())
}
msg.SetQuiet(false)
msg.Qprint("hello, world")
if !checkExpected(buf, "hello, world") {
t.Fatalf("expected output %q, have %q", "hello, world", buf.String())
}
buf.Reset()
msg.Qprintf("hello, %s", "world")
if !checkExpected(buf, "hello, world") {
t.Fatalf("expected output %q, have %q", "hello, world", buf.String())
}
buf.Reset()
msg.Qprintln("hello, world")
if !checkExpected(buf, "hello, world\n") {
t.Fatalf("expected output %q, have %q", "hello, world\n", buf.String())
}
}
func TestDebugPrint(t *testing.T) {
buf := resetBuf()
msg.SetDebug(false) // ensure debug is explicitly not set
msg.Dprint("hello, world")
if buf.Len() != 0 {
t.Fatalf("expected no output, have %s", buf.String())
}
msg.Dprintf("hello, %s", "world")
if buf.Len() != 0 {
t.Fatalf("expected no output, have %s", buf.String())
}
msg.Dprintln("hello, world")
if buf.Len() != 0 {
t.Fatalf("expected no output, have %s", buf.String())
}
msg.StackTrace()
if buf.Len() != 0 {
t.Fatalf("expected no output, have %s", buf.String())
}
msg.SetDebug(true)
msg.Dprint("hello, world")
if !checkExpected(buf, "hello, world") {
t.Fatalf("expected output %q, have %q", "hello, world", buf.String())
}
buf.Reset()
msg.Dprintf("hello, %s", "world")
if !checkExpected(buf, "hello, world") {
t.Fatalf("expected output %q, have %q", "hello, world", buf.String())
}
buf.Reset()
msg.Dprintln("hello, world")
if !checkExpected(buf, "hello, world\n") {
t.Fatalf("expected output %q, have %q", "hello, world\n", buf.String())
}
buf.Reset()
msg.StackTrace()
if buf.Len() == 0 {
t.Fatal("expected stack trace output, received no output")
}
}