Compare commits

...

23 Commits

Author SHA1 Message Date
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
b17fad4334 Update CHANGELOG for v1.15.0. 2025-11-19 01:36:45 -08:00
154d5a6c2e Major refactoring.
+ Many lib functions have been split out into separate packages.
+ Adding cert/key generation tooling.
+ Add new time.Duration parser.
2025-11-19 01:35:37 -08:00
90a48a1890 Add unit tests for keymatch. 2025-11-19 00:32:39 -08:00
245cf78ebb certlib/hosts: update doc string to describe valid targets. 2025-11-18 23:54:50 -08:00
f4851af42f Update CHANGELOG for v1.14.7. 2025-11-18 23:45:45 -08:00
bf29d214c5 lib: add base64 hex encoding; linter fixes. 2025-11-18 23:45:21 -08:00
ff34eb4eff cmd/ca-signed: clean up the codebase 2025-11-18 23:01:58 -08:00
43 changed files with 1316 additions and 380 deletions

View File

@@ -101,7 +101,7 @@ linters:
- loggercheck # checks key value pairs for common logger libraries (kitlog,klog,logr,zap)
- makezero # finds slice declarations with non-zero initial length
- 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
- musttag # enforces field tags in (un)marshaled structs
- nakedret # finds naked returns in functions greater than a specified function length

View File

@@ -1,5 +1,48 @@
CHANGELOG
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
Changed:
- lib: fetcher and dialer moved to separate packages.
- cmd/ca-signed: cleaned up code internally.
- lib: add base64 encoding to HexEncode.
- linter fixes.
Added:
- certlib/certgen: add support for generating and signing certificates.
v1.14.6 - 2025-11-18
Added:

View File

@@ -422,6 +422,24 @@ func encodeCertsToFiles(
name: baseName + ".pem",
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":
if isSingle {
// For single file in DER, concatenate all cert DER bytes

224
certlib/certgen/config.go Normal file
View File

@@ -0,0 +1,224 @@
package certgen
import (
"crypto"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"math/big"
"net"
"strings"
"time"
"git.wntrmute.dev/kyle/goutils/lib"
)
type KeySpec struct {
Algorithm string `yaml:"algorithm"`
Size int `yaml:"size"`
}
func (ks KeySpec) Generate() (crypto.PublicKey, crypto.PrivateKey, error) {
switch strings.ToLower(ks.Algorithm) {
case "rsa":
return GenerateKey(x509.RSA, ks.Size)
case "ecdsa":
return GenerateKey(x509.ECDSA, ks.Size)
case "ed25519":
return GenerateKey(x509.Ed25519, 0)
default:
return nil, nil, fmt.Errorf("unknown key algorithm: %s", ks.Algorithm)
}
}
func (ks KeySpec) SigningAlgorithm() (x509.SignatureAlgorithm, error) {
switch strings.ToLower(ks.Algorithm) {
case "rsa":
return x509.SHA512WithRSAPSS, nil
case "ecdsa":
return x509.ECDSAWithSHA512, nil
case "ed25519":
return x509.PureEd25519, nil
default:
return 0, fmt.Errorf("unknown key algorithm: %s", ks.Algorithm)
}
}
type Subject struct {
CommonName string `yaml:"common_name"`
Country string `yaml:"country"`
Locality string `yaml:"locality"`
Province string `yaml:"province"`
Organization string `yaml:"organization"`
OrganizationalUnit string `yaml:"organizational_unit"`
Email string `yaml:"email"`
DNSNames []string `yaml:"dns"`
IPAddresses []string `yaml:"ips"`
}
type CertificateRequest struct {
KeySpec KeySpec `yaml:"key"`
Subject Subject `yaml:"subject"`
Profile Profile `yaml:"profile"`
}
func (cs CertificateRequest) Request(priv crypto.PrivateKey) (*x509.CertificateRequest, error) {
subject := pkix.Name{}
subject.CommonName = cs.Subject.CommonName
subject.Country = []string{cs.Subject.Country}
subject.Locality = []string{cs.Subject.Locality}
subject.Province = []string{cs.Subject.Province}
subject.Organization = []string{cs.Subject.Organization}
subject.OrganizationalUnit = []string{cs.Subject.OrganizationalUnit}
ipAddresses := make([]net.IP, 0, len(cs.Subject.IPAddresses))
for i, ip := range cs.Subject.IPAddresses {
ipAddresses = append(ipAddresses, net.ParseIP(ip))
if ipAddresses[i] == nil {
return nil, fmt.Errorf("invalid IP address: %s", ip)
}
}
req := &x509.CertificateRequest{
PublicKeyAlgorithm: 0,
PublicKey: getPublic(priv),
Subject: subject,
DNSNames: cs.Subject.DNSNames,
IPAddresses: ipAddresses,
}
reqBytes, err := x509.CreateCertificateRequest(rand.Reader, req, priv)
if err != nil {
return nil, fmt.Errorf("failed to create certificate request: %w", err)
}
req, err = x509.ParseCertificateRequest(reqBytes)
if err != nil {
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
}
type Profile struct {
IsCA bool `yaml:"is_ca"`
PathLen int `yaml:"path_len"`
KeyUse string `yaml:"key_uses"`
ExtKeyUsages []string `yaml:"ext_key_usages"`
Expiry string `yaml:"expiry"`
}
func (p Profile) templateFromRequest(req *x509.CertificateRequest) (*x509.Certificate, error) {
serial, err := SerialNumber()
if err != nil {
return nil, fmt.Errorf("failed to generate serial number: %w", err)
}
expiry, err := lib.ParseDuration(p.Expiry)
if err != nil {
return nil, fmt.Errorf("parsing expiry: %w", err)
}
certTemplate := &x509.Certificate{
SignatureAlgorithm: req.SignatureAlgorithm,
PublicKeyAlgorithm: req.PublicKeyAlgorithm,
PublicKey: req.PublicKey,
SerialNumber: serial,
Subject: req.Subject,
NotBefore: time.Now().Add(-1 * time.Hour),
NotAfter: time.Now().Add(expiry),
BasicConstraintsValid: true,
IsCA: p.IsCA,
MaxPathLen: p.PathLen,
DNSNames: req.DNSNames,
IPAddresses: req.IPAddresses,
}
var ok bool
certTemplate.KeyUsage, ok = keyUsageStrings[p.KeyUse]
if !ok {
return nil, fmt.Errorf("invalid key usage: %s", p.KeyUse)
}
var eku x509.ExtKeyUsage
for _, extKeyUsage := range p.ExtKeyUsages {
eku, ok = extKeyUsageStrings[extKeyUsage]
if !ok {
return nil, fmt.Errorf("invalid extended key usage: %s", extKeyUsage)
}
certTemplate.ExtKeyUsage = append(certTemplate.ExtKeyUsage, eku)
}
return certTemplate, nil
}
func (p Profile) SignRequest(
parent *x509.Certificate,
req *x509.CertificateRequest,
priv crypto.PrivateKey,
) (*x509.Certificate, error) {
tpl, err := p.templateFromRequest(req)
if err != nil {
return nil, fmt.Errorf("failed to create certificate template: %w", err)
}
certBytes, err := x509.CreateCertificate(rand.Reader, tpl, parent, req.PublicKey, priv)
if err != nil {
return nil, fmt.Errorf("failed to create certificate: %w", err)
}
cert, err := x509.ParseCertificate(certBytes)
if err != nil {
return nil, fmt.Errorf("failed to parse certificate: %w", err)
}
return cert, nil
}
func (p Profile) SelfSign(req *x509.CertificateRequest, priv crypto.PrivateKey) (*x509.Certificate, error) {
certTemplate, err := p.templateFromRequest(req)
if err != nil {
return nil, fmt.Errorf("failed to create certificate template: %w", err)
}
return p.SignRequest(certTemplate, req, priv)
}
func SerialNumber() (*big.Int, error) {
serialNumberBytes := make([]byte, 20)
_, err := rand.Read(serialNumberBytes)
if err != nil {
return nil, fmt.Errorf("failed to generate serial number: %w", err)
}
return new(big.Int).SetBytes(serialNumberBytes), nil
}
// GenerateSelfSigned generates a self-signed certificate using the given certificate request.
func GenerateSelfSigned(creq *CertificateRequest) (*x509.Certificate, crypto.PrivateKey, error) {
priv, req, err := creq.Generate()
if err != nil {
return nil, nil, fmt.Errorf("failed to generate certificate request: %w", err)
}
cert, err := creq.Profile.SelfSign(req, priv)
if err != nil {
return nil, nil, fmt.Errorf("failed to self-sign certificate: %w", err)
}
return cert, priv, nil
}

86
certlib/certgen/keygen.go Normal file
View File

@@ -0,0 +1,86 @@
package certgen
import (
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"errors"
"fmt"
)
// var (
// oidEd25519 = asn1.ObjectIdentifier{1, 3, 101, 110}
//)
func GenerateKey(algorithm x509.PublicKeyAlgorithm, bitSize int) (crypto.PublicKey, crypto.PrivateKey, error) {
var key crypto.PrivateKey
var pub crypto.PublicKey
var err error
switch algorithm {
case x509.Ed25519:
pub, key, err = ed25519.GenerateKey(rand.Reader)
case x509.RSA:
key, err = rsa.GenerateKey(rand.Reader, bitSize)
if err == nil {
rsaPriv, ok := key.(*rsa.PrivateKey)
if !ok {
panic("failed to cast RSA private key to *rsa.PrivateKey")
}
pub = rsaPriv.Public()
}
case x509.ECDSA:
var curve elliptic.Curve
switch bitSize {
case 256:
curve = elliptic.P256()
case 384:
curve = elliptic.P384()
case 521:
curve = elliptic.P521()
default:
return nil, nil, fmt.Errorf("unsupported curve size %d", bitSize)
}
key, err = ecdsa.GenerateKey(curve, rand.Reader)
if err == nil {
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:
err = errors.New("unsupported algorithm")
}
if err != nil {
return nil, nil, err
}
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
}
}

32
certlib/certgen/ku.go Normal file
View File

@@ -0,0 +1,32 @@
package certgen
import "crypto/x509"
var keyUsageStrings = map[string]x509.KeyUsage{
"signing": x509.KeyUsageDigitalSignature,
"digital signature": x509.KeyUsageDigitalSignature,
"content commitment": x509.KeyUsageContentCommitment,
"key encipherment": x509.KeyUsageKeyEncipherment,
"key agreement": x509.KeyUsageKeyAgreement,
"data encipherment": x509.KeyUsageDataEncipherment,
"cert sign": x509.KeyUsageCertSign,
"crl sign": x509.KeyUsageCRLSign,
"encipher only": x509.KeyUsageEncipherOnly,
"decipher only": x509.KeyUsageDecipherOnly,
}
var extKeyUsageStrings = map[string]x509.ExtKeyUsage{
"any": x509.ExtKeyUsageAny,
"server auth": x509.ExtKeyUsageServerAuth,
"client auth": x509.ExtKeyUsageClientAuth,
"code signing": x509.ExtKeyUsageCodeSigning,
"email protection": x509.ExtKeyUsageEmailProtection,
"s/mime": x509.ExtKeyUsageEmailProtection,
"ipsec end system": x509.ExtKeyUsageIPSECEndSystem,
"ipsec tunnel": x509.ExtKeyUsageIPSECTunnel,
"ipsec user": x509.ExtKeyUsageIPSECUser,
"timestamping": x509.ExtKeyUsageTimeStamping,
"ocsp signing": x509.ExtKeyUsageOCSPSigning,
"microsoft sgc": x509.ExtKeyUsageMicrosoftServerGatedCrypto,
"netscape sgc": x509.ExtKeyUsageNetscapeServerGatedCrypto,
}

View File

@@ -1,10 +1,19 @@
package certlib
import (
"bytes"
"crypto"
"crypto/dsa"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"os"
"strings"
"git.wntrmute.dev/kyle/goutils/certlib/certerr"
)
@@ -12,6 +21,7 @@ import (
// ReadCertificate reads a DER or PEM-encoded certificate from the
// byte slice.
func ReadCertificate(in []byte) (*x509.Certificate, []byte, error) {
in = bytes.TrimSpace(in)
if len(in) == 0 {
return nil, nil, certerr.ParsingError(certerr.ErrorSourceCertificate, certerr.ErrEmptyCertificate)
}
@@ -23,10 +33,10 @@ func ReadCertificate(in []byte) (*x509.Certificate, []byte, error) {
}
rest := remaining
if p.Type != "CERTIFICATE" {
if p.Type != pemTypeCertificate {
return nil, rest, certerr.ParsingError(
certerr.ErrorSourceCertificate,
certerr.ErrInvalidPEMType(p.Type, "CERTIFICATE"),
certerr.ErrInvalidPEMType(p.Type, pemTypeCertificate),
)
}
@@ -93,3 +103,194 @@ func LoadCertificates(path string) ([]*x509.Certificate, error) {
return ReadCertificates(in)
}
func PoolFromBytes(certBytes []byte) (*x509.CertPool, error) {
pool := x509.NewCertPool()
certs, err := ReadCertificates(certBytes)
if err != nil {
return nil, fmt.Errorf("failed to read certificates: %w", err)
}
for _, cert := range certs {
pool.AddCert(cert)
}
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:
return fmt.Sprintf("ECDSA-%s", ka.curve.Params().Name)
case x509.Ed25519:
return "Ed25519"
case x509.DSA:
return "DSA"
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.PublicKeyAlgorithm)
}
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
import (
"crypto/elliptic"
"crypto/x509"
"fmt"
"strings"
"testing"
"git.wntrmute.dev/kyle/goutils/assert"
@@ -138,3 +141,33 @@ func TestReadCertificates(t *testing.T) {
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"
)
func TestFileTypeEC(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)
}
}

View File

@@ -249,11 +249,6 @@ func showBasicConstraints(cert *x509.Certificate) {
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) {
tabs := ""
var tabsSb140 strings.Builder
@@ -265,11 +260,12 @@ func wrapPrint(text string, indent int) {
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")
if showHash {
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("Issuer: "+DisplayName(cert.Issuer), 0))
fmt.Fprintf(w, "\tSignature algorithm: %s / %s\n", sigAlgoPK(cert.SignatureAlgorithm),
@@ -285,8 +281,8 @@ func DisplayCert(w io.Writer, cert *x509.Certificate) {
fmt.Fprintf(w, "\t%s\n", wrap("SKI: "+dumpHex(cert.SubjectKeyId), 1))
}
wrapPrint("Valid from: "+cert.NotBefore.Format(dateFormat), 1)
fmt.Fprintf(w, "\t until: %s\n", cert.NotAfter.Format(dateFormat))
wrapPrint("Valid from: "+cert.NotBefore.Format(lib.DateShortFormat), 1)
fmt.Fprintf(w, "\t until: %s\n", cert.NotAfter.Format(lib.DateShortFormat))
fmt.Fprintf(w, "\tKey usages: %s\n", keyUsages(cert.KeyUsage))
if len(cert.ExtKeyUsage) > 0 {

View File

@@ -75,6 +75,12 @@ var DelegationExtension = pkix.Extension{
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
// nanosecond. This allows time.After to be used inclusively.
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
for _, cert := range certs {
if err := pem.Encode(&buffer, &pem.Block{
Type: "CERTIFICATE",
Type: pemTypeCertificate,
Bytes: cert.Raw,
}); err != nil {
return nil

View File

@@ -1,3 +1,13 @@
// Package hosts provides a simple way to parse hostnames and ports.
// Supported formats are:
// - https://example.com:8080
// - https://example.com
// - tls://example.com:8080
// - tls://example.com
// - example.com:8080
// - example.com
//
// Hosts parsed here are expected to be TLS hosts, and the port defaults to 443.
package hosts
import (

49
certlib/keymatch_test.go Normal file
View File

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

23
certlib/testdata/cert1.pem vendored Normal file
View File

@@ -0,0 +1,23 @@
-----BEGIN CERTIFICATE-----
MIID2zCCAsOgAwIBAgIUN0qOIUWB0UCmtutt2RH6PCmcuhEwDQYJKoZIhvcNAQEL
BQAwfTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExIjAgBgNVBAoM
GVdOVFJNVVRFIEhlYXZ5IEluZHVzdHJpZXMxHzAdBgNVBAsMFkNyeXB0b2dyYXBo
aWMgU2VydmljZXMxFDASBgNVBAMMC3Rlc3QtY2VydC0xMB4XDTI1MTExOTA4MjM1
MFoXDTQ1MTExNDA4MjM1MFowfTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlm
b3JuaWExIjAgBgNVBAoMGVdOVFJNVVRFIEhlYXZ5IEluZHVzdHJpZXMxHzAdBgNV
BAsMFkNyeXB0b2dyYXBoaWMgU2VydmljZXMxFDASBgNVBAMMC3Rlc3QtY2VydC0x
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7sbIJyBfBBF2oHnFOfLS
rtcIUpZcz0fJ9JNtjzazwfyykVV9nuIC4JyD+VhxxSnSQN1H6kHqmcNNJlsQkGjK
TcA6wcFxMRcWyaV5MY3U7MTe1WJJXTrpAFYTOoo0pQaoONBaWn48qfdQc9OvtU17
wgBFhNWfdJaDKDAcyz4pHj9ihl80brvThOwrhUAWRw3ooyZ3m+T8Bgrkqp4ZPv3w
A8oaAoA91UKT5yKRcIAJHAkE4ep0UZdcNPKhBu7L5Jqh8I4EtG0FnZKkOR7gpw+y
YhIhuewWlQWRJwXBv3TwX9njmKwfE6Uftgy9HPbc66mK61FR3fEsU9KHaCmkXDwH
SQIDAQABo1MwUTAdBgNVHQ4EFgQUD2idNc+Yq+6am5/+lizTVJ5HRBUwHwYDVR0j
BBgwFoAUD2idNc+Yq+6am5/+lizTVJ5HRBUwDwYDVR0TAQH/BAUwAwEB/zANBgkq
hkiG9w0BAQsFAAOCAQEAcsa8Htaxw4HhtS8mboC41+FiqFisXfASO0LbsCLGjmrg
Vi9MP9cg06g1AjxxlYw9KsbSXdn/jdbVqcQJxGItZ+CE1AcwUVg3c4ZmPOGIl4LS
Pv2p2Lv4nCRWXrbp96O+lmC1xclziUTYGdQO9pNi71LcSapjLNlxWCWyvAJhWrVe
zZHjGi1nG6ygpPXpldXFyyw61xpjPKc1eghoI125Am5xr3YhPjLM9IGGA1i6R9rC
TlKjQOy8nUPC00jZrAf+HWdMWSpa320eOPi+qz18qbyfl8KMOBFvmA3mdumoABGn
Mre0Gq9fUcd/KdPEHu++XAcLH3M8pqmeUQHHHse0gQ==
-----END CERTIFICATE-----

34
certlib/testdata/cert2.pem vendored Normal file
View File

@@ -0,0 +1,34 @@
-----BEGIN CERTIFICATE-----
MIIF2zCCA8OgAwIBAgIUXosHyc+4br2XvK+fLJ+6uG8G/eYwDQYJKoZIhvcNAQEL
BQAwfTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExIjAgBgNVBAoM
GVdOVFJNVVRFIEhlYXZ5IEluZHVzdHJpZXMxHzAdBgNVBAsMFkNyeXB0b2dyYXBo
aWMgU2VydmljZXMxFDASBgNVBAMMC3Rlc3QtY2VydC0yMB4XDTI1MTExOTA4MjQy
MloXDTQ1MTExNDA4MjQyMlowfTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlm
b3JuaWExIjAgBgNVBAoMGVdOVFJNVVRFIEhlYXZ5IEluZHVzdHJpZXMxHzAdBgNV
BAsMFkNyeXB0b2dyYXBoaWMgU2VydmljZXMxFDASBgNVBAMMC3Rlc3QtY2VydC0y
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA8G39r3JD0RNTT2d1Omtf
WSxv2XzSgSmiAZl6wpcmvE/C9smltXskK/74vxTRpTSoTVtMi1dNWbZlYag+BaqF
60Cp0kGPESIyLDtUQCZpQypKYjOXVPiwd9xXGAdE7Br7dFaUArGRJiWzPX/vjgdK
mruRk+c3ABFhdbiq3CWCPz3uheu9ekUTgK8CEAFsWg2ehTjWIEJU61M6AITvSIUZ
GUEaNC3cAeP7Wx3Vy694fT9WoHpyr6dtWsTzbWyuSPtQ8uR2BEcunUxiBtQthio5
xv20ZgD9C+dJnwr9tE7JKh1MCrFQNkt7EedKABTVYxYxMVATYUUg+jZPy68v1KnL
kYIeB/TBB6iVGIOc9EKWjGv+luebR7OGgu3sZTFxsW5Dq0LSjzLJqoKROtYEEnJt
sWo6V1j7WMs1MPl8NtqqmJjlSJx/OUaVuseB/uji107aIMEKgOwTmFDfPdVYDhQG
eQ3V0Ro25/A/oe5yxEDNnSWGPtOHRq7aSJHM3/0qaPxg+RPrObb3ISRkXs5GBOHV
ss+Nk4McbCV6Zccy6gi+wrz7fiXHijpSWcVVfN9A61TSTTjAX+S9CphcjkS4I2JW
OJY5i9ANP62mr73d2NSikoTXavUCgOBlW00m0gAR0JJYNe/31yS96UwvH0xCHxer
1tX3qwGEjE0fhnwMhxP4tmkCAwEAAaNTMFEwHQYDVR0OBBYEFCs+ZbZ0uorYlg26
m5PSz4Ov66KEMB8GA1UdIwQYMBaAFCs+ZbZ0uorYlg26m5PSz4Ov66KEMA8GA1Ud
EwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBAJneix0lCM4CqdrajmaHa8Y4
Sdr3URSufzW0l8zoBWss1z88X9aZemWKCd4UDSVpN+T6hEASvAw6zRSd2WkmCsdq
KnwHFnDDWGANt/CBcbr69Sk09YLMO+F0Cku8Ymp2jcAFy074E/wjwxgT6JJ/BtQE
q1JJNusanYN2jrYamB1PUnc4lWyOIOOTIU6oqofcJobtTJbSAA7Gvx4p85TMBnQu
YJdBQ3jnFFH3pjCXA9BXaZnaiJjnfDggsJJT7CXngC4US/ti4qZr7+Poc0Ikb/Pm
8EChKKvljZEtcxrhLhsVEzsJtk72F9Ravl+q2jS1zDqnS3OY6kf45nuYnvZ4QkX4
Nk8Y6PmGGk00QCAxyVsiFrm7wZHHvnQyQr8nxjPOv2MryV5e3rW9WAzAG4vHPS1F
5wi3ELiuivkoO5daDwzfVsKhQ3Nl2uAfS8pvY/NvTVPJnR+wdduJqgLMzAWhbRnx
r6WxuiY9mdkdkr6PDDnrw/4lm+GRFw8ksn8ErB3nZf73lo1Ai+Iv2FIi5Ore/qeq
vZjVNvBpZBiMo5d2zDWtp3m8vWgmgXDaKZXn0YAJATkqnhAKbdZ2cmwbGTZhIdrZ
pqoq2KPY+luirIIDiKDbkW4b2HRxwSM8cI2HxONGcB43FcZHlpMhOtM3DD0Z8lQD
b2Hi9ZK8kpL8qa2vFpOe
-----END CERTIFICATE-----

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

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

@@ -0,0 +1,13 @@
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/priv1.pem vendored Normal file
View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDuxsgnIF8EEXag
ecU58tKu1whSllzPR8n0k22PNrPB/LKRVX2e4gLgnIP5WHHFKdJA3UfqQeqZw00m
WxCQaMpNwDrBwXExFxbJpXkxjdTsxN7VYkldOukAVhM6ijSlBqg40Fpafjyp91Bz
06+1TXvCAEWE1Z90loMoMBzLPikeP2KGXzRuu9OE7CuFQBZHDeijJneb5PwGCuSq
nhk+/fADyhoCgD3VQpPnIpFwgAkcCQTh6nRRl1w08qEG7svkmqHwjgS0bQWdkqQ5
HuCnD7JiEiG57BaVBZEnBcG/dPBf2eOYrB8TpR+2DL0c9tzrqYrrUVHd8SxT0odo
KaRcPAdJAgMBAAECggEALeHOK7CNeYFmj2MeyioWIGkrDP2eM2lqzf+3VYXwKEZH
xOQN2cY5wdHpjTQY1odZAsRSkZnde/L6o/RrPCiauTKHR9yFRObYJuLQZTyJDf8t
h4jVqp/Ljpg7pSvR/mUHVbV5qzpnK0zd7Yffk2Hidk6pjSMkexmB9eq62bYl3gz2
dlgKrLgjlwUmhD0P5OhwCW2Z2rmrGwY1y3pj/FjvIckxpPcEle0o/xUIEbW7lZux
3fCAu2Lvg+I9qE5MaWIfZX4aUQi5gJmUZpUCuDJjwFIztO+vSqKmw4zOUFKCRrAc
VsicvHvwmhUCrVT/ebEkf0ntSQq1ED0FARJdYhfOlQKBgQD8ngiviLbVPxVur6Wo
tMzNUUpaJxfyWfZ4w5eYLWKkYSlax1HMCLYyMU0dwSWdmmri+ibm91+VXEJ5DxQh
O/nIF5f0DpWcFmnl4C16xlouWiY6kaSTALQfy/PnsEsEd7oljxesqrpdw7s7/S8q
OUGkTP20M+U0WQQ/RNDWZoyMbQKBgQDx+U1I28ceHSrE6ss/ufWBt2WqiyqvC2NN
444/WkBps5XWUN0HSOBrr8PlMY4jsxyPXuqDVn6P4yg26zIRrIvBLonZ1v1PAMbk
nL1kVB78QOxS/xYOOO2Y2YFtPSztmFZnm8b7l/+9YzHAVp4IrpTsny6UyVZaYVSD
3v7XowlkzQKBgGJrO50P2ZOZQUNfYV4qGoR/gEVBZ93+2LzSDzS1sfGy/QamEyM3
3awOcyn9fyc46x3FMfTYOcAaMrexfTk5gaZIMuZd7EHkpZtuzKlBsA7RBoXZClJP
et3MexkwIPn7n2VUq3eVCIjRYhgMGx0LM5zMdieH9GuBptrzd52gVG+9AoGAVhRL
7AlTMmFJ37dvCoKK1dR6NEtBqfexIfo7lkny9CdQvGcT2g2Q2H40gAo6+HQ1SsOH
RaW1bFZw7eiJbUQmi1iU7YvPnRU3rAgeT9ylETO/Xl8kZ3bU/zURF91VaEhzJHSE
Ouh9r8/j2Pp3SbthezO9jGx7bbeGK0te+TMkmlkCgYAwYst1HRndKGMVdNPCEdlW
aye+R3VtpTWGqDCJiMQCMUsCZF8KcYkCAQk7nXh55putfvTwnWfnqRn91e9yp+/7
rsE3vnGRcbkjvcgZaFyZL7800pOYWEm8FF2xRSBBC49b8kjZPA1i5OME2P0Y4lon
naIddZmTj87qOtEaY/MSGQ==
-----END PRIVATE KEY-----

52
certlib/testdata/priv2.pem vendored Normal file
View File

@@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
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
UAkHH7gJHv8bZgg5FVXt0B9875Z9qAI=
-----END PRIVATE KEY-----

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

@@ -0,0 +1,8 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIBCjCBvQIBADCBiTELMAkGA1UEBhMCVVMxCTAHBgNVBAgTADEJMAcGA1UEBxMA
MSIwIAYDVQQKExlXTlRSTVVURSBIRUFWWSBJTkRVU1RSSUVTMR8wHQYDVQQLExZD
UllQVE9HUkFQSElDIFNFUlZJQ0VTMR8wHQYDVQQDExZXTlRSTVVURSBURVNUIFJT
QSBDQSAxMCowBQYDK2VwAyEA1Lai2WChuUH2kq4LWddp6TlcmpuuBz6G43e9efsZ
GBqgADAFBgMrZXADQQDbBl1gW07c0g9UQmK2g8QkVIXzr2TLrOjXVAptlcW/3rPO
M3iQM2mGwZWMwv7t6C4C7xBaLcUkcqT3b4S+MaUK
-----END CERTIFICATE REQUEST-----

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

@@ -0,0 +1,14 @@
-----BEGIN CERTIFICATE-----
MIICHDCCAc6gAwIBAgIVAN1AKHhLNsqcBEKYCqgjEMG65hhvMAUGAytlcDCBiTEL
MAkGA1UEBhMCVVMxCTAHBgNVBAgTADEJMAcGA1UEBxMAMSIwIAYDVQQKExlXTlRS
TVVURSBIRUFWWSBJTkRVU1RSSUVTMR8wHQYDVQQLExZDUllQVE9HUkFQSElDIFNF
UlZJQ0VTMR8wHQYDVQQDExZXTlRSTVVURSBURVNUIFJTQSBDQSAxMB4XDTI1MTEx
OTIxMDQyNVoXDTQ1MTExNDIyMDQyNVowgYkxCzAJBgNVBAYTAlVTMQkwBwYDVQQI
EwAxCTAHBgNVBAcTADEiMCAGA1UEChMZV05UUk1VVEUgSEVBVlkgSU5EVVNUUklF
UzEfMB0GA1UECxMWQ1JZUFRPR1JBUEhJQyBTRVJWSUNFUzEfMB0GA1UEAxMWV05U
Uk1VVEUgVEVTVCBSU0EgQ0EgMTAqMAUGAytlcAMhANS2otlgoblB9pKuC1nXaek5
XJqbrgc+huN3vXn7GRgao0UwQzAOBgNVHQ8BAf8EBAMCAgQwEgYDVR0TAQH/BAgw
BgEB/wIBAzAdBgNVHQ4EFgQUetUgY5rlFq+OCeYe0Eqmp8Ek488wBQYDK2VwA0EA
LIFZo6FQL+8q8h66Bm7favIh2AlqsXA45DpRUN2LpjNm/7NbTPDw52y8cLegUUMc
UhDyk20fGg5g6cLywC0mDA==
-----END CERTIFICATE-----

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

@@ -0,0 +1,3 @@
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIDDkYbIZKArACSevxtX2Rr8MQSeJ4Jz0qJEe/YgHfjzo
-----END PRIVATE KEY-----

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

@@ -0,0 +1,13 @@
key:
algorithm: ed25519
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

View File

@@ -8,7 +8,8 @@ import (
"io"
"git.wntrmute.dev/kyle/goutils/certlib/revoke"
"git.wntrmute.dev/kyle/goutils/lib"
"git.wntrmute.dev/kyle/goutils/lib/dialer"
"git.wntrmute.dev/kyle/goutils/lib/fetch"
)
func bundleIntermediates(w io.Writer, chain []*x509.Certificate, pool *x509.CertPool, verbose bool) *x509.CertPool {
@@ -45,7 +46,7 @@ func prepareVerification(w io.Writer, target string, opts *Opts) (*verifyResult,
if opts == nil {
opts = &Opts{
Config: lib.StrictBaselineTLSConfig(),
Config: dialer.StrictBaselineTLSConfig(),
ForceIntermediates: false,
}
}
@@ -67,7 +68,7 @@ func prepareVerification(w io.Writer, target string, opts *Opts) (*verifyResult,
roots = opts.Config.RootCAs.Clone()
chain, err := lib.GetCertificateChain(target, opts.Config)
chain, err := fetch.GetCertificateChain(target, opts.Config)
if err != nil {
return nil, fmt.Errorf("fetching certificate chain: %w", err)
}

View File

@@ -1,157 +1,24 @@
package main
import (
"bytes"
"crypto/x509"
"embed"
"errors"
"flag"
"fmt"
"os"
"path/filepath"
"time"
"git.wntrmute.dev/kyle/goutils/certlib"
"git.wntrmute.dev/kyle/goutils/certlib/verify"
"git.wntrmute.dev/kyle/goutils/die"
"git.wntrmute.dev/kyle/goutils/lib"
"git.wntrmute.dev/kyle/goutils/lib/dialer"
"git.wntrmute.dev/kyle/goutils/lib/fetch"
)
// loadCertsFromFile attempts to parse certificates from a file that may be in
// PEM or DER/PKCS#7 format. Returns the parsed certificates or an error.
func loadCertsFromFile(path string) ([]*x509.Certificate, error) {
var certs []*x509.Certificate
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
if certs, err = certlib.ParseCertificatesPEM(data); err == nil {
return certs, nil
}
if certs, _, err = certlib.ParseCertificatesDER(data, ""); err == nil {
return certs, nil
}
return nil, err
}
func makePoolFromFile(path string) (*x509.CertPool, error) {
// Try PEM via helper (it builds a pool)
if pool, err := certlib.LoadPEMCertPool(path); err == nil && pool != nil {
return pool, nil
}
// Fallback: read as DER(s), add to a new pool
certs, err := loadCertsFromFile(path)
if err != nil || len(certs) == 0 {
return nil, fmt.Errorf("failed to load CA certificates from %s", path)
}
pool := x509.NewCertPool()
for _, c := range certs {
pool.AddCert(c)
}
return pool, nil
}
//go:embed testdata/*.pem
var embeddedTestdata embed.FS
// loadCertsFromBytes attempts to parse certificates from bytes that may be in
// PEM or DER/PKCS#7 format.
func loadCertsFromBytes(data []byte) ([]*x509.Certificate, error) {
certs, err := certlib.ParseCertificatesPEM(data)
if err == nil {
return certs, nil
}
certs, _, err = certlib.ParseCertificatesDER(data, "")
if err == nil {
return certs, nil
}
return nil, err
}
func makePoolFromBytes(data []byte) (*x509.CertPool, error) {
certs, err := loadCertsFromBytes(data)
if err != nil || len(certs) == 0 {
return nil, errors.New("failed to load CA certificates from embedded bytes")
}
pool := x509.NewCertPool()
for _, c := range certs {
pool.AddCert(c)
}
return pool, nil
}
// isSelfSigned returns true if the given certificate is self-signed.
// It checks that the subject and issuer match and that the certificate's
// signature verifies against its own public key.
func isSelfSigned(cert *x509.Certificate) bool {
if cert == nil {
return false
}
// Quick check: subject and issuer match
if cert.Subject.String() != cert.Issuer.String() {
return false
}
// Cryptographic check: the certificate is signed by itself
if err := cert.CheckSignatureFrom(cert); err != nil {
return false
}
return true
}
func verifyAgainstCA(caPool *x509.CertPool, path string) (bool, string) {
certs, err := loadCertsFromFile(path)
if err != nil || len(certs) == 0 {
return false, ""
}
leaf := certs[0]
ints := x509.NewCertPool()
if len(certs) > 1 {
for _, ic := range certs[1:] {
ints.AddCert(ic)
}
}
opts := x509.VerifyOptions{
Roots: caPool,
Intermediates: ints,
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
}
if _, err = leaf.Verify(opts); err != nil {
return false, ""
}
return true, leaf.NotAfter.Format("2006-01-02")
}
func verifyAgainstCABytes(caPool *x509.CertPool, certData []byte) (bool, string) {
certs, err := loadCertsFromBytes(certData)
if err != nil || len(certs) == 0 {
return false, ""
}
leaf := certs[0]
ints := x509.NewCertPool()
if len(certs) > 1 {
for _, ic := range certs[1:] {
ints.AddCert(ic)
}
}
opts := x509.VerifyOptions{
Roots: caPool,
Intermediates: ints,
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
}
if _, err = leaf.Verify(opts); err != nil {
return false, ""
}
return true, leaf.NotAfter.Format("2006-01-02")
}
type testCase struct {
name string
caFile string
@@ -170,18 +37,25 @@ func (tc testCase) Run() error {
return fmt.Errorf("selftest: failed to read embedded %s: %w", tc.certFile, err)
}
pool, err := makePoolFromBytes(caBytes)
pool, err := certlib.PoolFromBytes(caBytes)
if err != nil || pool == nil {
return fmt.Errorf("selftest: failed to build CA pool for %s: %w", tc.caFile, err)
}
ok, exp := verifyAgainstCABytes(pool, certBytes)
cert, _, err := certlib.ReadCertificate(certBytes)
if err != nil {
return fmt.Errorf("selftest: failed to parse certificate from %s: %w", tc.certFile, err)
}
_, err = verify.CertWith(cert, pool, nil, false)
ok := err == nil
if ok != tc.expectOK {
return fmt.Errorf("%s: unexpected result: got %v, want %v", tc.name, ok, tc.expectOK)
}
if ok {
fmt.Printf("%s: OK (expires %s)\n", tc.name, exp)
fmt.Printf("%s: OK (expires %s)\n", tc.name, cert.NotAfter.Format(lib.DateShortFormat))
}
fmt.Printf("%s: INVALID (as expected)\n", tc.name)
@@ -237,14 +111,16 @@ func selftest() int {
failures++
continue
}
certs, err := loadCertsFromBytes(b)
certs, err := certlib.ReadCertificates(b)
if err != nil || len(certs) == 0 {
fmt.Fprintf(os.Stderr, "selftest: failed to parse cert(s) from %s: %v\n", root, err)
failures++
continue
}
leaf := certs[0]
if isSelfSigned(leaf) {
if len(leaf.AuthorityKeyId) == 0 || bytes.Equal(leaf.AuthorityKeyId, leaf.SubjectKeyId) {
fmt.Printf("%s: SELF-SIGNED (as expected)\n", root)
} else {
fmt.Printf("%s: expected SELF-SIGNED, but was not detected as such\n", root)
@@ -260,66 +136,58 @@ func selftest() int {
return 1
}
// expiryString returns a YYYY-MM-DD date string to display for certificate
// expiry. If an explicit exp string is provided, it is used. Otherwise, if a
// leaf certificate is available, its NotAfter is formatted. As a last resort,
// it falls back to today's date (should not normally happen).
func expiryString(leaf *x509.Certificate, exp string) string {
if exp != "" {
return exp
}
if leaf != nil {
return leaf.NotAfter.Format("2006-01-02")
}
return time.Now().Format("2006-01-02")
}
// processCert verifies a single certificate file against the provided CA pool
// and prints the result in the required format, handling self-signed
// certificates specially.
func processCert(caPool *x509.CertPool, certPath string) {
ok, exp := verifyAgainstCA(caPool, certPath)
name := filepath.Base(certPath)
// Try to load the leaf cert for self-signed detection and expiry fallback
var leaf *x509.Certificate
if certs, err := loadCertsFromFile(certPath); err == nil && len(certs) > 0 {
leaf = certs[0]
}
// Prefer the SELF-SIGNED label if applicable
if isSelfSigned(leaf) {
fmt.Printf("%s: SELF-SIGNED\n", name)
return
}
if ok {
fmt.Printf("%s: OK (expires %s)\n", name, expiryString(leaf, exp))
return
}
fmt.Printf("%s: INVALID\n", name)
}
func main() {
// Special selftest mode: single argument "selftest"
if len(os.Args) == 2 && os.Args[1] == "selftest" {
var skipVerify, useStrict bool
dialer.StrictTLSFlag(&useStrict)
flag.BoolVar(&skipVerify, "k", false, "don't verify certificates")
flag.Parse()
tcfg, err := dialer.BaselineTLSConfig(skipVerify, useStrict)
die.If(err)
args := flag.Args()
if len(args) == 1 && args[0] == "selftest" {
os.Exit(selftest())
}
if len(os.Args) < 3 {
prog := filepath.Base(os.Args[0])
fmt.Fprintf(os.Stderr, "Usage:\n %s ca.pem cert1.pem cert2.pem ...\n %s selftest\n", prog, prog)
os.Exit(2)
}
caPath := os.Args[1]
caPool, err := makePoolFromFile(caPath)
if err != nil || caPool == nil {
fmt.Fprintf(os.Stderr, "failed to load CA certificate(s): %v\n", err)
if len(args) < 2 {
fmt.Println("No certificates to check.")
os.Exit(1)
}
for _, certPath := range os.Args[2:] {
processCert(caPool, certPath)
caFile := args[0]
args = args[1:]
caCert, err := certlib.LoadCertificates(caFile)
die.If(err)
if len(caCert) != 1 {
die.With("only one CA certificate should be presented.")
}
roots := x509.NewCertPool()
roots.AddCert(caCert[0])
for _, arg := range args {
var cert *x509.Certificate
cert, err = fetch.GetCertificate(arg, tcfg)
if err != nil {
lib.Warn(err, "while parsing certificate from %s", arg)
continue
}
if bytes.Equal(cert.AuthorityKeyId, caCert[0].AuthorityKeyId) {
fmt.Printf("%s: SELF-SIGNED\n", arg)
continue
}
if _, err = verify.CertWith(cert, roots, nil, false); err != nil {
fmt.Printf("%s: INVALID\n", arg)
} else {
fmt.Printf("%s: OK (expires %s)\n", arg, cert.NotAfter.Format(lib.DateShortFormat))
}
}
}

View File

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

View File

@@ -15,7 +15,7 @@ import (
hosts "git.wntrmute.dev/kyle/goutils/certlib/hosts"
"git.wntrmute.dev/kyle/goutils/certlib/revoke"
"git.wntrmute.dev/kyle/goutils/fileutil"
"git.wntrmute.dev/kyle/goutils/lib"
"git.wntrmute.dev/kyle/goutils/lib/dialer"
)
var (
@@ -39,7 +39,7 @@ func main() {
revoke.HardFail = hardfail
// Build a proxy-aware HTTP client for OCSP/CRL fetches
if httpClient, err := lib.NewHTTPClient(lib.DialerOpts{Timeout: timeout}); err == nil {
if httpClient, err := dialer.NewHTTPClient(dialer.Opts{Timeout: timeout}); err == nil {
revoke.HTTPClient = httpClient
}
@@ -105,7 +105,7 @@ func checkSite(hostport string) (string, error) {
defer cancel()
// Use proxy-aware TLS dialer
conn, err := lib.DialTLS(ctx, target.String(), lib.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
ServerName: target.Host,
}})

View File

@@ -11,7 +11,7 @@ import (
"strings"
"git.wntrmute.dev/kyle/goutils/die"
"git.wntrmute.dev/kyle/goutils/lib"
"git.wntrmute.dev/kyle/goutils/lib/dialer"
)
var hasPort = regexp.MustCompile(`:\d+$`)
@@ -25,7 +25,11 @@ func main() {
}
// Use proxy-aware TLS dialer
conn, err := lib.DialTLS(context.Background(), server, lib.DialerOpts{TLSConfig: &tls.Config{}}) // #nosec G402
conn, err := dialer.DialTLS(
context.Background(),
server,
dialer.Opts{TLSConfig: &tls.Config{}},
) // #nosec G402
die.If(err)
defer conn.Close()

View File

@@ -9,6 +9,7 @@ import (
"git.wntrmute.dev/kyle/goutils/certlib/dump"
"git.wntrmute.dev/kyle/goutils/lib"
"git.wntrmute.dev/kyle/goutils/lib/fetch"
)
var config struct {
@@ -27,19 +28,19 @@ func main() {
for _, filename := range flag.Args() {
fmt.Fprintf(os.Stdout, "--%s ---%s", filename, "\n")
certs, err := lib.GetCertificateChain(filename, tlsCfg)
certs, err := fetch.GetCertificateChain(filename, tlsCfg)
if err != nil {
lib.Warn(err, "couldn't read certificate")
continue
}
if config.leafOnly {
dump.DisplayCert(os.Stdout, certs[0])
dump.DisplayCert(os.Stdout, certs[0], config.showHash)
continue
}
for i := range certs {
dump.DisplayCert(os.Stdout, certs[i])
dump.DisplayCert(os.Stdout, certs[i], config.showHash)
}
}
}

View File

@@ -8,6 +8,8 @@ import (
"git.wntrmute.dev/kyle/goutils/certlib/verify"
"git.wntrmute.dev/kyle/goutils/die"
"git.wntrmute.dev/kyle/goutils/lib"
"git.wntrmute.dev/kyle/goutils/lib/dialer"
"git.wntrmute.dev/kyle/goutils/lib/fetch"
)
func main() {
@@ -18,20 +20,20 @@ func main() {
warnOnly bool
)
lib.StrictTLSFlag(&strictTLS)
dialer.StrictTLSFlag(&strictTLS)
flag.BoolVar(&skipVerify, "k", false, "skip server verification") // #nosec G402
flag.BoolVar(&warnOnly, "q", false, "only warn about expiring certs")
flag.DurationVar(&leeway, "t", leeway, "warn if certificates are closer than this to expiring")
flag.Parse()
tlsCfg, err := lib.BaselineTLSConfig(skipVerify, strictTLS)
tlsCfg, err := dialer.BaselineTLSConfig(skipVerify, strictTLS)
die.If(err)
for _, file := range flag.Args() {
var certs []*x509.Certificate
certs, err = lib.GetCertificateChain(file, tlsCfg)
certs, err = fetch.GetCertificateChain(file, tlsCfg)
if err != nil {
_, _ = lib.Warn(err, "while parsing certificates")
continue

View File

@@ -8,6 +8,8 @@ import (
"git.wntrmute.dev/kyle/goutils/die"
"git.wntrmute.dev/kyle/goutils/lib"
"git.wntrmute.dev/kyle/goutils/lib/dialer"
"git.wntrmute.dev/kyle/goutils/lib/fetch"
)
const displayInt lib.HexEncodeMode = iota
@@ -33,13 +35,13 @@ func serialString(cert *x509.Certificate, mode lib.HexEncodeMode) string {
func main() {
var skipVerify bool
var strictTLS bool
lib.StrictTLSFlag(&strictTLS)
dialer.StrictTLSFlag(&strictTLS)
displayAs := flag.String("d", "int", "display mode (int, hex, uhex)")
showExpiry := flag.Bool("e", false, "show expiry date")
flag.BoolVar(&skipVerify, "k", false, "skip server verification") // #nosec G402
flag.Parse()
tlsCfg, err := lib.BaselineTLSConfig(skipVerify, strictTLS)
tlsCfg, err := dialer.BaselineTLSConfig(skipVerify, strictTLS)
die.If(err)
displayMode := parseDisplayMode(*displayAs)
@@ -47,7 +49,7 @@ func main() {
for _, arg := range flag.Args() {
var cert *x509.Certificate
cert, err = lib.GetCertificate(arg, tlsCfg)
cert, err = fetch.GetCertificate(arg, tlsCfg)
die.If(err)
fmt.Printf("%s: %s", arg, serialString(cert, displayMode))

View File

@@ -10,6 +10,7 @@ import (
"git.wntrmute.dev/kyle/goutils/certlib/verify"
"git.wntrmute.dev/kyle/goutils/die"
"git.wntrmute.dev/kyle/goutils/lib"
"git.wntrmute.dev/kyle/goutils/lib/dialer"
)
type appConfig struct {
@@ -28,7 +29,7 @@ func parseFlags() appConfig {
flag.BoolVar(&cfg.skipVerify, "k", false, "skip CA verification")
flag.BoolVar(&cfg.revexp, "r", false, "print revocation and expiry information")
flag.BoolVar(&cfg.verbose, "v", false, "verbose")
lib.StrictTLSFlag(&cfg.strictTLS)
dialer.StrictTLSFlag(&cfg.strictTLS)
flag.Parse()
if flag.NArg() == 0 {
@@ -71,7 +72,7 @@ func main() {
die.If(err)
}
opts.Config, err = lib.BaselineTLSConfig(cfg.skipVerify, cfg.strictTLS)
opts.Config, err = dialer.BaselineTLSConfig(cfg.skipVerify, cfg.strictTLS)
die.If(err)
opts.Config.RootCAs = roots

View File

@@ -14,6 +14,7 @@ import (
"git.wntrmute.dev/kyle/goutils/ahash"
"git.wntrmute.dev/kyle/goutils/die"
"git.wntrmute.dev/kyle/goutils/lib"
"git.wntrmute.dev/kyle/goutils/lib/dialer"
)
func usage(w io.Writer) {
@@ -84,7 +85,7 @@ func main() {
continue
}
// Use proxy-aware HTTP client with a reasonable timeout for connects/handshakes
httpClient, err := lib.NewHTTPClient(lib.DialerOpts{Timeout: 30 * time.Second})
httpClient, err := dialer.NewHTTPClient(dialer.Opts{Timeout: 30 * time.Second})
if err != nil {
_, _ = lib.Warn(err, "building HTTP client for %s", remote)
continue

View File

@@ -11,20 +11,20 @@ import (
"git.wntrmute.dev/kyle/goutils/certlib"
"git.wntrmute.dev/kyle/goutils/die"
"git.wntrmute.dev/kyle/goutils/lib"
"git.wntrmute.dev/kyle/goutils/lib/dialer"
)
func main() {
var sysRoot, serverName string
var skipVerify bool
var strictTLS bool
lib.StrictTLSFlag(&strictTLS)
dialer.StrictTLSFlag(&strictTLS)
flag.StringVar(&sysRoot, "ca", "", "provide an alternate CA bundle")
flag.StringVar(&serverName, "sni", "", "provide an SNI name")
flag.BoolVar(&skipVerify, "noverify", false, "don't verify certificates")
flag.Parse()
tlsCfg, err := lib.BaselineTLSConfig(skipVerify, strictTLS)
tlsCfg, err := dialer.BaselineTLSConfig(skipVerify, strictTLS)
die.If(err)
if sysRoot != "" {
@@ -43,7 +43,7 @@ func main() {
}
var conn *tls.Conn
conn, err = lib.DialTLS(context.Background(), site, lib.DialerOpts{TLSConfig: tlsCfg})
conn, err = dialer.DialTLS(context.Background(), site, dialer.Opts{TLSConfig: tlsCfg})
die.If(err)
cs := conn.ConnectionState()

View File

@@ -9,7 +9,7 @@ import (
"git.wntrmute.dev/kyle/goutils/certlib/hosts"
"git.wntrmute.dev/kyle/goutils/die"
"git.wntrmute.dev/kyle/goutils/lib"
"git.wntrmute.dev/kyle/goutils/lib/dialer"
)
func main() {
@@ -22,10 +22,10 @@ func main() {
die.If(err)
// Use proxy-aware TLS dialer; skip verification as before
conn, err := lib.DialTLS(
conn, err := dialer.DialTLS(
context.Background(),
hostPort.String(),
lib.DialerOpts{TLSConfig: &tls.Config{InsecureSkipVerify: true}},
dialer.Opts{TLSConfig: &tls.Config{InsecureSkipVerify: true}},
) // #nosec G402
die.If(err)
@@ -65,7 +65,7 @@ func printPeerCertificates(certificates []*x509.Certificate) {
fmt.Printf("\tSubject: %s\n", cert.Subject)
fmt.Printf("\tIssuer: %s\n", cert.Issuer)
fmt.Printf("\tDNS Names: %v\n", cert.DNSNames)
fmt.Printf("\tNot Before: %s\n:", cert.NotBefore)
fmt.Printf("\tNot Before: %s\n", cert.NotBefore)
fmt.Printf("\tNot After: %s\n", cert.NotAfter)
}
}

View File

@@ -1,5 +1,5 @@
// Package lib contains reusable helpers. This file provides proxy-aware
// dialers for plain TCP and TLS connections using environment variables.
// Package dialer provides proxy-aware dialers for plain TCP and TLS
// connections using environment variables.
//
// Supported proxy environment variables (checked case-insensitively):
// - SOCKS5_PROXY (e.g., socks5://user:pass@host:1080)
@@ -12,7 +12,7 @@
// 3. HTTP_PROXY
//
// Both uppercase and lowercase variable names are honored.
package lib
package dialer
import (
"bufio"
@@ -66,7 +66,7 @@ func BaselineTLSConfig(skipVerify bool, secure bool) (*tls.Config, error) {
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
// 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
// the target endpoint. If TLSConfig.ServerName is empty, it will be set
// from the host portion of the address passed to DialContext.
type DialerOpts struct {
type Opts struct {
Timeout time.Duration
TLSConfig *tls.Config
}
@@ -88,7 +88,7 @@ type ContextDialer interface {
// DialTCP is a convenience helper that dials a TCP connection to address
// using a proxy-aware dialer derived from opts. It honors SOCKS5_PROXY,
// 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)
if err != nil {
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.
// It honors SOCKS5_PROXY, HTTPS_PROXY, and HTTP_PROXY environment variables and
// 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)
if err != nil {
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).
// The returned dialer supports context cancellation for direct and HTTP(S)
// 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 {
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
// TLS handshake to the target using opts.TLSConfig.
func NewTLSDialer(opts DialerOpts) (ContextDialer, error) {
func NewTLSDialer(opts Opts) (ContextDialer, error) {
if opts.Timeout <= 0 {
opts.Timeout = 30 * time.Second
}
@@ -247,7 +247,7 @@ func getProxyURLFromEnv(name string) *url.URL {
// HTTPS_PROXY, and NO_PROXY/no_proxy.
// - Connection and TLS handshake timeouts are derived from opts.Timeout.
// - 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 {
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.
func newSOCKS5Dialer(u *url.URL, opts DialerOpts) (ContextDialer, error) {
func newSOCKS5Dialer(u *url.URL, opts Opts) (ContextDialer, error) {
var auth *xproxy.Auth
if u.User != nil {
user := u.User.Username()

1
lib/duration/duration.go Normal file
View File

@@ -0,0 +1 @@
package duration

View File

@@ -1,4 +1,4 @@
package lib
package fetch
import (
"context"
@@ -12,6 +12,8 @@ import (
"git.wntrmute.dev/kyle/goutils/certlib"
"git.wntrmute.dev/kyle/goutils/certlib/hosts"
"git.wntrmute.dev/kyle/goutils/fileutil"
"git.wntrmute.dev/kyle/goutils/lib"
"git.wntrmute.dev/kyle/goutils/lib/dialer"
)
// Note: Previously this package exposed a FetcherOpts type. It has been
@@ -61,18 +63,18 @@ func ParseServer(host string) (*ServerFetcher, error) {
}
func (sf *ServerFetcher) String() string {
return fmt.Sprintf("tls://%s", net.JoinHostPort(sf.host, Itoa(sf.port, -1)))
return fmt.Sprintf("tls://%s", net.JoinHostPort(sf.host, lib.Itoa(sf.port, -1)))
}
func (sf *ServerFetcher) GetChain() ([]*x509.Certificate, error) {
opts := DialerOpts{
opts := dialer.Opts{
TLSConfig: &tls.Config{
InsecureSkipVerify: sf.insecure, // #nosec G402 - no shit sherlock
RootCAs: sf.roots,
},
}
conn, err := DialTLS(context.Background(), net.JoinHostPort(sf.host, Itoa(sf.port, -1)), opts)
conn, err := dialer.DialTLS(context.Background(), net.JoinHostPort(sf.host, lib.Itoa(sf.port, -1)), opts)
if err != nil {
return nil, fmt.Errorf("failed to dial server: %w", err)
}

View File

@@ -1,10 +1,13 @@
package lib
import (
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"time"
)
@@ -111,6 +114,88 @@ func Duration(d time.Duration) string {
return s
}
// IsDigit checks if a byte is a decimal digit.
func IsDigit(b byte) bool {
return b >= '0' && b <= '9'
}
const signedaMask64 = 1<<63 - 1
// ParseDuration parses a duration string into a time.Duration.
// It supports standard units (ns, us/µs, ms, s, m, h) plus extended units:
// d (days, 24h), w (weeks, 7d), y (years, 365d).
// Units can be combined without spaces, e.g., "1y2w3d4h5m6s".
// Case-insensitive. Years and days are approximations (no leap seconds/months).
// Returns an error for invalid input.
func ParseDuration(s string) (time.Duration, error) {
s = strings.ToLower(s) // Normalize to lowercase for case-insensitivity.
if s == "" {
return 0, errors.New("empty duration string")
}
var total time.Duration
i := 0
for i < len(s) {
// Parse the number part.
start := i
for i < len(s) && IsDigit(s[i]) {
i++
}
if start == i {
return 0, fmt.Errorf("expected number at position %d", start)
}
numStr := s[start:i]
num, err := strconv.ParseUint(numStr, 10, 64)
if err != nil {
return 0, fmt.Errorf("invalid number %q: %w", numStr, err)
}
// Parse the unit part.
if i >= len(s) {
return 0, fmt.Errorf("expected unit after number %q", numStr)
}
unitStart := i
i++ // Consume the first char of the unit.
unit := s[unitStart:i]
// Handle potential two-char units like "ms".
if unit == "m" && i < len(s) && s[i] == 's' {
i++ // Consume the 's'.
unit = "ms"
}
// Convert to duration based on unit.
var d time.Duration
switch unit {
case "ns":
d = time.Nanosecond * time.Duration(num&signedaMask64) // #nosec G115 - masked off
case "us", "µs":
d = time.Microsecond * time.Duration(num&signedaMask64) // #nosec G115 - masked off
case "ms":
d = time.Millisecond * time.Duration(num&signedaMask64) // #nosec G115 - masked off
case "s":
d = time.Second * time.Duration(num&signedaMask64) // #nosec G115 - masked off
case "m":
d = time.Minute * time.Duration(num&signedaMask64) // #nosec G115 - masked off
case "h":
d = time.Hour * time.Duration(num&signedaMask64) // #nosec G115 - masked off
case "d":
d = 24 * time.Hour * time.Duration(num&signedaMask64) // #nosec G115 - masked off
case "w":
d = 7 * 24 * time.Hour * time.Duration(num&signedaMask64) // #nosec G115 - masked off
case "y":
// Approximate, non-leap year.
d = 365 * 24 * time.Hour * time.Duration(num&signedaMask64) // #nosec G115 - masked off;
default:
return 0, fmt.Errorf("unknown unit %q at position %d", s[unitStart:i], unitStart)
}
total += d
}
return total, nil
}
type HexEncodeMode uint8
const (
@@ -126,6 +211,8 @@ const (
HexEncodeUpperColon
// HexEncodeBytes prints the string as a sequence of []byte.
HexEncodeBytes
// HexEncodeBase64 prints the string as a base64-encoded string.
HexEncodeBase64
)
func (m HexEncodeMode) String() string {
@@ -140,6 +227,8 @@ func (m HexEncodeMode) String() string {
return "ucolon"
case HexEncodeBytes:
return "bytes"
case HexEncodeBase64:
return "base64"
default:
panic("invalid hex encode mode")
}
@@ -157,6 +246,8 @@ func ParseHexEncodeMode(s string) HexEncodeMode {
return HexEncodeUpperColon
case "bytes":
return HexEncodeBytes
case "base64":
return HexEncodeBase64
}
panic("invalid hex encode mode")
@@ -218,21 +309,22 @@ func bytesAsByteSliceString(buf []byte) string {
return sb.String()
}
// HexEncode encodes the given bytes as a hexadecimal string.
// HexEncode encodes the given bytes as a hexadecimal string. It
// also supports a few other binary-encoding formats as well.
func HexEncode(b []byte, mode HexEncodeMode) string {
str := hexEncode(b)
switch mode {
case HexEncodeLower:
return str
return hexEncode(b)
case HexEncodeUpper:
return strings.ToUpper(str)
return strings.ToUpper(hexEncode(b))
case HexEncodeLowerColon:
return hexColons(str)
return hexColons(hexEncode(b))
case HexEncodeUpperColon:
return strings.ToUpper(hexColons(str))
return strings.ToUpper(hexColons(hexEncode(b)))
case HexEncodeBytes:
return bytesAsByteSliceString(b)
case HexEncodeBase64:
return base64.StdEncoding.EncodeToString(b)
default:
panic("invalid hex encode mode")
}

View File

@@ -2,10 +2,46 @@ package lib_test
import (
"testing"
"time"
"git.wntrmute.dev/kyle/goutils/lib"
)
func TestParseDuration(t *testing.T) {
tests := []struct {
name string
input string
expected time.Duration
wantErr bool
}{
// Valid durations
{"hour", "1h", time.Hour, false},
{"day", "2d", 2 * 24 * time.Hour, false},
{"minute", "3m", 3 * time.Minute, false},
{"second", "4s", 4 * time.Second, false},
// Edge cases
{"zero seconds", "0s", 0, false},
{"empty string", "", 0, true},
{"no numeric before unit", "h", 0, true},
{"invalid unit", "1x", 0, true},
{"non-numeric input", "abc", 0, true},
{"missing unit", "10", 0, true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got, err := lib.ParseDuration(tc.input)
if (err != nil) != tc.wantErr {
t.Fatalf("unexpected error: %v, wantErr: %v", err, tc.wantErr)
}
if got != tc.expected {
t.Fatalf("expected %v, got %v", tc.expected, got)
}
})
}
}
func TestHexEncode_LowerUpper(t *testing.T) {
b := []byte{0x0f, 0xa1, 0x00, 0xff}