Compare commits

..

43 Commits

Author SHA1 Message Date
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
d511aeb52d Update CHANGELOG for v1.16.0. 2025-11-20 18:22:00 -08:00
eac59fd5a6 msg: add new package for CLI output. 2025-11-20 18:21:01 -08:00
bd5ec3f425 cmd/kgz: linter fixes 2025-11-20 18:20:28 -08:00
b81709cfdd lib/fetch: documentation 2025-11-20 18:20:28 -08:00
8518cc6e56 lib: add DummyWriteCloser. 2025-11-20 18:20:28 -08:00
0bdd30f506 make the linter happy 2025-11-19 23:23:18 -08:00
0afa4b37b0 Update CHANGELOG for v1.15.8. 2025-11-19 22:12:15 -08:00
e9c7fec86f certlib: fix CSR FileKind, add test cases. 2025-11-19 22:09:24 -08:00
80b3376fa5 Update CHANGELOG for v1.15.7. 2025-11-19 14:50:22 -08:00
603724c2c9 cmd/tlsinfo: fix typo in output. 2025-11-19 14:48:02 -08:00
85de524a02 certlib/certgen: GenerateKey was generating wrong key type.
The ed25519 block was being used to generate RSA keys.
2025-11-19 14:46:54 -08:00
02fb85aec0 certlib: update FileKind with algo information.
Additionally, key algo wasn't being set on PEM files.
2025-11-19 14:46:17 -08:00
b1a2039c7d Update CHANGELOG for v1.15.6. 2025-11-19 09:46:09 -08:00
46c9976e73 certlib: Add file kind functionality. 2025-11-19 09:45:57 -08:00
5a5dd5e6ea Update CHANGELOG for v1.15.5. 2025-11-19 08:45:08 -08:00
3317b8c33b certlib/bundler: add support for pemcrt. 2025-11-19 08:43:46 -08:00
fb1b1ffcad Update CHANGELOG for v1.15.4. 2025-11-19 02:57:40 -08:00
7bb6973341 QoL for CSR generation. 2025-11-19 02:57:26 -08:00
8e997bda34 Update CHANGELOG for v1.15.3. 2025-11-19 02:43:47 -08:00
d76db4a947 Minor bug fixes. 2025-11-19 02:43:25 -08:00
7e36a828d4 Update CHANGELOG for v1.15.2. 2025-11-19 02:20:36 -08:00
8eaca580be Minor bug fixes. 2025-11-19 02:20:21 -08:00
fd31e31afa Update CHANGELOG for v1.15.1. 2025-11-19 01:47:48 -08:00
7426988ae4 linter fixes. 2025-11-19 01:47:42 -08:00
39 changed files with 1662 additions and 138 deletions

View File

@@ -101,7 +101,7 @@ linters:
- loggercheck # checks key value pairs for common logger libraries (kitlog,klog,logr,zap) - loggercheck # checks key value pairs for common logger libraries (kitlog,klog,logr,zap)
- makezero # finds slice declarations with non-zero initial length - makezero # finds slice declarations with non-zero initial length
- mirror # reports wrong mirror patterns of bytes/strings usage - mirror # reports wrong mirror patterns of bytes/strings usage
- mnd # detects magic numbers # - mnd # detects magic numbers
- modernize # suggests simplifications to Go code, using modern language and library features - modernize # suggests simplifications to Go code, using modern language and library features
- musttag # enforces field tags in (un)marshaled structs - musttag # enforces field tags in (un)marshaled structs
- nakedret # finds naked returns in functions greater than a specified function length - nakedret # finds naked returns in functions greater than a specified function length
@@ -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,97 @@
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
Added:
- msg: package for command line outputs.
Changed:
- lib: add DummyWriteCloser
- Miscellaneous linter fixes and documentation updates.
v1.15.8 - 2025-11-20
Changed:
- certlib: fix CSR FileKind, add test cases.
v1.15.7 - 2025-11-19
Changed:
- certlib: update FileKind with algo information and fix bug where PEM
files didn't have their algorithm set.
- certlib/certgen: GenerateKey had the blocks for Ed25519 and RSA keys
swapped.
- cmd/tlsinfo: fix type in output.
v1.15.6 - 2025-11-19
certlib: add FileKind function to determine file type.
v1.15.5 - 2025-11-19
certlib/bundler: add support for crt files that are pem-encoded.
v1.15.4 - 2025-11-19
Quality of life fixes for CSR generation.
v1.15.3 - 2025-11-19
Minor bug fixes.
v1.15.2 - 2025-11-19
Minor bug fixes.
v1.15.1 - 2025-11-19
Changed:
- linter fixes.
Removed:
- mnd removed from linter.
v1.15.0 - 2025-11-19 v1.15.0 - 2025-11-19
Changed: Changed:

View File

@@ -84,6 +84,7 @@ Contents:
lib/ Commonly-useful functions for writing Go programs. lib/ Commonly-useful functions for writing Go programs.
log/ A syslog library. log/ A syslog library.
logging/ A logging library. logging/ A logging library.
msg/ Output library for command line programs.
mwc/ MultiwriteCloser implementation. mwc/ MultiwriteCloser implementation.
sbuf/ A byte buffer that can be wiped. sbuf/ A byte buffer that can be wiped.
seekbuf/ A read-seekable byte buffer. seekbuf/ A read-seekable byte buffer.

View File

@@ -422,6 +422,24 @@ func encodeCertsToFiles(
name: baseName + ".pem", name: baseName + ".pem",
content: pemContent, content: pemContent,
}) })
case "crt":
pemContent := encodeCertsToPEM(certs)
files = append(files, fileEntry{
name: baseName + ".crt",
content: pemContent,
})
case "pemcrt":
pemContent := encodeCertsToPEM(certs)
files = append(files, fileEntry{
name: baseName + ".pem",
content: pemContent,
})
pemContent = encodeCertsToPEM(certs)
files = append(files, fileEntry{
name: baseName + ".crt",
content: pemContent,
})
case "der": case "der":
if isSingle { if isSingle {
// For single file in DER, concatenate all cert DER bytes // For single file in DER, concatenate all cert DER bytes
@@ -430,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,
}) })
} }
@@ -454,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"`
} }
@@ -63,12 +72,7 @@ type CertificateRequest struct {
Profile Profile `yaml:"profile"` Profile Profile `yaml:"profile"`
} }
func (cs CertificateRequest) Generate() (crypto.PrivateKey, *x509.CertificateRequest, error) { func (cs CertificateRequest) Request(priv crypto.PrivateKey) (*x509.CertificateRequest, error) {
pub, priv, err := cs.KeySpec.Generate()
if err != nil {
return nil, nil, err
}
subject := pkix.Name{} subject := pkix.Name{}
subject.CommonName = cs.Subject.CommonName subject.CommonName = cs.Subject.CommonName
subject.Country = []string{cs.Subject.Country} subject.Country = []string{cs.Subject.Country}
@@ -81,37 +85,59 @@ func (cs CertificateRequest) Generate() (crypto.PrivateKey, *x509.CertificateReq
for i, ip := range cs.Subject.IPAddresses { for i, ip := range cs.Subject.IPAddresses {
ipAddresses = append(ipAddresses, net.ParseIP(ip)) ipAddresses = append(ipAddresses, net.ParseIP(ip))
if ipAddresses[i] == nil { if ipAddresses[i] == nil {
return nil, nil, fmt.Errorf("invalid IP address: %s", ip) return nil, fmt.Errorf("invalid IP address: %s", ip)
} }
} }
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: pub, PublicKey: getPublic(priv),
Subject: subject, Subject: subject,
DNSNames: cs.Subject.DNSNames, EmailAddresses: cs.Subject.Email,
DNSNames: dnsNames,
IPAddresses: ipAddresses, IPAddresses: ipAddresses,
} }
reqBytes, err := x509.CreateCertificateRequest(rand.Reader, req, priv) reqBytes, err := x509.CreateCertificateRequest(rand.Reader, req, priv)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("failed to create certificate request: %w", err) return nil, fmt.Errorf("failed to create certificate request: %w", err)
} }
req, err = x509.ParseCertificateRequest(reqBytes) req, err = x509.ParseCertificateRequest(reqBytes)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("failed to parse certificate request: %w", err) return nil, fmt.Errorf("failed to parse certificate request: %w", err)
}
return req, nil
}
func (cs CertificateRequest) Generate() (crypto.PrivateKey, *x509.CertificateRequest, error) {
_, priv, err := cs.KeySpec.Generate()
if err != nil {
return nil, nil, err
}
req, err := cs.Request(priv)
if err != nil {
return nil, nil, err
} }
return priv, req, nil return priv, req, nil
} }
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) {
@@ -140,25 +166,38 @@ 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)
}
certTemplate.KeyUsage |= ku
} }
var eku x509.ExtKeyUsage
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
} }
func (p Profile) SignRequest(parent *x509.Certificate, req *x509.CertificateRequest, priv crypto.PrivateKey) (*x509.Certificate, error) { func (p Profile) SignRequest(
parent *x509.Certificate,
req *x509.CertificateRequest,
priv crypto.PrivateKey,
) (*x509.Certificate, error) {
tpl, err := p.templateFromRequest(req) tpl, err := p.templateFromRequest(req)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create certificate template: %w", err) return nil, fmt.Errorf("failed to create certificate template: %w", err)
@@ -186,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

@@ -8,13 +8,16 @@ import (
"crypto/rand" "crypto/rand"
"crypto/rsa" "crypto/rsa"
"crypto/x509" "crypto/x509"
"encoding/asn1"
"errors" "errors"
"fmt" "fmt"
) )
var ( // var (
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) {
@@ -23,12 +26,17 @@ func GenerateKey(algorithm x509.PublicKeyAlgorithm, bitSize int) (crypto.PublicK
var err error var err error
switch algorithm { switch algorithm {
case x509.RSA:
pub, key, err = ed25519.GenerateKey(rand.Reader)
case x509.Ed25519: case x509.Ed25519:
pub, key, err = ed25519.GenerateKey(rand.Reader)
case x509.RSA:
key, err = rsa.GenerateKey(rand.Reader, bitSize) key, err = rsa.GenerateKey(rand.Reader, bitSize)
if err == nil { if err == nil {
pub = key.(*rsa.PrivateKey).Public() rsaPriv, ok := key.(*rsa.PrivateKey)
if !ok {
panic("failed to cast RSA private key to *rsa.PrivateKey")
}
pub = rsaPriv.Public()
} }
case x509.ECDSA: case x509.ECDSA:
var curve elliptic.Curve var curve elliptic.Curve
@@ -46,8 +54,17 @@ func GenerateKey(algorithm x509.PublicKeyAlgorithm, bitSize int) (crypto.PublicK
key, err = ecdsa.GenerateKey(curve, rand.Reader) key, err = ecdsa.GenerateKey(curve, rand.Reader)
if err == nil { if err == nil {
pub = key.(*ecdsa.PrivateKey).Public() ecPriv, ok := key.(*ecdsa.PrivateKey)
if !ok {
panic("failed to cast ECDSA private key to *ecdsa.PrivateKey")
}
pub = ecPriv.Public()
} }
case x509.DSA:
fallthrough
case x509.UnknownPublicKeyAlgorithm:
fallthrough
default: default:
err = errors.New("unsupported algorithm") err = errors.New("unsupported algorithm")
} }
@@ -58,3 +75,16 @@ func GenerateKey(algorithm x509.PublicKeyAlgorithm, bitSize int) (crypto.PublicK
return pub, key, nil return pub, key, nil
} }
func getPublic(priv crypto.PrivateKey) crypto.PublicKey {
switch priv := priv.(type) {
case *rsa.PrivateKey:
return &priv.PublicKey
case *ecdsa.PrivateKey:
return &priv.PublicKey
case *ed25519.PrivateKey:
return priv.Public()
default:
return nil
}
}

View File

@@ -1,11 +1,19 @@
package certlib package certlib
import ( import (
"bytes"
"crypto"
"crypto/dsa"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rsa"
"crypto/x509" "crypto/x509"
"encoding/pem" "encoding/pem"
"errors" "errors"
"fmt" "fmt"
"os" "os"
"strings"
"git.wntrmute.dev/kyle/goutils/certlib/certerr" "git.wntrmute.dev/kyle/goutils/certlib/certerr"
) )
@@ -13,6 +21,7 @@ import (
// ReadCertificate reads a DER or PEM-encoded certificate from the // ReadCertificate reads a DER or PEM-encoded certificate from the
// byte slice. // byte slice.
func ReadCertificate(in []byte) (*x509.Certificate, []byte, error) { func ReadCertificate(in []byte) (*x509.Certificate, []byte, error) {
in = bytes.TrimSpace(in)
if len(in) == 0 { if len(in) == 0 {
return nil, nil, certerr.ParsingError(certerr.ErrorSourceCertificate, certerr.ErrEmptyCertificate) return nil, nil, certerr.ParsingError(certerr.ErrorSourceCertificate, certerr.ErrEmptyCertificate)
} }
@@ -24,10 +33,10 @@ func ReadCertificate(in []byte) (*x509.Certificate, []byte, error) {
} }
rest := remaining rest := remaining
if p.Type != "CERTIFICATE" { if p.Type != pemTypeCertificate {
return nil, rest, certerr.ParsingError( return nil, rest, certerr.ParsingError(
certerr.ErrorSourceCertificate, certerr.ErrorSourceCertificate,
certerr.ErrInvalidPEMType(p.Type, "CERTIFICATE"), certerr.ErrInvalidPEMType(p.Type, pemTypeCertificate),
) )
} }
@@ -109,3 +118,184 @@ func PoolFromBytes(certBytes []byte) (*x509.CertPool, error) {
return pool, nil return pool, nil
} }
func ExportPrivateKeyPEM(priv crypto.PrivateKey) ([]byte, error) {
keyDER, err := x509.MarshalPKCS8PrivateKey(priv)
if err != nil {
return nil, err
}
return pem.EncodeToMemory(&pem.Block{Type: pemTypePrivateKey, Bytes: keyDER}), nil
}
func LoadCSR(path string) (*x509.CertificateRequest, error) {
in, err := os.ReadFile(path)
if err != nil {
return nil, certerr.LoadingError(certerr.ErrorSourceCSR, err)
}
req, _, err := ParseCSR(in)
return req, err
}
func ExportCSRAsPEM(req *x509.CertificateRequest) []byte {
return pem.EncodeToMemory(&pem.Block{Type: pemTypeCertificateRequest, Bytes: req.Raw})
}
type FileFormat uint8
const (
FormatPEM FileFormat = iota + 1
FormatDER
)
func (f FileFormat) String() string {
switch f {
case FormatPEM:
return "PEM"
case FormatDER:
return "DER"
default:
return "unknown"
}
}
type KeyAlgo struct {
Type x509.PublicKeyAlgorithm
Size int
curve elliptic.Curve
}
func (ka KeyAlgo) String() string {
switch ka.Type {
case x509.RSA:
return fmt.Sprintf("RSA-%d", ka.Size)
case x509.ECDSA:
if ka.curve == nil {
return fmt.Sprintf("ECDSA (unknown %d)", ka.Size)
}
return fmt.Sprintf("ECDSA-%s", ka.curve.Params().Name)
case x509.Ed25519:
return "Ed25519"
case x509.DSA:
return "DSA"
case x509.UnknownPublicKeyAlgorithm:
fallthrough // make linter happy
default:
return "unknown"
}
}
func publicKeyAlgoFromPublicKey(key crypto.PublicKey) KeyAlgo {
switch key := key.(type) {
case *rsa.PublicKey:
return KeyAlgo{
Type: x509.RSA,
Size: key.N.BitLen(),
}
case *ecdsa.PublicKey:
return KeyAlgo{
Type: x509.ECDSA,
curve: key.Curve,
Size: key.Params().BitSize,
}
case *ed25519.PublicKey:
return KeyAlgo{
Type: x509.Ed25519,
}
case *dsa.PublicKey:
return KeyAlgo{
Type: x509.DSA,
}
default:
return KeyAlgo{
Type: x509.UnknownPublicKeyAlgorithm,
}
}
}
func publicKeyAlgoFromKey(key crypto.PrivateKey) KeyAlgo {
switch key := key.(type) {
case *rsa.PrivateKey:
return KeyAlgo{
Type: x509.RSA,
Size: key.PublicKey.N.BitLen(),
}
case *ecdsa.PrivateKey:
return KeyAlgo{
Type: x509.ECDSA,
curve: key.PublicKey.Curve,
Size: key.Params().BitSize,
}
case *ed25519.PrivateKey:
return KeyAlgo{
Type: x509.Ed25519,
}
case *dsa.PrivateKey:
return KeyAlgo{
Type: x509.DSA,
}
default:
return KeyAlgo{
Type: x509.UnknownPublicKeyAlgorithm,
}
}
}
func publicKeyAlgoFromCert(cert *x509.Certificate) KeyAlgo {
return publicKeyAlgoFromPublicKey(cert.PublicKey)
}
func publicKeyAlgoFromCSR(csr *x509.CertificateRequest) KeyAlgo {
return publicKeyAlgoFromPublicKey(csr.PublicKey)
}
type FileType struct {
Format FileFormat
Type string
Algo KeyAlgo
}
func (ft FileType) String() string {
if ft.Type == "" {
return ft.Format.String()
}
return fmt.Sprintf("%s %s (%s)", ft.Algo, ft.Type, ft.Format)
}
// FileKind returns the file type of the given file.
func FileKind(path string) (*FileType, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
ft := &FileType{Format: FormatDER}
block, _ := pem.Decode(data)
if block != nil {
data = block.Bytes
ft.Type = strings.ToLower(block.Type)
ft.Format = FormatPEM
}
cert, err := x509.ParseCertificate(data)
if err == nil {
ft.Algo = publicKeyAlgoFromCert(cert)
return ft, nil
}
csr, err := x509.ParseCertificateRequest(data)
if err == nil {
ft.Algo = publicKeyAlgoFromCSR(csr)
return ft, nil
}
priv, err := x509.ParsePKCS8PrivateKey(data)
if err == nil {
ft.Algo = publicKeyAlgoFromKey(priv)
return ft, nil
}
return nil, errors.New("certlib; unknown file type")
}

View File

@@ -2,7 +2,10 @@
package certlib package certlib
import ( import (
"crypto/elliptic"
"crypto/x509"
"fmt" "fmt"
"strings"
"testing" "testing"
"git.wntrmute.dev/kyle/goutils/assert" "git.wntrmute.dev/kyle/goutils/assert"
@@ -138,3 +141,153 @@ func TestReadCertificates(t *testing.T) {
assert.BoolT(t, cert != nil, "lib: expected an actual certificate to have been returned") assert.BoolT(t, cert != nil, "lib: expected an actual certificate to have been returned")
} }
} }
var (
ecTestCACert = "testdata/ec-ca-cert.pem"
ecTestCAPriv = "testdata/ec-ca-priv.pem"
ecTestCAReq = "testdata/ec-ca-cert.csr"
rsaTestCACert = "testdata/rsa-ca-cert.pem"
rsaTestCAPriv = "testdata/rsa-ca-priv.pem"
rsaTestCAReq = "testdata/rsa-ca-cert.csr"
)
func TestFileTypeECPrivate(t *testing.T) {
ft, err := FileKind(ecTestCAPriv)
assert.NoErrorT(t, err)
if ft.Format != FormatPEM {
t.Errorf("certlib: expected format '%s', got '%s'", FormatPEM, ft.Format)
}
if ft.Type != strings.ToLower(pemTypePrivateKey) {
t.Errorf("certlib: expected type '%s', got '%s'",
strings.ToLower(pemTypePrivateKey), ft.Type)
}
expectedAlgo := KeyAlgo{
Type: x509.ECDSA,
Size: 521,
curve: elliptic.P521(),
}
if ft.Algo.String() != expectedAlgo.String() {
t.Errorf("certlib: expected algo '%s', got '%s'", expectedAlgo, ft.Algo)
}
}
func TestFileTypeECCertRequest(t *testing.T) {
ft, err := FileKind(ecTestCAReq)
assert.NoErrorT(t, err)
if ft.Format != FormatPEM {
t.Errorf("certlib: expected format '%s', got '%s'", FormatPEM, ft.Format)
}
if ft.Type != strings.ToLower(pemTypeCertificateRequest) {
t.Errorf("certlib: expected type '%s', got '%s'",
strings.ToLower(pemTypeCertificateRequest), ft.Type)
}
expectedAlgo := KeyAlgo{
Type: x509.ECDSA,
Size: 521,
curve: elliptic.P521(),
}
if ft.Algo.String() != expectedAlgo.String() {
t.Errorf("certlib: expected algo '%s', got '%s'", expectedAlgo, ft.Algo)
}
}
func TestFileTypeECCertificate(t *testing.T) {
ft, err := FileKind(ecTestCACert)
assert.NoErrorT(t, err)
if ft.Format != FormatPEM {
t.Errorf("certlib: expected format '%s', got '%s'", FormatPEM, ft.Format)
}
if ft.Type != strings.ToLower(pemTypeCertificate) {
t.Errorf("certlib: expected type '%s', got '%s'",
strings.ToLower(pemTypeCertificate), ft.Type)
}
expectedAlgo := KeyAlgo{
Type: x509.ECDSA,
Size: 521,
curve: elliptic.P521(),
}
if ft.Algo.String() != expectedAlgo.String() {
t.Errorf("certlib: expected algo '%s', got '%s'", expectedAlgo, ft.Algo)
}
}
func TestFileTypeRSAPrivate(t *testing.T) {
ft, err := FileKind(rsaTestCAPriv)
assert.NoErrorT(t, err)
if ft.Format != FormatPEM {
t.Errorf("certlib: expected format '%s', got '%s'", FormatPEM, ft.Format)
}
if ft.Type != strings.ToLower(pemTypePrivateKey) {
t.Errorf("certlib: expected type '%s', got '%s'",
strings.ToLower(pemTypePrivateKey), ft.Type)
}
expectedAlgo := KeyAlgo{
Type: x509.RSA,
Size: 4096,
}
if ft.Algo.String() != expectedAlgo.String() {
t.Errorf("certlib: expected algo '%s', got '%s'", expectedAlgo, ft.Algo)
}
}
func TestFileTypeRSACertRequest(t *testing.T) {
ft, err := FileKind(rsaTestCAReq)
assert.NoErrorT(t, err)
if ft.Format != FormatPEM {
t.Errorf("certlib: expected format '%s', got '%s'", FormatPEM, ft.Format)
}
if ft.Type != strings.ToLower(pemTypeCertificateRequest) {
t.Errorf("certlib: expected type '%s', got '%s'",
strings.ToLower(pemTypeCertificateRequest), ft.Type)
}
expectedAlgo := KeyAlgo{
Type: x509.RSA,
Size: 4096,
}
if ft.Algo.String() != expectedAlgo.String() {
t.Errorf("certlib: expected algo '%s', got '%s'", expectedAlgo, ft.Algo)
}
}
func TestFileTypeRSACertificate(t *testing.T) {
ft, err := FileKind(rsaTestCACert)
assert.NoErrorT(t, err)
if ft.Format != FormatPEM {
t.Errorf("certlib: expected format '%s', got '%s'", FormatPEM, ft.Format)
}
if ft.Type != strings.ToLower(pemTypeCertificate) {
t.Errorf("certlib: expected type '%s', got '%s'",
strings.ToLower(pemTypeCertificate), ft.Type)
}
expectedAlgo := KeyAlgo{
Type: x509.RSA,
Size: 4096,
}
if ft.Algo.String() != expectedAlgo.String() {
t.Errorf("certlib: expected algo '%s', got '%s'", expectedAlgo, ft.Algo)
}
}

View File

@@ -54,8 +54,6 @@ var extKeyUsages = map[x509.ExtKeyUsage]string{
x509.ExtKeyUsageMicrosoftKernelCodeSigning: "microsoft kernel code signing", x509.ExtKeyUsageMicrosoftKernelCodeSigning: "microsoft kernel code signing",
} }
func sigAlgoPK(a x509.SignatureAlgorithm) string { func sigAlgoPK(a x509.SignatureAlgorithm) string {
switch a { switch a {
case x509.MD2WithRSA, x509.MD5WithRSA, x509.SHA1WithRSA, x509.SHA256WithRSA, x509.SHA384WithRSA, x509.SHA512WithRSA: case x509.MD2WithRSA, x509.MD5WithRSA, x509.SHA1WithRSA, x509.SHA256WithRSA, x509.SHA384WithRSA, x509.SHA512WithRSA:
@@ -167,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
@@ -251,11 +271,6 @@ func showBasicConstraints(cert *x509.Certificate) {
fmt.Fprintln(os.Stdout) fmt.Fprintln(os.Stdout)
} }
var (
dateFormat string
showHash bool // if true, print a SHA256 hash of the certificate's Raw field
)
func wrapPrint(text string, indent int) { func wrapPrint(text string, indent int) {
tabs := "" tabs := ""
var tabsSb140 strings.Builder var tabsSb140 strings.Builder
@@ -267,11 +282,12 @@ func wrapPrint(text string, indent int) {
fmt.Fprintf(os.Stdout, tabs+"%s\n", wrap(text, indent)) fmt.Fprintf(os.Stdout, tabs+"%s\n", wrap(text, indent))
} }
func DisplayCert(w io.Writer, cert *x509.Certificate) { func DisplayCert(w io.Writer, cert *x509.Certificate, showHash bool) {
fmt.Fprintln(w, "CERTIFICATE") fmt.Fprintln(w, "CERTIFICATE")
if showHash { if showHash {
fmt.Fprintln(w, wrap(fmt.Sprintf("SHA256: %x", sha256.Sum256(cert.Raw)), 0)) fmt.Fprintln(w, wrap(fmt.Sprintf("SHA256: %x", sha256.Sum256(cert.Raw)), 0))
} }
fmt.Fprintln(w, wrap("Subject: "+DisplayName(cert.Subject), 0)) fmt.Fprintln(w, wrap("Subject: "+DisplayName(cert.Subject), 0))
fmt.Fprintln(w, wrap("Issuer: "+DisplayName(cert.Issuer), 0)) fmt.Fprintln(w, wrap("Issuer: "+DisplayName(cert.Issuer), 0))
fmt.Fprintf(w, "\tSignature algorithm: %s / %s\n", sigAlgoPK(cert.SignatureAlgorithm), fmt.Fprintf(w, "\tSignature algorithm: %s / %s\n", sigAlgoPK(cert.SignatureAlgorithm),
@@ -287,8 +303,8 @@ func DisplayCert(w io.Writer, cert *x509.Certificate) {
fmt.Fprintf(w, "\t%s\n", wrap("SKI: "+dumpHex(cert.SubjectKeyId), 1)) fmt.Fprintf(w, "\t%s\n", wrap("SKI: "+dumpHex(cert.SubjectKeyId), 1))
} }
wrapPrint("Valid from: "+cert.NotBefore.Format(dateFormat), 1) wrapPrint("Valid from: "+cert.NotBefore.Format(lib.DateShortFormat), 1)
fmt.Fprintf(w, "\t until: %s\n", cert.NotAfter.Format(dateFormat)) fmt.Fprintf(w, "\t until: %s\n", cert.NotAfter.Format(lib.DateShortFormat))
fmt.Fprintf(w, "\tKey usages: %s\n", keyUsages(cert.KeyUsage)) fmt.Fprintf(w, "\tKey usages: %s\n", keyUsages(cert.KeyUsage))
if len(cert.ExtKeyUsage) > 0 { if len(cert.ExtKeyUsage) > 0 {
@@ -339,3 +355,36 @@ func DisplayCert(w io.Writer, cert *x509.Certificate) {
} }
} }
} }
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

@@ -75,6 +75,12 @@ var DelegationExtension = pkix.Extension{
Value: []byte{0x05, 0x00}, // ASN.1 NULL Value: []byte{0x05, 0x00}, // ASN.1 NULL
} }
const (
pemTypeCertificate = "CERTIFICATE"
pemTypeCertificateRequest = "CERTIFICATE REQUEST"
pemTypePrivateKey = "PRIVATE KEY"
)
// InclusiveDate returns the time.Time representation of a date - 1 // InclusiveDate returns the time.Time representation of a date - 1
// nanosecond. This allows time.After to be used inclusively. // nanosecond. This allows time.After to be used inclusively.
func InclusiveDate(year int, month time.Month, day int) time.Time { func InclusiveDate(year int, month time.Month, day int) time.Time {
@@ -246,7 +252,7 @@ func EncodeCertificatesPEM(certs []*x509.Certificate) []byte {
var buffer bytes.Buffer var buffer bytes.Buffer
for _, cert := range certs { for _, cert := range certs {
if err := pem.Encode(&buffer, &pem.Block{ if err := pem.Encode(&buffer, &pem.Block{
Type: "CERTIFICATE", Type: pemTypeCertificate,
Bytes: cert.Raw, Bytes: cert.Raw,
}); err != nil { }); err != nil {
return nil return nil

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

@@ -1,6 +1,10 @@
package certlib package certlib_test
import "testing" import (
"testing"
"git.wntrmute.dev/kyle/goutils/certlib"
)
var ( var (
testCert1 = "testdata/cert1.pem" testCert1 = "testdata/cert1.pem"
@@ -16,25 +20,25 @@ type testCase struct {
} }
var testCases = []testCase{ var testCases = []testCase{
{"testdata/cert1.pem", "testdata/priv1.pem", true}, {testCert1, testPriv1, true},
{"testdata/cert2.pem", "testdata/priv2.pem", true}, {testCert2, testPriv2, true},
{"testdata/cert1.pem", "testdata/priv2.pem", false}, {testCert1, testPriv2, false},
{"testdata/cert2.pem", "testdata/priv1.pem", false}, {testCert2, testPriv1, false},
} }
func TestMatchKeys(t *testing.T) { func TestMatchKeys(t *testing.T) {
for i, tc := range testCases { for i, tc := range testCases {
cert, err := LoadCertificate(tc.cert) cert, err := certlib.LoadCertificate(tc.cert)
if err != nil { if err != nil {
t.Fatalf("failed to load cert %d: %v", i, err) t.Fatalf("failed to load cert %d: %v", i, err)
} }
priv, err := LoadPrivateKey(tc.key) priv, err := certlib.LoadPrivateKey(tc.key)
if err != nil { if err != nil {
t.Fatalf("failed to load key %d: %v", i, err) t.Fatalf("failed to load key %d: %v", i, err)
} }
ok, _ := MatchKeys(cert, priv) ok, _ := certlib.MatchKeys(cert, priv)
switch { switch {
case ok && !tc.match: case ok && !tc.match:
t.Fatalf("case %d: cert %s/key %s should not match", i, tc.cert, tc.key) t.Fatalf("case %d: cert %s/key %s should not match", i, tc.cert, tc.key)

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

12
certlib/testdata/ec-ca-cert.csr vendored Normal file
View File

@@ -0,0 +1,12 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIBzTCCAS4CAQAwgYgxCzAJBgNVBAYTAlVTMQkwBwYDVQQIEwAxCTAHBgNVBAcT
ADEiMCAGA1UEChMZV05UUk1VVEUgSEVBVlkgSU5EVVNUUklFUzEfMB0GA1UECxMW
Q1JZUFRPR1JBUEhJQyBTRVJWSUNFUzEeMBwGA1UEAxMVV05UUk1VVEUgVEVTVCBF
QyBDQSAxMIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAQxmTxzo1XOK0HDrtn92b
exC4sXr8GnU+oATiXied3e1AWVOux9XtaWduY+a+r6Kb1rxMVyebn9KqtwNw+9KS
XaEB1IN9QzfdxEcJgRIAVtFplOqCip5xKK0B+woo3wXm3ndq2kJts86aONqQ0m2g
RrsmAKAX4pwmMnAHFF7veBcpsqugADAKBggqhkjOPQQDBAOBjAAwgYgCQgDG8Hdu
FkC3z0u0MU01+Bi/2MorcVTvdkurLm6Rh2Zf65aaXK8PDdV/cPZ98qx7NoLDSvwF
83gJuUI/3nVB/Ith7wJCAb6SAkXroT7y41XHayyTYb6+RKSlxxb9e5rtVCp/nG23
s59r23vUC/wDb4VWJE5jKi5vmXfjY+RAL9FOnpr2wsX0
-----END CERTIFICATE REQUEST-----

18
certlib/testdata/ec-ca-cert.pem vendored Normal file
View File

@@ -0,0 +1,18 @@
-----BEGIN CERTIFICATE-----
MIIC4TCCAkKgAwIBAgIUSnrCuvU8kj0nxNzmTgibiPLrQ8QwCgYIKoZIzj0EAwQw
gYgxCzAJBgNVBAYTAlVTMQkwBwYDVQQIEwAxCTAHBgNVBAcTADEiMCAGA1UEChMZ
V05UUk1VVEUgSEVBVlkgSU5EVVNUUklFUzEfMB0GA1UECxMWQ1JZUFRPR1JBUEhJ
QyBTRVJWSUNFUzEeMBwGA1UEAxMVV05UUk1VVEUgVEVTVCBFQyBDQSAxMB4XDTI1
MTExOTIwNTgwMVoXDTQ1MTExNDIxNTgwMVowgYgxCzAJBgNVBAYTAlVTMQkwBwYD
VQQIEwAxCTAHBgNVBAcTADEiMCAGA1UEChMZV05UUk1VVEUgSEVBVlkgSU5EVVNU
UklFUzEfMB0GA1UECxMWQ1JZUFRPR1JBUEhJQyBTRVJWSUNFUzEeMBwGA1UEAxMV
V05UUk1VVEUgVEVTVCBFQyBDQSAxMIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQA
QxmTxzo1XOK0HDrtn92bexC4sXr8GnU+oATiXied3e1AWVOux9XtaWduY+a+r6Kb
1rxMVyebn9KqtwNw+9KSXaEB1IN9QzfdxEcJgRIAVtFplOqCip5xKK0B+woo3wXm
3ndq2kJts86aONqQ0m2gRrsmAKAX4pwmMnAHFF7veBcpsqujRTBDMA4GA1UdDwEB
/wQEAwICBDASBgNVHRMBAf8ECDAGAQH/AgEDMB0GA1UdDgQWBBSNqRkvwUgIHGa2
jKmA2Q3w6Ju/FzAKBggqhkjOPQQDBAOBjAAwgYgCQgCckIFCjzJExxbV9dqm92nr
safC3kqhCxjmilf0IYWVj5f1kymoFr3jPpmy0iFcteUk0QTcqpnUT4i140lxtyK8
NAJCAVxbicZgVns9rgp6hu14l81j0XMpNgzy0QxscjMpWS/17iDJ4Y5vCWpwekrr
F1cmmRpsodONacAvTml4ehKE2ekx
-----END CERTIFICATE-----

8
certlib/testdata/ec-ca-priv.pem vendored Normal file
View File

@@ -0,0 +1,8 @@
-----BEGIN PRIVATE KEY-----
MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIAzkf/rvLGJBTVHHHr
lUhzsRJZgkyzSY5YE3KBReDyFWc+OB48C1gdYB1u7+PxgyfwYACjPx2y1AxN8fJh
XonY39mhgYkDgYYABABDGZPHOjVc4rQcOu2f3Zt7ELixevwadT6gBOJeJ53d7UBZ
U67H1e1pZ25j5r6vopvWvExXJ5uf0qq3A3D70pJdoQHUg31DN93ERwmBEgBW0WmU
6oKKnnEorQH7CijfBebed2raQm2zzpo42pDSbaBGuyYAoBfinCYycAcUXu94Fymy
qw==
-----END PRIVATE KEY-----

14
certlib/testdata/ec-ca.yaml vendored Normal file
View File

@@ -0,0 +1,14 @@
key:
algorithm: ecdsa
size: 521
subject:
common_name: WNTRMUTE TEST EC CA 1
country: US
organization: WNTRMUTE HEAVY INDUSTRIES
organizational_unit: CRYPTOGRAPHIC SERVICES
profile:
is_ca: true
path_len: 3
key_uses:
- cert sign
expiry: 20y

28
certlib/testdata/rsa-ca-cert.csr vendored Normal file
View File

@@ -0,0 +1,28 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIEzzCCArcCAQAwgYkxCzAJBgNVBAYTAlVTMQkwBwYDVQQIEwAxCTAHBgNVBAcT
ADEiMCAGA1UEChMZV05UUk1VVEUgSEVBVlkgSU5EVVNUUklFUzEfMB0GA1UECxMW
Q1JZUFRPR1JBUEhJQyBTRVJWSUNFUzEfMB0GA1UEAxMWV05UUk1VVEUgVEVTVCBS
U0EgQ0EgMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANq2EqMMNnQD
x/FwQ9Xf+UqYJCsdeSxeRDk9CGRbsToKeBlYfsOMgZ3pghsZ1srnJyB+pF1cSM1q
PJCXCvRdn11Q+FfZ25ye3pOaAY589GJSbEpcxitweJ7dsiY3sbqZjh5XnmwX5qHy
CE2qamKKJoAUkJ1YH/gWqX4bMYPG5oRo6KpCxb6pKi5ScMTl7kvn9fagkHEVJLf2
ZrQMWzTDwijjJGsKcjMWVZQegP9ODC+wut4uq1ZIFaXGW+dlrQkowVIZXZrBkL3l
s3u4RJiDadOSvEH3VJB9yjz9/LKT+JFUzgbMWCyZ2Gq3gr/HY+Xsodu8JsPqQxAW
PCxi19gi+Mx7Mk7jOqBShfDXby15mnqJxFU5VcjPtX5jPPIvDsF46IJX5lOwSNJa
VQsp/s54OL4bzbel/BsHWztRcDNzAxvOW3edZHzCE+o7UWkMwvJER+ciAfJSSm8s
oG5QiL5GdMvtiqwQe/l8bkbEws4OAnks9U+U9/5S3kLJq93Mw+oeId4m8bRGqCFB
QF9OWaZOOHO5kET89jr/UF0Udi6IMNIvj1fbTJVKZdM4gDEcLHTiev3Wqhmsy+4m
R7nVdr0bC8y5INLQ4aI4N4BUlzWUopWdFBasZYaJdWqt5sBVYHvEVvkThlJoDlCm
mBPQC7TtvqUA0lEhIgWteR33FU/D+OfTAgMBAAGgADANBgkqhkiG9w0BAQsFAAOC
AgEAOVmZNOOcyFMCF7p1ea7POU2Ev6l5x3vBnxqss+spRj07qWGKbKaFi6/smGoy
If2SYSFY0bJi1wzuz78m2DQfQDl1AAxKdd33prFs1+nOsQPKuVAmMETKW8t+ZRQd
hLq1I7aGcJjCU0nXnXEFM7XHJ2uUf1Af4WTCYOV8BvKanCz+xuTnjjW0fOYx6pZU
3lPAl5e4lNlbrsF9SNomX6u0zdmjECxSmDbDl/XIx5NB0wzdBwmm6QO2Ulp+ytr1
85OmOC6RxL+cBIS42k9WIZpYo6xRtJSoHhtpPHyWkDOnL32okxcZ4hfas3rXmpS+
E0S+r39+f3a7W3U3sq6lkZ1o5EUuqzkwX70XSMHVypRN1HZDEXPvH5CM9pns4iTq
FQoKWFjn7ZY9eazILtzlwAk5JalK0U4oQZwbtBl4EP5Dhmeok5u3QByAxD1wXC3p
RZvEBEXmZ4BvNjol6aHPLTb7ff2urnLMWRJklM4JN9OB+IdWPvDzjbzwPwxGuwow
TUr/Mmheps4YlcWQZxWsRJHAqCr/cw3EczMLqJ46KFqjj8qu5w8y5zKgt48PckD8
MnV35R2B04STrxnN2vINt7/SkCxlwk45/wMnyi2/GKO2N9GS9DI10SbVrvul3TTk
t0DJsQobX+ew2Cn4aSbHSSQG2tsE3gUVomwEjuyGDP1TIFY=
-----END CERTIFICATE REQUEST-----

34
certlib/testdata/rsa-ca-cert.pem vendored Normal file
View File

@@ -0,0 +1,34 @@
-----BEGIN CERTIFICATE-----
MIIF6DCCA9CgAwIBAgIVANc3mjaz6CKa3IT0+lJZ/hxvcbw6MA0GCSqGSIb3DQEB
CwUAMIGJMQswCQYDVQQGEwJVUzEJMAcGA1UECBMAMQkwBwYDVQQHEwAxIjAgBgNV
BAoTGVdOVFJNVVRFIEhFQVZZIElORFVTVFJJRVMxHzAdBgNVBAsTFkNSWVBUT0dS
QVBISUMgU0VSVklDRVMxHzAdBgNVBAMTFldOVFJNVVRFIFRFU1QgUlNBIENBIDEw
HhcNMjUxMTE5MjE1NzQ1WhcNNDUxMTE0MjI1NzQ1WjCBiTELMAkGA1UEBhMCVVMx
CTAHBgNVBAgTADEJMAcGA1UEBxMAMSIwIAYDVQQKExlXTlRSTVVURSBIRUFWWSBJ
TkRVU1RSSUVTMR8wHQYDVQQLExZDUllQVE9HUkFQSElDIFNFUlZJQ0VTMR8wHQYD
VQQDExZXTlRSTVVURSBURVNUIFJTQSBDQSAxMIICIjANBgkqhkiG9w0BAQEFAAOC
Ag8AMIICCgKCAgEA2rYSoww2dAPH8XBD1d/5SpgkKx15LF5EOT0IZFuxOgp4GVh+
w4yBnemCGxnWyucnIH6kXVxIzWo8kJcK9F2fXVD4V9nbnJ7ek5oBjnz0YlJsSlzG
K3B4nt2yJjexupmOHleebBfmofIITapqYoomgBSQnVgf+Bapfhsxg8bmhGjoqkLF
vqkqLlJwxOXuS+f19qCQcRUkt/ZmtAxbNMPCKOMkawpyMxZVlB6A/04ML7C63i6r
VkgVpcZb52WtCSjBUhldmsGQveWze7hEmINp05K8QfdUkH3KPP38spP4kVTOBsxY
LJnYareCv8dj5eyh27wmw+pDEBY8LGLX2CL4zHsyTuM6oFKF8NdvLXmaeonEVTlV
yM+1fmM88i8OwXjoglfmU7BI0lpVCyn+zng4vhvNt6X8GwdbO1FwM3MDG85bd51k
fMIT6jtRaQzC8kRH5yIB8lJKbyygblCIvkZ0y+2KrBB7+XxuRsTCzg4CeSz1T5T3
/lLeQsmr3czD6h4h3ibxtEaoIUFAX05Zpk44c7mQRPz2Ov9QXRR2Logw0i+PV9tM
lUpl0ziAMRwsdOJ6/daqGazL7iZHudV2vRsLzLkg0tDhojg3gFSXNZSilZ0UFqxl
hol1aq3mwFVge8RW+ROGUmgOUKaYE9ALtO2+pQDSUSEiBa15HfcVT8P459MCAwEA
AaNFMEMwDgYDVR0PAQH/BAQDAgIEMBIGA1UdEwEB/wQIMAYBAf8CAQMwHQYDVR0O
BBYEFAf60HUhXFOzcdtO8MJC2sN5qsmmMA0GCSqGSIb3DQEBCwUAA4ICAQAHBYjp
hN6U00cqqU/tk1CyUuJsPq2tGGIb3PxN+PvGLrhx27P+F8a5Sn2zBbkweX5vCu+i
o8EPavHAARIA+gF0UyM5MwPZdjdhNHDRGdASPphx7ZBa0e5Qp2XFyruw6EwHztyK
m7cF45MslGiEjRc7cciR5AUElRFhgY2QAlCcA8Tp6h3XJVSlaDhf+sS1EWlseVJN
GU5+Mu1L9vA6aiCKVtDviETfr7PmSY1obMrq9pDIoyo1jwflu/kTtmqDkDMkI1MI
mGKoHuKfAtZHiavjL7DMilO6X6ZMNPSYl4snm2hovHnoemifGuwlJ/V+HnDIMQAs
B5U3NY+IV6vlEYW3CmUfTsFjUzVpS/o/X5GBhG3pTAg9jUgpVsLNuVJrCg5PNpSL
xXMWRxj/y5ITm0m0/agNAd80KEDvCTbdORdDz4iYVG/L/GoaH3yPcmrBsE+2pPQb
rR1ihPU02wjY/oqlVt3mNzqczXZYoOW7FoW3O4dpP10kPA4O17nUJJ0FOU/vWXCS
7TgJwdlzoTPptK7c9zoZcHwPY2j0BVVgSofKlKlR1tJvqxbDA16pw2nsWl+r53Uc
Emw7SdHQfvDdbt42PL9g1CYqiYba7J9WkRWOYegSdOYLuaddYKN36xhCwT6p2/HM
EaRCxfUq2tmFzL2NhJLJlvNhpe7Zt5s/UF1oiQ==
-----END CERTIFICATE-----

52
certlib/testdata/rsa-ca-priv.pem vendored Normal file
View File

@@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDathKjDDZ0A8fx
cEPV3/lKmCQrHXksXkQ5PQhkW7E6CngZWH7DjIGd6YIbGdbK5ycgfqRdXEjNajyQ
lwr0XZ9dUPhX2ducnt6TmgGOfPRiUmxKXMYrcHie3bImN7G6mY4eV55sF+ah8ghN
qmpiiiaAFJCdWB/4Fql+GzGDxuaEaOiqQsW+qSouUnDE5e5L5/X2oJBxFSS39ma0
DFs0w8Io4yRrCnIzFlWUHoD/TgwvsLreLqtWSBWlxlvnZa0JKMFSGV2awZC95bN7
uESYg2nTkrxB91SQfco8/fyyk/iRVM4GzFgsmdhqt4K/x2Pl7KHbvCbD6kMQFjws
YtfYIvjMezJO4zqgUoXw128teZp6icRVOVXIz7V+YzzyLw7BeOiCV+ZTsEjSWlUL
Kf7OeDi+G823pfwbB1s7UXAzcwMbzlt3nWR8whPqO1FpDMLyREfnIgHyUkpvLKBu
UIi+RnTL7YqsEHv5fG5GxMLODgJ5LPVPlPf+Ut5CyavdzMPqHiHeJvG0RqghQUBf
TlmmTjhzuZBE/PY6/1BdFHYuiDDSL49X20yVSmXTOIAxHCx04nr91qoZrMvuJke5
1Xa9GwvMuSDS0OGiODeAVJc1lKKVnRQWrGWGiXVqrebAVWB7xFb5E4ZSaA5QppgT
0Au07b6lANJRISIFrXkd9xVPw/jn0wIDAQABAoICAFk4c0veXIxhSnx8zr99+eVr
QT3xbRAjeHNdKYI/QYIq6Sl1x2igdfPkYTYLCWuGdpiz8PtA/VYG46QcadScKLnZ
oSW9cvBmguf1qHLnGI7PjuubAyCPZjVwvQ8II1G6+JX6Kl9wNJ6V7Ls6LOH7947C
VOhLHeeH3ybZkw5t4nXbkiZ6zM5llhaFfQllvxtqChXNFH99H5iIRQdoDwDsZtVl
K+MaxNGAZ/LfqsH7pc8CqoiewziUeXhB/hXcjYUyAgMq49uQ4SoGfXyYBCuvWEl+
D5xdeDrlhc3x0tdKs9kdnlp5m/K94+JM8GKpxV/zc2f/TlUXyLnUSEHXJLRAN/v5
oMeZ/3N+gbOUZtu8E/xsYLCSgjVdnWlqBxhnNJ9KsrlhHNzM0FQMOMSHf2aQVUjS
yhSPwhwOmNJ3sOznHF27yZS52MS+lgIE+Te7swRAUt/Rb3Vx2SUwbfBHWLeSY0Wy
DOYljRSc7jliNxgN9FGdReHQpLRbysotBV9XkyYks5nrkbqFJP5gfRm0Y8nk2Nlr
NJFi3fTDVjKF5PXaSskymwL7RQdYdBD//wsRdcqZxbs93we7xjM5POZqEcX7WUvr
LqivREko+ZaUR0BSLZVYRMIDFwFUFJuTy3uEdWvhaB0KYdL/nu85iLHqLg86Jteg
aMkVEgFlyfMZI17DEhjBAoIBAQDnzGWl1JCMnuNNOeQw4mcRuuKun6cCPaoU0Nl/
SLOFd6P6XLUy4kTIvDopo9mg9Qi5EpWUDaLEWuqFIv45KN90n0/7uGEerkGob6ic
DjHJiVoqsRV2/keQsGk/vIoKWXemdDIFIVy6AEQ7GEV/EWPIDSS3Xr8EymPtpuYP
kqp6o0iMFpvkaAPNj33Lz2RigNKYPTJ/tjIPE4yw2B1zuanMwTBCHahJeMZqF3qL
nqdDfRqdEB8/LLwRibRY1lvKzPQPxoUdv2MGKXZ/T3oPQEblMpOAU8EhifUZPpef
vZYeJ/XURLcBNsdYdJQzeuzxr+rxl3gEdpErZafBh9DXgtXzAoIBAQDxi+AnpIlr
jmIec4aFDoS+PjzIhe4sZEuLnTlYe8XarbhN8kedYaRhvZQ5L7QVpmuk8jyMORB8
VKfabmQKQoYKtKb9nHS8C/WW4dJRhWu7vcr22BHEh+ylwsJmBPLywpybjo0YX9k6
epbMzgIIP+woFCzo5IeQ9fd4XzQTFF8nJmNv+vOzj3PMf7Cc5/q7DqiDKDnXGl5b
u2mdZCM5GFY6wjpEkJllSE82JjEY18N0wsJMfcNckY9oq4ZkWdPfhT4ZcnknZjqC
uJABe28r+CE3lAtRSgD5XLFCvPuP0FbGe1MovuOFFPbkVKA6ECGF0az/A8F4t8PB
sSuzoNu8Ar6hAoIBAQC04KahFJIHaSTt6jLKgqDzEOY6ZZKpCP1jaOWPkWekyotG
nnk2z6HlEhxAyf7UvuCjqoDWGx3cIyXF5lyCtgZItthvEJ2Yl1nc2eS0gc8P+QJH
NhAN3rZxjXdTqQf+s3nOhfVSU4pMClEz2+i/Ew7N2JPCE0jzsAryM75qgIRPVoMR
7cKQJSpyiXocRCWNSAENkxOI3N+LLDIo/TteRo7dnBLQRNxBGOGbf968fH0BCOpv
jVkUrw/Cj7YPbJYMVopMlRji8amP8WLqTVZt+DZaO3EmPjUCuuhrXpBqskImHgCS
N1ymsdw0hiPvWAj1P9UR2KRqtyrotlaFijnJMetJAoIBADDQD5BzU8IEmBeHSRwC
fxjjAu2TAzq9Wfbw4vHasXUrvh8iYw6O+OU3poiX91CYvRAsU8gSkB5QDUu7G0Rn
hScMsuJ1h7GoyQygvhvzVn4uMKIJsC2DOnOVFCwBvAcLBRL6j9DpLcD/nRHuX8LD
CDphOWInLK5CxqvwsVlZuJD01QuAL1eOGdytwUc0Khs7LxqyOl4Z2g+3o/RGlEep
f2OIdLX+csFhB4Dt3uYiVEF4SkOi9qPyVoTUhOgqrwJwrsf9tjYcFp7sJU3nX+QG
1M+if1cCGYhLDxdpkXzSoXai3X9SdDAkuHAUGf0h3WRppwgx/hsjJ9AwuaAnVcB8
3YECggEBALBNp7jHCdmRJeZk1pLrG8v5cMFvZfHV8u80Pk8FXe1ULSQzDx7Pse/G
s9K1Q5j3KbWW+WfD2klq1TlJuYyLCF1gEl0dYIbHSSGauzZDRZ+NzlgYBt2MFKcz
qCuqbI7wU5Ou60jJoVG4E2F6xwLyQuHRP5sZn+dN2jsxqouBCRltkpd2mlL2+AU0
StbDpQ5k70/6OhJsZjNDUiUiLUaM73wiIPoOQEslxVaWyuud2U13kbGeB9SKyipR
Te53TuEakRGEmrgkqQYIX/w90LAkobKdATkrYk/IIr6y7wvvY80nacZgYyZ14FSC
eWRtwt2K2iouhIrKnXvlgEnfRUd9XXI=
-----END PRIVATE KEY-----

14
certlib/testdata/rsa-ca.yaml vendored Normal file
View File

@@ -0,0 +1,14 @@
key:
algorithm: rsa
size: 4096
subject:
common_name: WNTRMUTE TEST RSA CA 1
country: US
organization: WNTRMUTE HEAVY INDUSTRIES
organizational_unit: CRYPTOGRAPHIC SERVICES
profile:
is_ca: true
path_len: 3
key_uses:
- cert sign
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

@@ -12,6 +12,7 @@ chains:
include_single: true include_single: true
include_individual: true include_individual: true
manifest: true manifest: true
encoding: pemcrt
formats: formats:
- zip - zip
- tgz - tgz

View File

@@ -39,7 +39,7 @@ func main() {
revoke.HardFail = hardfail revoke.HardFail = hardfail
// Build a proxy-aware HTTP client for OCSP/CRL fetches // Build a proxy-aware HTTP client for OCSP/CRL fetches
if httpClient, err := dialer.NewHTTPClient(dialer.DialerOpts{Timeout: timeout}); err == nil { if httpClient, err := dialer.NewHTTPClient(dialer.Opts{Timeout: timeout}); err == nil {
revoke.HTTPClient = httpClient revoke.HTTPClient = httpClient
} }
@@ -105,7 +105,7 @@ func checkSite(hostport string) (string, error) {
defer cancel() defer cancel()
// Use proxy-aware TLS dialer // Use proxy-aware TLS dialer
conn, err := dialer.DialTLS(ctx, target.String(), dialer.DialerOpts{Timeout: timeout, TLSConfig: &tls.Config{ conn, err := dialer.DialTLS(ctx, target.String(), dialer.Opts{Timeout: timeout, TLSConfig: &tls.Config{
InsecureSkipVerify: true, // #nosec G402 -- CLI tool only verifies revocation InsecureSkipVerify: true, // #nosec G402 -- CLI tool only verifies revocation
ServerName: target.Host, ServerName: target.Host,
}}) }})

View File

@@ -25,7 +25,11 @@ func main() {
} }
// Use proxy-aware TLS dialer // Use proxy-aware TLS dialer
conn, err := dialer.DialTLS(context.Background(), server, dialer.DialerOpts{TLSConfig: &tls.Config{}}) // #nosec G402 conn, err := dialer.DialTLS(
context.Background(),
server,
dialer.Opts{TLSConfig: &tls.Config{}},
) // #nosec G402
die.If(err) die.If(err)
defer conn.Close() defer conn.Close()

View File

@@ -35,12 +35,12 @@ func main() {
} }
if config.leafOnly { if config.leafOnly {
dump.DisplayCert(os.Stdout, certs[0]) dump.DisplayCert(os.Stdout, certs[0], config.showHash)
continue continue
} }
for i := range certs { for i := range certs {
dump.DisplayCert(os.Stdout, certs[i]) dump.DisplayCert(os.Stdout, certs[i], config.showHash)
} }
} }
} }

View File

@@ -101,7 +101,7 @@ func buildExtraForPath(st unix.Stat_t, path string, setUID, setGID int) []byte {
gid = uint32(setGID & 0xFFFFFFFF) //#nosec G115 - masked gid = uint32(setGID & 0xFFFFFFFF) //#nosec G115 - masked
} }
} }
mode := uint32(st.Mode & 0o7777) mode := st.Mode & 0o7777
// Use portable helper to gather ctime // Use portable helper to gather ctime
var cts int64 var cts int64
@@ -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.

View File

@@ -85,7 +85,7 @@ func main() {
continue continue
} }
// Use proxy-aware HTTP client with a reasonable timeout for connects/handshakes // Use proxy-aware HTTP client with a reasonable timeout for connects/handshakes
httpClient, err := dialer.NewHTTPClient(dialer.DialerOpts{Timeout: 30 * time.Second}) httpClient, err := dialer.NewHTTPClient(dialer.Opts{Timeout: 30 * time.Second})
if err != nil { if err != nil {
_, _ = lib.Warn(err, "building HTTP client for %s", remote) _, _ = lib.Warn(err, "building HTTP client for %s", remote)
continue continue

View File

@@ -43,7 +43,7 @@ func main() {
} }
var conn *tls.Conn var conn *tls.Conn
conn, err = dialer.DialTLS(context.Background(), site, dialer.DialerOpts{TLSConfig: tlsCfg}) conn, err = dialer.DialTLS(context.Background(), site, dialer.Opts{TLSConfig: tlsCfg})
die.If(err) die.If(err)
cs := conn.ConnectionState() cs := conn.ConnectionState()

View File

@@ -25,7 +25,7 @@ func main() {
conn, err := dialer.DialTLS( conn, err := dialer.DialTLS(
context.Background(), context.Background(),
hostPort.String(), hostPort.String(),
dialer.DialerOpts{TLSConfig: &tls.Config{InsecureSkipVerify: true}}, dialer.Opts{TLSConfig: &tls.Config{InsecureSkipVerify: true}},
) // #nosec G402 ) // #nosec G402
die.If(err) die.If(err)
@@ -65,7 +65,7 @@ func printPeerCertificates(certificates []*x509.Certificate) {
fmt.Printf("\tSubject: %s\n", cert.Subject) fmt.Printf("\tSubject: %s\n", cert.Subject)
fmt.Printf("\tIssuer: %s\n", cert.Issuer) fmt.Printf("\tIssuer: %s\n", cert.Issuer)
fmt.Printf("\tDNS Names: %v\n", cert.DNSNames) fmt.Printf("\tDNS Names: %v\n", cert.DNSNames)
fmt.Printf("\tNot Before: %s\n:", cert.NotBefore) fmt.Printf("\tNot Before: %s\n", cert.NotBefore)
fmt.Printf("\tNot After: %s\n", cert.NotAfter) fmt.Printf("\tNot After: %s\n", cert.NotAfter)
} }
} }

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

@@ -1,5 +1,5 @@
// Package lib contains reusable helpers. This file provides proxy-aware // Package dialer provides proxy-aware dialers for plain TCP and TLS
// dialers for plain TCP and TLS connections using environment variables. // connections using environment variables.
// //
// Supported proxy environment variables (checked case-insensitively): // Supported proxy environment variables (checked case-insensitively):
// - SOCKS5_PROXY (e.g., socks5://user:pass@host:1080) // - SOCKS5_PROXY (e.g., socks5://user:pass@host:1080)
@@ -66,7 +66,7 @@ func BaselineTLSConfig(skipVerify bool, secure bool) (*tls.Config, error) {
var debug = dbg.NewFromEnv() var debug = dbg.NewFromEnv()
// DialerOpts controls creation of proxy-aware dialers. // Opts controls creation of proxy-aware dialers.
// //
// Timeout controls the maximum amount of time spent establishing the // Timeout controls the maximum amount of time spent establishing the
// underlying TCP connection and any proxy handshake. If zero, a // underlying TCP connection and any proxy handshake. If zero, a
@@ -75,7 +75,7 @@ var debug = dbg.NewFromEnv()
// TLSConfig is used by the TLS dialer to configure the TLS handshake to // TLSConfig is used by the TLS dialer to configure the TLS handshake to
// the target endpoint. If TLSConfig.ServerName is empty, it will be set // the target endpoint. If TLSConfig.ServerName is empty, it will be set
// from the host portion of the address passed to DialContext. // from the host portion of the address passed to DialContext.
type DialerOpts struct { type Opts struct {
Timeout time.Duration Timeout time.Duration
TLSConfig *tls.Config TLSConfig *tls.Config
} }
@@ -88,7 +88,7 @@ type ContextDialer interface {
// DialTCP is a convenience helper that dials a TCP connection to address // DialTCP is a convenience helper that dials a TCP connection to address
// using a proxy-aware dialer derived from opts. It honors SOCKS5_PROXY, // using a proxy-aware dialer derived from opts. It honors SOCKS5_PROXY,
// HTTPS_PROXY, and HTTP_PROXY environment variables. // HTTPS_PROXY, and HTTP_PROXY environment variables.
func DialTCP(ctx context.Context, address string, opts DialerOpts) (net.Conn, error) { func DialTCP(ctx context.Context, address string, opts Opts) (net.Conn, error) {
d, err := NewNetDialer(opts) d, err := NewNetDialer(opts)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -100,7 +100,7 @@ func DialTCP(ctx context.Context, address string, opts DialerOpts) (net.Conn, er
// address using a proxy-aware dialer derived from opts. It returns a *tls.Conn. // address using a proxy-aware dialer derived from opts. It returns a *tls.Conn.
// It honors SOCKS5_PROXY, HTTPS_PROXY, and HTTP_PROXY environment variables and // It honors SOCKS5_PROXY, HTTPS_PROXY, and HTTP_PROXY environment variables and
// uses opts.TLSConfig for the handshake (filling ServerName from address if empty). // uses opts.TLSConfig for the handshake (filling ServerName from address if empty).
func DialTLS(ctx context.Context, address string, opts DialerOpts) (*tls.Conn, error) { func DialTLS(ctx context.Context, address string, opts Opts) (*tls.Conn, error) {
d, err := NewTLSDialer(opts) d, err := NewTLSDialer(opts)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -123,7 +123,7 @@ func DialTLS(ctx context.Context, address string, opts DialerOpts) (*tls.Conn, e
// proxies discovered from the environment (SOCKS5_PROXY, HTTPS_PROXY, HTTP_PROXY). // proxies discovered from the environment (SOCKS5_PROXY, HTTPS_PROXY, HTTP_PROXY).
// The returned dialer supports context cancellation for direct and HTTP(S) // The returned dialer supports context cancellation for direct and HTTP(S)
// proxies and applies the configured timeout to connection/proxy handshake. // proxies and applies the configured timeout to connection/proxy handshake.
func NewNetDialer(opts DialerOpts) (ContextDialer, error) { func NewNetDialer(opts Opts) (ContextDialer, error) {
if opts.Timeout <= 0 { if opts.Timeout <= 0 {
opts.Timeout = 30 * time.Second opts.Timeout = 30 * time.Second
} }
@@ -165,7 +165,7 @@ func NewNetDialer(opts DialerOpts) (ContextDialer, error) {
// //
// The returned dialer performs proxy negotiation (if any), then completes a // The returned dialer performs proxy negotiation (if any), then completes a
// TLS handshake to the target using opts.TLSConfig. // TLS handshake to the target using opts.TLSConfig.
func NewTLSDialer(opts DialerOpts) (ContextDialer, error) { func NewTLSDialer(opts Opts) (ContextDialer, error) {
if opts.Timeout <= 0 { if opts.Timeout <= 0 {
opts.Timeout = 30 * time.Second opts.Timeout = 30 * time.Second
} }
@@ -247,7 +247,7 @@ func getProxyURLFromEnv(name string) *url.URL {
// HTTPS_PROXY, and NO_PROXY/no_proxy. // HTTPS_PROXY, and NO_PROXY/no_proxy.
// - Connection and TLS handshake timeouts are derived from opts.Timeout. // - Connection and TLS handshake timeouts are derived from opts.Timeout.
// - For HTTPS targets, opts.TLSConfig is applied to the transport. // - For HTTPS targets, opts.TLSConfig is applied to the transport.
func NewHTTPClient(opts DialerOpts) (*http.Client, error) { func NewHTTPClient(opts Opts) (*http.Client, error) {
if opts.Timeout <= 0 { if opts.Timeout <= 0 {
opts.Timeout = 30 * time.Second opts.Timeout = 30 * time.Second
} }
@@ -422,7 +422,7 @@ func drainHeaders(br *bufio.Reader) error {
} }
// newSOCKS5Dialer builds a context-aware wrapper over the x/net/proxy dialer. // newSOCKS5Dialer builds a context-aware wrapper over the x/net/proxy dialer.
func newSOCKS5Dialer(u *url.URL, opts DialerOpts) (ContextDialer, error) { func newSOCKS5Dialer(u *url.URL, opts Opts) (ContextDialer, error) {
var auth *xproxy.Auth var auth *xproxy.Auth
if u.User != nil { if u.User != nil {
user := u.User.Username() user := u.User.Username()
@@ -468,8 +468,8 @@ func (s *socks5ContextDialer) DialContext(ctx context.Context, network, address
// tlsWrappingDialer performs a TLS handshake over an existing base dialer. // tlsWrappingDialer performs a TLS handshake over an existing base dialer.
type tlsWrappingDialer struct { type tlsWrappingDialer struct {
base ContextDialer base ContextDialer
tcfg *tls.Config tcfg *tls.Config
timeout time.Duration timeout time.Duration
} }

View File

@@ -22,43 +22,50 @@ import (
// Fetcher is an interface for fetching certificates from a remote source. It // Fetcher is an interface for fetching certificates from a remote source. It
// currently supports fetching from a server or a file. // currently supports fetching from a server or a file.
type Fetcher interface { type Fetcher interface {
// Get retrieves the leaf certificate from the source.
Get() (*x509.Certificate, error) Get() (*x509.Certificate, error)
// GetChain retrieves the entire chain from the Fetcher.
GetChain() ([]*x509.Certificate, error) GetChain() ([]*x509.Certificate, error)
// String returns a string representation of the Fetcher.
String() string String() string
} }
func NewFetcher(spec string, tcfg *tls.Config) (Fetcher, error) {
if fileutil.FileDoesExist(spec) || spec == "-" {
return NewFileFetcher(spec), nil
}
fetcher, err := ParseServer(spec, tcfg)
if err != nil {
return nil, err
}
fetcher.config = tcfg
return fetcher, nil
}
// ServerFetcher retrieves certificates from a TLS connection.
type ServerFetcher struct { type ServerFetcher struct {
host string host string
port int port int
insecure bool config *tls.Config
roots *x509.CertPool
}
// WithRoots sets the roots for the ServerFetcher.
func WithRoots(roots *x509.CertPool) func(*ServerFetcher) {
return func(sf *ServerFetcher) {
sf.roots = roots
}
}
// WithSkipVerify sets the insecure flag for the ServerFetcher.
func WithSkipVerify() func(*ServerFetcher) {
return func(sf *ServerFetcher) {
sf.insecure = true
}
} }
// ParseServer parses a server string into a ServerFetcher. It can be a URL or a // ParseServer parses a server string into a ServerFetcher. It can be a URL or a
// a host:port pair. // a host:port pair.
func ParseServer(host string) (*ServerFetcher, error) { func ParseServer(host string, cfg *tls.Config) (*ServerFetcher, error) {
target, err := hosts.ParseHost(host) target, err := hosts.ParseHost(host)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse server: %w", err) return nil, fmt.Errorf("failed to parse server: %w", err)
} }
return &ServerFetcher{ return &ServerFetcher{
host: target.Host, host: target.Host,
port: target.Port, port: target.Port,
config: cfg,
}, nil }, nil
} }
@@ -67,11 +74,8 @@ func (sf *ServerFetcher) String() string {
} }
func (sf *ServerFetcher) GetChain() ([]*x509.Certificate, error) { func (sf *ServerFetcher) GetChain() ([]*x509.Certificate, error) {
opts := dialer.DialerOpts{ opts := dialer.Opts{
TLSConfig: &tls.Config{ TLSConfig: sf.config,
InsecureSkipVerify: sf.insecure, // #nosec G402 - no shit sherlock
RootCAs: sf.roots,
},
} }
conn, err := dialer.DialTLS(context.Background(), net.JoinHostPort(sf.host, lib.Itoa(sf.port, -1)), opts) conn, err := dialer.DialTLS(context.Background(), net.JoinHostPort(sf.host, lib.Itoa(sf.port, -1)), opts)
@@ -93,6 +97,7 @@ func (sf *ServerFetcher) Get() (*x509.Certificate, error) {
return certs[0], nil return certs[0], nil
} }
// FileFetcher retrieves certificates from files on disk.
type FileFetcher struct { type FileFetcher struct {
path string path string
} }
@@ -139,20 +144,11 @@ func (ff *FileFetcher) Get() (*x509.Certificate, error) {
// configuration will be used to control verification behavior (e.g., // configuration will be used to control verification behavior (e.g.,
// InsecureSkipVerify, RootCAs). // InsecureSkipVerify, RootCAs).
func GetCertificateChain(spec string, cfg *tls.Config) ([]*x509.Certificate, error) { func GetCertificateChain(spec string, cfg *tls.Config) ([]*x509.Certificate, error) {
if fileutil.FileDoesExist(spec) { fetcher, err := NewFetcher(spec, cfg)
return NewFileFetcher(spec).GetChain()
}
fetcher, err := ParseServer(spec)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if cfg != nil {
fetcher.insecure = cfg.InsecureSkipVerify
fetcher.roots = cfg.RootCAs
}
return fetcher.GetChain() return fetcher.GetChain()
} }

View File

@@ -3,7 +3,9 @@ package lib
import ( import (
"encoding/base64" "encoding/base64"
"encoding/hex" "encoding/hex"
"errors"
"fmt" "fmt"
"io"
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
@@ -118,6 +120,8 @@ func IsDigit(b byte) bool {
return b >= '0' && b <= '9' return b >= '0' && b <= '9'
} }
const signedaMask64 = 1<<63 - 1
// ParseDuration parses a duration string into a time.Duration. // ParseDuration parses a duration string into a time.Duration.
// It supports standard units (ns, us/µs, ms, s, m, h) plus extended units: // It supports standard units (ns, us/µs, ms, s, m, h) plus extended units:
// d (days, 24h), w (weeks, 7d), y (years, 365d). // d (days, 24h), w (weeks, 7d), y (years, 365d).
@@ -127,7 +131,7 @@ func IsDigit(b byte) bool {
func ParseDuration(s string) (time.Duration, error) { func ParseDuration(s string) (time.Duration, error) {
s = strings.ToLower(s) // Normalize to lowercase for case-insensitivity. s = strings.ToLower(s) // Normalize to lowercase for case-insensitivity.
if s == "" { if s == "" {
return 0, fmt.Errorf("empty duration string") return 0, errors.New("empty duration string")
} }
var total time.Duration var total time.Duration
@@ -165,23 +169,24 @@ func ParseDuration(s string) (time.Duration, error) {
var d time.Duration var d time.Duration
switch unit { switch unit {
case "ns": case "ns":
d = time.Nanosecond * time.Duration(num) d = time.Nanosecond * time.Duration(num&signedaMask64) // #nosec G115 - masked off
case "us", "µs": case "us", "µs":
d = time.Microsecond * time.Duration(num) d = time.Microsecond * time.Duration(num&signedaMask64) // #nosec G115 - masked off
case "ms": case "ms":
d = time.Millisecond * time.Duration(num) d = time.Millisecond * time.Duration(num&signedaMask64) // #nosec G115 - masked off
case "s": case "s":
d = time.Second * time.Duration(num) d = time.Second * time.Duration(num&signedaMask64) // #nosec G115 - masked off
case "m": case "m":
d = time.Minute * time.Duration(num) d = time.Minute * time.Duration(num&signedaMask64) // #nosec G115 - masked off
case "h": case "h":
d = time.Hour * time.Duration(num) d = time.Hour * time.Duration(num&signedaMask64) // #nosec G115 - masked off
case "d": case "d":
d = 24 * time.Hour * time.Duration(num) d = 24 * time.Hour * time.Duration(num&signedaMask64) // #nosec G115 - masked off
case "w": case "w":
d = 7 * 24 * time.Hour * time.Duration(num) d = 7 * 24 * time.Hour * time.Duration(num&signedaMask64) // #nosec G115 - masked off
case "y": case "y":
d = 365 * 24 * time.Hour * time.Duration(num) // Approximate, non-leap year. // Approximate, non-leap year.
d = 365 * 24 * time.Hour * time.Duration(num&signedaMask64) // #nosec G115 - masked off;
default: default:
return 0, fmt.Errorf("unknown unit %q at position %d", s[unitStart:i], unitStart) return 0, fmt.Errorf("unknown unit %q at position %d", s[unitStart:i], unitStart)
} }
@@ -325,3 +330,20 @@ func HexEncode(b []byte, mode HexEncodeMode) string {
panic("invalid hex encode mode") panic("invalid hex encode mode")
} }
} }
// DummyWriteCloser wraps an io.Writer in a struct with a no-op Close.
type DummyWriteCloser struct {
w io.Writer
}
func WithCloser(w io.Writer) io.WriteCloser {
return &DummyWriteCloser{w: w}
}
func (dwc *DummyWriteCloser) Write(p []byte) (int, error) {
return dwc.w.Write(p)
}
func (dwc *DummyWriteCloser) Close() error {
return nil
}

139
msg/msg.go Normal file
View File

@@ -0,0 +1,139 @@
// Package msg is a tool for handling commandline output based on
// flags for quiet, verbose, and debug modes. The default is to
// have all modes disabled.
//
// The Qprint messages will only output messages if quiet mode is
// disabled
// The Vprint messages will only output messages if verbose mode
// is enabled.
// The Dprint messages will only output messages if debug mode
// is enabled.
package msg
import (
"fmt"
"io"
"os"
"git.wntrmute.dev/kyle/goutils/lib"
"git.wntrmute.dev/kyle/goutils/dbg"
)
var (
enableQuiet bool
enableVerbose bool
debug = dbg.New()
w io.Writer = os.Stdout
)
func Reset() {
enableQuiet = false
enableVerbose = false
debug = dbg.New()
w = os.Stdout
}
func SetQuiet(q bool) {
enableQuiet = q
}
func SetVerbose(v bool) {
enableVerbose = v
}
func SetDebug(d bool) {
debug.Enabled = d
}
func Set(q, v, d bool) {
SetQuiet(q)
SetVerbose(v)
SetDebug(d)
}
func Qprint(a ...any) {
if enableQuiet {
return
}
fmt.Fprint(w, a...)
}
func Qprintf(format string, a ...any) {
if enableQuiet {
return
}
fmt.Fprintf(w, format, a...)
}
func Qprintln(a ...any) {
if enableQuiet {
return
}
fmt.Fprintln(w, a...)
}
func Dprint(a ...any) {
debug.Print(a...)
}
func Dprintf(format string, a ...any) {
debug.Printf(format, a...)
}
func Dprintln(a ...any) {
debug.Println(a...)
}
func StackTrace() {
debug.StackTrace()
}
func Vprint(a ...any) {
if !enableVerbose {
return
}
fmt.Fprint(w, a...)
}
func Vprintf(format string, a ...any) {
if !enableVerbose {
return
}
fmt.Fprintf(w, format, a...)
}
func Vprintln(a ...any) {
if !enableVerbose {
return
}
fmt.Fprintln(w, a...)
}
func Print(a ...any) {
fmt.Fprint(w, a...)
}
func Printf(format string, a ...any) {
fmt.Fprintf(w, format, a...)
}
func Println(a ...any) {
fmt.Fprintln(w, a...)
}
// SetWriter changes the output for messages.
func SetWriter(dst io.Writer) {
w = dst
dbgEnabled := debug.Enabled
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")
}
}