Skip to content

Commit 13e9940

Browse files
authored
Merge pull request #130 from sensiblebit/chore/strict-golangci-lint
chore(lint): enforce strict golangci rules
2 parents 066ad9f + 581ecd7 commit 13e9940

83 files changed

Lines changed: 1561 additions & 600 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.golangci.yml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
version: "2"
2+
3+
run:
4+
timeout: 10m
5+
tests: true
6+
7+
issues:
8+
max-issues-per-linter: 0
9+
max-same-issues: 0
10+
11+
linters:
12+
default: standard
13+
enable:
14+
- bodyclose
15+
- contextcheck
16+
- err113
17+
- errname
18+
- errorlint
19+
- exhaustive
20+
- gocritic
21+
- gosec
22+
- loggercheck
23+
- misspell
24+
- modernize
25+
- nilerr
26+
- nilnil
27+
- nilnesserr
28+
- nolintlint
29+
- perfsprint
30+
- predeclared
31+
- revive
32+
- rowserrcheck
33+
- sloglint
34+
- sqlclosecheck
35+
- testableexamples
36+
- testifylint
37+
- thelper
38+
- tparallel
39+
- unconvert
40+
- unparam
41+
- usetesting
42+
- usestdlibvars
43+
- wastedassign
44+
- wrapcheck
45+
- canonicalheader
46+
- copyloopvar
47+
- durationcheck
48+
- fatcontext

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616
- Consolidate shared `connect` status-line formatting between standard and verbose text output paths. ([#121])
1717
- Centralize pre-commit hooks under the shared `sensiblebit/.github` hook set (including shared `markdownlint`) and run dependency update hooks first; refresh resulting indirect Go and web lockfile dependencies. ([#128])
1818
- Remove the arbitrary 200-file cap from WASM upload and inspect flows; rely on byte limits instead. ([#129])
19+
- Enforce a stricter repo-local `golangci-lint` policy and refactor error handling, protocol encoding helpers, file-permission behavior, and tests to satisfy the higher lint bar. ([#130])
1920

2021
### Added
2122

CLAUDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ Every PR runs 11 parallel checks (`.github/workflows/ci.yml`):
318318
| PR Conventions | Branch name, commit messages, verified commits |
319319
| Go Checks | `go build`, `go fix`, `go vet`, goimports |
320320
| Go Test | `go test -race -count=1 ./...` |
321-
| Lint (golangci-lint) | errcheck, staticcheck, unused, etc. |
321+
| Lint (golangci-lint) | standard linters plus any stricter repo-local config in `.golangci.yml` |
322322
| Vulnerability Check | `govulncheck ./...` |
323323
| WASM Build | `GOOS=js GOARCH=wasm` vet + build |
324324
| Docs | `go generate ./...` + `git diff --exit-code README.md` |
@@ -352,7 +352,7 @@ These are enforced by the pre-commit hooks; run `pre-commit run --all-files` ins
352352
- **G-1 (MUST)** `go fix ./...` leaves no pending changes.
353353
- **G-2 (MUST)** `go vet ./...` passes.
354354
- **G-3 (MUST)** `go test -race ./...` passes.
355-
- **G-4 (MUST)** `golangci-lint run` passes with default linters (errcheck, staticcheck, unused, etc.). No `.golangci.yml` config — uses golangci-lint defaults.
355+
- **G-4 (MUST)** `golangci-lint run` passes. Repo-local `.golangci.yml` is allowed when the repo intentionally enforces stricter rules than golangci-lint defaults.
356356
- **G-5 (MUST)** `GOOS=js GOARCH=wasm go vet ./cmd/wasm/` and `go build` pass.
357357
- **G-6 (MUST)** `cd web && npm test` passes (vitest).
358358
- **G-7 (MUST)** `cd web && wrangler pages functions build` compiles (local only, no credentials).

bundle.go

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,28 @@ import (
2323
var (
2424
mozillaPoolOnce sync.Once
2525
mozillaPool *x509.CertPool
26-
mozillaPoolErr error
26+
errMozillaPool error
2727
mozillaSubjectsOnce sync.Once
2828
mozillaSubjects map[string]bool
2929
mozillaRootKeysOnce sync.Once
3030
mozillaRootKeys map[string][]byte // RawSubject → marshaled PKIX public key
3131

3232
// ErrChainVerificationFailed indicates that certificate path validation failed.
33-
ErrChainVerificationFailed = errors.New("chain verification failed")
33+
ErrChainVerificationFailed = errors.New("chain verification failed")
34+
errMozillaRootParse = errors.New("parsing embedded Mozilla root certificates")
35+
errAIAAddressBlocked = errors.New("blocked address for AIA fetch")
36+
errAIAPrivateAddress = errors.New("blocked private address for AIA fetch")
37+
errAIAUnsupportedScheme = errors.New("unsupported scheme")
38+
errAIAMissingHostname = errors.New("missing hostname in URL")
39+
errAIAResolveNoIPs = errors.New("no IP addresses returned")
40+
errFetchLeafHTTPSRequired = errors.New("invalid URL scheme")
41+
errFetchLeafMissingHostname = errors.New("fetch leaf URL is missing hostname")
42+
errFetchLeafNotTLS = errors.New("TLS dial did not return TLS connection")
43+
errFetchLeafNoCerts = errors.New("no certificates returned by TLS server")
44+
errAIAFetchRedirects = errors.New("AIA redirect limit exceeded")
45+
errAIAHTTPStatus = errors.New("AIA server returned non-200 status")
46+
errBundleLeafNil = errors.New("leaf certificate is nil")
47+
errBundleUnknownTrustStore = errors.New("unknown trust_store")
3448
)
3549

3650
// privateNetworks contains CIDR ranges for private, reserved, and shared
@@ -69,12 +83,12 @@ func MozillaRootPool() (*x509.CertPool, error) {
6983
mozillaPoolOnce.Do(func() {
7084
pool := x509.NewCertPool()
7185
if !pool.AppendCertsFromPEM([]byte(embedded.MozillaCACertificatesPEM())) {
72-
mozillaPoolErr = errors.New("parsing embedded Mozilla root certificates")
86+
errMozillaPool = errMozillaRootParse
7387
return
7488
}
7589
mozillaPool = pool
7690
})
77-
return mozillaPool, mozillaPoolErr
91+
return mozillaPool, errMozillaPool
7892
}
7993

8094
// MozillaRootSubjects returns a set of raw ASN.1 subject byte strings from all
@@ -186,11 +200,11 @@ type lookupIPAddressesFunc func(ctx context.Context, host string) ([]net.IP, err
186200

187201
func ipBlockedForAIA(ip net.IP) error {
188202
if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || ip.IsUnspecified() {
189-
return fmt.Errorf("blocked address %s (loopback, link-local, or unspecified)", ip.String())
203+
return fmt.Errorf("%w %s (loopback, link-local, or unspecified)", errAIAAddressBlocked, ip.String())
190204
}
191205
for _, network := range privateNetworks {
192206
if network.Contains(ip) {
193-
return fmt.Errorf("blocked private address %s", ip.String())
207+
return fmt.Errorf("%w %s", errAIAPrivateAddress, ip.String())
194208
}
195209
}
196210
return nil
@@ -213,11 +227,11 @@ func ValidateAIAURLWithOptions(ctx context.Context, input ValidateAIAURLInput) e
213227
case "http", "https":
214228
// allowed
215229
default:
216-
return fmt.Errorf("unsupported scheme %q (only http and https are allowed)", parsed.Scheme)
230+
return fmt.Errorf("%w %q (only http and https are allowed)", errAIAUnsupportedScheme, parsed.Scheme)
217231
}
218232
host := parsed.Hostname()
219233
if host == "" {
220-
return fmt.Errorf("missing hostname in URL")
234+
return errAIAMissingHostname
221235
}
222236

223237
if input.AllowPrivateNetworks {
@@ -248,7 +262,7 @@ func ValidateAIAURLWithOptions(ctx context.Context, input ValidateAIAURLInput) e
248262
return fmt.Errorf("resolving host %q: %w", host, err)
249263
}
250264
if len(ips) == 0 {
251-
return fmt.Errorf("resolving host %q: no IP addresses returned", host)
265+
return fmt.Errorf("resolving host %q: %w", host, errAIAResolveNoIPs)
252266
}
253267
for _, resolvedIP := range ips {
254268
if blockedErr := ipBlockedForAIA(resolvedIP); blockedErr != nil {
@@ -366,12 +380,12 @@ func FetchLeafFromURL(ctx context.Context, input FetchLeafFromURLInput) (*x509.C
366380
return nil, fmt.Errorf("parsing URL: %w", err)
367381
}
368382
if parsed.Scheme != "https" {
369-
return nil, fmt.Errorf("invalid URL scheme %q (https required)", parsed.Scheme)
383+
return nil, fmt.Errorf("%w: %q", errFetchLeafHTTPSRequired, parsed.Scheme)
370384
}
371385

372386
host := parsed.Hostname()
373387
if host == "" {
374-
return nil, fmt.Errorf("missing hostname in URL")
388+
return nil, errFetchLeafMissingHostname
375389
}
376390
port := parsed.Port()
377391
if port == "" {
@@ -395,11 +409,11 @@ func FetchLeafFromURL(ctx context.Context, input FetchLeafFromURLInput) (*x509.C
395409

396410
tlsConn, ok := conn.(*tls.Conn)
397411
if !ok {
398-
return nil, fmt.Errorf("TLS dial to %s:%s: connection is not TLS", host, port)
412+
return nil, fmt.Errorf("%w for %s:%s", errFetchLeafNotTLS, host, port)
399413
}
400414
certs := tlsConn.ConnectionState().PeerCertificates
401415
if len(certs) == 0 {
402-
return nil, fmt.Errorf("no certificates returned by %s:%s", host, port)
416+
return nil, fmt.Errorf("%w for %s:%s", errFetchLeafNoCerts, host, port)
403417
}
404418
return certs[0], nil
405419
}
@@ -429,7 +443,7 @@ func FetchAIACertificates(ctx context.Context, input FetchAIACertificatesInput)
429443
Timeout: input.Timeout,
430444
CheckRedirect: func(req *http.Request, via []*http.Request) error {
431445
if len(via) >= maxRedirects {
432-
return fmt.Errorf("stopped after %d redirects", maxRedirects)
446+
return fmt.Errorf("%w: stopped after %d redirects", errAIAFetchRedirects, maxRedirects)
433447
}
434448
if err := ValidateAIAURLWithOptions(req.Context(), ValidateAIAURLInput{URL: req.URL.String(), AllowPrivateNetworks: input.AllowPrivateNetworks}); err != nil {
435449
return fmt.Errorf("redirect blocked: %w", err)
@@ -488,7 +502,7 @@ func fetchCertificatesFromURL(ctx context.Context, input fetchCertificatesFromUR
488502
defer func() { _ = resp.Body.Close() }()
489503

490504
if resp.StatusCode != http.StatusOK {
491-
return nil, fmt.Errorf("HTTP %d from %s", resp.StatusCode, input.URL)
505+
return nil, fmt.Errorf("%w: HTTP %d from %s", errAIAHTTPStatus, resp.StatusCode, input.URL)
492506
}
493507

494508
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) // 1MB limit
@@ -538,8 +552,7 @@ func detectAndSwapLeaf(leaf *x509.Certificate, extras []*x509.Certificate) (*x50
538552
func checkSHA1Signatures(chain []*x509.Certificate) []string {
539553
var warnings []string
540554
for _, cert := range chain {
541-
switch cert.SignatureAlgorithm {
542-
case x509.SHA1WithRSA, x509.ECDSAWithSHA1:
555+
if cert.SignatureAlgorithm == x509.SHA1WithRSA || cert.SignatureAlgorithm == x509.ECDSAWithSHA1 {
543556
warnings = append(warnings, fmt.Sprintf("certificate %q uses deprecated SHA-1 signature algorithm (%s)", cert.Subject.CommonName, cert.SignatureAlgorithm))
544557
}
545558
}
@@ -572,7 +585,7 @@ type BundleInput struct {
572585
// Bundle resolves the full certificate chain for a leaf certificate.
573586
func Bundle(ctx context.Context, input BundleInput) (*BundleResult, error) {
574587
if input.Leaf == nil {
575-
return nil, fmt.Errorf("leaf certificate is nil")
588+
return nil, errBundleLeafNil
576589
}
577590
leaf := input.Leaf
578591
opts := input.Options
@@ -627,7 +640,7 @@ func Bundle(ctx context.Context, input BundleInput) (*BundleResult, error) {
627640
rootPool.AddCert(cert)
628641
}
629642
default:
630-
return nil, fmt.Errorf("unknown trust_store: %q", opts.TrustStore)
643+
return nil, fmt.Errorf("%w: %q", errBundleUnknownTrustStore, opts.TrustStore)
631644
}
632645

633646
// Verify

bundle_lookup_default.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,16 @@ package certkit
44

55
import (
66
"context"
7+
"fmt"
78
"net"
89
)
910

1011
func defaultLookupIPAddresses(ctx context.Context, host string) ([]net.IP, error) {
11-
return net.DefaultResolver.LookupIP(ctx, "ip", host)
12+
ips, err := net.DefaultResolver.LookupIP(ctx, "ip", host)
13+
if err != nil {
14+
return nil, fmt.Errorf("lookup IP addresses for %s: %w", host, err)
15+
}
16+
return ips, nil
1217
}
1318

1419
func aiaDNSResolutionAvailable() bool {

bundle_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -588,7 +588,7 @@ func TestFetchAIACertificates_duplicateURLs(t *testing.T) {
588588
}
589589

590590
var fetchCount atomic.Int32
591-
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
591+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
592592
fetchCount.Add(1)
593593
_, _ = w.Write(issuerBytes)
594594
}))

0 commit comments

Comments
 (0)