diff --git a/.junie/memory/language.json b/.junie/memory/language.json index e320ead..b450953 100644 --- a/.junie/memory/language.json +++ b/.junie/memory/language.json @@ -1 +1 @@ -[{"lang":"en","usageCount":2}] \ No newline at end of file +[{"lang":"en","usageCount":4}] \ No newline at end of file diff --git a/cmd/metacrypt/init.go b/cmd/metacrypt/init.go index 7624a3c..ec9a12e 100644 --- a/cmd/metacrypt/init.go +++ b/cmd/metacrypt/init.go @@ -43,7 +43,7 @@ func runInit(cmd *cobra.Command, args []string) error { if err != nil { return err } - defer database.Close() + defer func() { _ = database.Close() }() if err := db.Migrate(database); err != nil { return err diff --git a/cmd/metacrypt/root.go b/cmd/metacrypt/root.go index 7cf2f2c..061831f 100644 --- a/cmd/metacrypt/root.go +++ b/cmd/metacrypt/root.go @@ -29,5 +29,5 @@ func initConfig() { } viper.AutomaticEnv() viper.SetEnvPrefix("METACRYPT") - viper.ReadInConfig() + _ = viper.ReadInConfig() } diff --git a/cmd/metacrypt/server.go b/cmd/metacrypt/server.go index 3484ef7..5c40537 100644 --- a/cmd/metacrypt/server.go +++ b/cmd/metacrypt/server.go @@ -50,7 +50,7 @@ func runServer(cmd *cobra.Command, args []string) error { if err != nil { return err } - defer database.Close() + defer func() { _ = database.Close() }() if err := db.Migrate(database); err != nil { return err diff --git a/cmd/metacrypt/snapshot.go b/cmd/metacrypt/snapshot.go index 82da51f..99ede15 100644 --- a/cmd/metacrypt/snapshot.go +++ b/cmd/metacrypt/snapshot.go @@ -40,7 +40,7 @@ func runSnapshot(cmd *cobra.Command, args []string) error { if err != nil { return err } - defer database.Close() + defer func() { _ = database.Close() }() if err := sqliteBackup(database, snapshotOutput); err != nil { return err diff --git a/cmd/metacrypt/status.go b/cmd/metacrypt/status.go index 5a3cabd..c749df3 100644 --- a/cmd/metacrypt/status.go +++ b/cmd/metacrypt/status.go @@ -26,7 +26,7 @@ var ( func init() { statusCmd.Flags().StringVar(&statusAddr, "addr", "", "server address (e.g., https://localhost:8443)") statusCmd.Flags().StringVar(&statusCACert, "ca-cert", "", "path to CA certificate for TLS verification") - statusCmd.MarkFlagRequired("addr") + _ = statusCmd.MarkFlagRequired("addr") rootCmd.AddCommand(statusCmd) } @@ -34,7 +34,7 @@ func runStatus(cmd *cobra.Command, args []string) error { tlsCfg := &tls.Config{MinVersion: tls.VersionTLS12} if statusCACert != "" { - pem, err := os.ReadFile(statusCACert) + pem, err := os.ReadFile(statusCACert) //nolint:gosec if err != nil { return fmt.Errorf("read CA cert: %w", err) } @@ -53,7 +53,7 @@ func runStatus(cmd *cobra.Command, args []string) error { if err != nil { return err } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() var status struct { State string `json:"state"` diff --git a/internal/acme/handlers.go b/internal/acme/handlers.go index 2c1a79c..3f6115b 100644 --- a/internal/acme/handlers.go +++ b/internal/acme/handlers.go @@ -19,12 +19,12 @@ import ( // directoryResponse is the ACME directory object (RFC 8555 §7.1.1). type directoryResponse struct { - NewNonce string `json:"newNonce"` - NewAccount string `json:"newAccount"` - NewOrder string `json:"newOrder"` - RevokeCert string `json:"revokeCert"` - KeyChange string `json:"keyChange"` Meta *directoryMeta `json:"meta,omitempty"` + NewNonce string `json:"newNonce"` + NewAccount string `json:"newAccount"` + NewOrder string `json:"newOrder"` + RevokeCert string `json:"revokeCert"` + KeyChange string `json:"keyChange"` } type directoryMeta struct { @@ -49,7 +49,7 @@ func (h *Handler) handleDirectory(w http.ResponseWriter, r *http.Request) { }, } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(dir) + _ = json.NewEncoder(w).Encode(dir) } // handleNewNonce serves HEAD and GET /acme/{mount}/new-nonce. @@ -65,10 +65,10 @@ func (h *Handler) handleNewNonce(w http.ResponseWriter, r *http.Request) { // newAccountPayload is the payload for the new-account request. type newAccountPayload struct { - TermsOfServiceAgreed bool `json:"termsOfServiceAgreed"` - Contact []string `json:"contact,omitempty"` + Contact []string `json:"contact,omitempty"` ExternalAccountBinding json.RawMessage `json:"externalAccountBinding,omitempty"` - OnlyReturnExisting bool `json:"onlyReturnExisting"` + TermsOfServiceAgreed bool `json:"termsOfServiceAgreed"` + OnlyReturnExisting bool `json:"onlyReturnExisting"` } // handleNewAccount handles POST /acme/{mount}/new-account. @@ -172,9 +172,9 @@ func (h *Handler) handleNewAccount(w http.ResponseWriter, r *http.Request) { // newOrderPayload is the payload for the new-order request. type newOrderPayload struct { - Identifiers []Identifier `json:"identifiers"` NotBefore string `json:"notBefore,omitempty"` NotAfter string `json:"notAfter,omitempty"` + Identifiers []Identifier `json:"identifiers"` } // handleNewOrder handles POST /acme/{mount}/new-order. @@ -350,7 +350,7 @@ func (h *Handler) handleChallenge(w http.ResponseWriter, r *http.Request) { h.writeJSON(w, http.StatusOK, h.challengeToWire(chall)) // Launch validation goroutine. - go h.validateChallenge(context.Background(), chall, acc.JWK) + go h.validateChallenge(context.Background(), chall, acc.JWK) //nolint:gosec } // handleFinalize handles POST /acme/{mount}/finalize/{id}. @@ -468,7 +468,7 @@ func (h *Handler) handleFinalize(w http.ResponseWriter, r *http.Request) { order.Status = StatusValid order.CertID = certID orderData, _ := json.Marshal(order) - h.barrier.Put(ctx, h.barrierPrefix()+"orders/"+orderID+".json", orderData) + _ = h.barrier.Put(ctx, h.barrierPrefix()+"orders/"+orderID+".json", orderData) h.writeJSON(w, http.StatusOK, h.orderToWire(order)) } @@ -502,7 +502,7 @@ func (h *Handler) handleGetCert(w http.ResponseWriter, r *http.Request) { h.addNonceHeader(w) w.Header().Set("Content-Type", "application/pem-certificate-chain") w.WriteHeader(http.StatusOK) - w.Write([]byte(cert.CertPEM)) + _, _ = w.Write([]byte(cert.CertPEM)) } // handleRevokeCert handles POST /acme/{mount}/revoke-cert. @@ -564,7 +564,7 @@ func (h *Handler) handleRevokeCert(w http.ResponseWriter, r *http.Request) { if issuedCert.SerialNumber.Cmp(targetCert.SerialNumber) == 0 { cert.Revoked = true updated, _ := json.Marshal(cert) - h.barrier.Put(ctx, h.barrierPrefix()+"certs/"+p, updated) + _ = h.barrier.Put(ctx, h.barrierPrefix()+"certs/"+p, updated) h.addNonceHeader(w) w.WriteHeader(http.StatusOK) return @@ -726,11 +726,11 @@ func (h *Handler) orderToWire(order *Order) map[string]interface{} { authzURLs[i] = h.authzURL(id) } m := map[string]interface{}{ - "status": order.Status, - "expires": order.ExpiresAt.Format(time.RFC3339), - "identifiers": order.Identifiers, + "status": order.Status, + "expires": order.ExpiresAt.Format(time.RFC3339), + "identifiers": order.Identifiers, "authorizations": authzURLs, - "finalize": h.finalizeURL(order.ID), + "finalize": h.finalizeURL(order.ID), } if order.CertID != "" { m["certificate"] = h.certURL(order.CertID) @@ -859,7 +859,7 @@ func readBody(r *http.Request) ([]byte, error) { if r.Body == nil { return nil, errors.New("empty body") } - defer r.Body.Close() + defer func() { _ = r.Body.Close() }() buf := make([]byte, 0, 4096) tmp := make([]byte, 512) for { diff --git a/internal/acme/jws.go b/internal/acme/jws.go index 2716893..df01a6b 100644 --- a/internal/acme/jws.go +++ b/internal/acme/jws.go @@ -27,18 +27,18 @@ type JWSHeader struct { Alg string `json:"alg"` Nonce string `json:"nonce"` URL string `json:"url"` - JWK json.RawMessage `json:"jwk,omitempty"` // present for new-account / new-order with new key - KID string `json:"kid,omitempty"` // present for subsequent requests + KID string `json:"kid,omitempty"` + JWK json.RawMessage `json:"jwk,omitempty"` } // ParsedJWS is the result of parsing a JWS request body. // Signature verification is NOT performed here; call VerifyJWS separately. type ParsedJWS struct { - Header JWSHeader - Payload []byte // decoded payload bytes; empty-string payload decodes to nil - SigningInput []byte // protected + "." + payload (ASCII; used for signature verification) - RawSignature []byte // decoded signature bytes - RawBody JWSFlat + Header JWSHeader + RawBody JWSFlat + Payload []byte + SigningInput []byte + RawSignature []byte } // ParseJWS decodes the flattened JWS from body without verifying the signature. @@ -77,7 +77,7 @@ func ParseJWS(body []byte) (*ParsedJWS, error) { return &ParsedJWS{ Header: header, Payload: payload, - SigningInput: signingInput, + SigningInput: signingInput, RawSignature: sig, RawBody: flat, }, nil diff --git a/internal/acme/nonce.go b/internal/acme/nonce.go index d62ced5..d1fca96 100644 --- a/internal/acme/nonce.go +++ b/internal/acme/nonce.go @@ -13,9 +13,9 @@ const nonceLifetime = 10 * time.Minute // NonceStore is a thread-safe single-use nonce store with expiry. // Nonces are short-lived per RFC 8555 §7.2. type NonceStore struct { - mu sync.Mutex nonces map[string]time.Time issued int + mu sync.Mutex } // NewNonceStore creates a new nonce store. diff --git a/internal/acme/server.go b/internal/acme/server.go index 6c39c9d..9e6e2fe 100644 --- a/internal/acme/server.go +++ b/internal/acme/server.go @@ -15,12 +15,12 @@ import ( // Handler implements the ACME protocol for a single CA mount. type Handler struct { - mount string barrier barrier.Barrier engines *engine.Registry nonces *NonceStore - baseURL string logger *slog.Logger + mount string + baseURL string } // NewHandler creates an ACME handler for the given CA mount. diff --git a/internal/acme/types.go b/internal/acme/types.go index 82260c4..4e1999c 100644 --- a/internal/acme/types.go +++ b/internal/acme/types.go @@ -4,63 +4,63 @@ import "time" // Account represents an ACME account (RFC 8555 §7.1.2). type Account struct { - ID string `json:"id"` - Status string `json:"status"` // "valid", "deactivated", "revoked" - Contact []string `json:"contact,omitempty"` - JWK []byte `json:"jwk"` // canonical JSON of account public key CreatedAt time.Time `json:"created_at"` - MCIASUsername string `json:"mcias_username"` // MCIAS user who created via EAB + ID string `json:"id"` + Status string `json:"status"` + MCIASUsername string `json:"mcias_username"` + Contact []string `json:"contact,omitempty"` + JWK []byte `json:"jwk"` } // EABCredential is an External Account Binding credential (RFC 8555 §7.3.4). type EABCredential struct { - KID string `json:"kid"` - HMACKey []byte `json:"hmac_key"` // raw 32-byte secret - Used bool `json:"used"` - CreatedBy string `json:"created_by"` // MCIAS username CreatedAt time.Time `json:"created_at"` + KID string `json:"kid"` + CreatedBy string `json:"created_by"` + HMACKey []byte `json:"hmac_key"` + Used bool `json:"used"` } // Order represents an ACME certificate order (RFC 8555 §7.1.3). type Order struct { - ID string `json:"id"` - AccountID string `json:"account_id"` - Status string `json:"status"` // "pending","ready","processing","valid","invalid" - Identifiers []Identifier `json:"identifiers"` - AuthzIDs []string `json:"authz_ids"` - CertID string `json:"cert_id,omitempty"` - NotBefore *time.Time `json:"not_before,omitempty"` - NotAfter *time.Time `json:"not_after,omitempty"` ExpiresAt time.Time `json:"expires_at"` CreatedAt time.Time `json:"created_at"` - IssuerName string `json:"issuer_name"` // which CA issuer to sign with + NotBefore *time.Time `json:"not_before,omitempty"` + NotAfter *time.Time `json:"not_after,omitempty"` + ID string `json:"id"` + AccountID string `json:"account_id"` + Status string `json:"status"` + CertID string `json:"cert_id,omitempty"` + IssuerName string `json:"issuer_name"` + Identifiers []Identifier `json:"identifiers"` + AuthzIDs []string `json:"authz_ids"` } // Identifier is a domain name or IP address in an order. type Identifier struct { - Type string `json:"type"` // "dns" or "ip" + Type string `json:"type"` // "dns" or "ip" Value string `json:"value"` } // Authorization represents an ACME authorization (RFC 8555 §7.1.4). type Authorization struct { + ExpiresAt time.Time `json:"expires_at"` + Identifier Identifier `json:"identifier"` ID string `json:"id"` AccountID string `json:"account_id"` - Status string `json:"status"` // "pending","valid","invalid","expired","deactivated","revoked" - Identifier Identifier `json:"identifier"` + Status string `json:"status"` ChallengeIDs []string `json:"challenge_ids"` - ExpiresAt time.Time `json:"expires_at"` } // Challenge represents an ACME challenge (RFC 8555 §8). type Challenge struct { - ID string `json:"id"` - AuthzID string `json:"authz_id"` - Type string `json:"type"` // "http-01" or "dns-01" - Status string `json:"status"` // "pending","processing","valid","invalid" - Token string `json:"token"` // base64url, 43 chars (32 random bytes) Error *ProblemDetail `json:"error,omitempty"` ValidatedAt *time.Time `json:"validated_at,omitempty"` + ID string `json:"id"` + AuthzID string `json:"authz_id"` + Type string `json:"type"` + Status string `json:"status"` + Token string `json:"token"` } // ProblemDetail is an RFC 7807 problem detail for ACME errors. @@ -71,12 +71,12 @@ type ProblemDetail struct { // IssuedCert stores the PEM and metadata for a certificate issued via ACME. type IssuedCert struct { + IssuedAt time.Time `json:"issued_at"` + ExpiresAt time.Time `json:"expires_at"` ID string `json:"id"` OrderID string `json:"order_id"` AccountID string `json:"account_id"` - CertPEM string `json:"cert_pem"` // full chain PEM - IssuedAt time.Time `json:"issued_at"` - ExpiresAt time.Time `json:"expires_at"` + CertPEM string `json:"cert_pem"` Revoked bool `json:"revoked"` } @@ -104,27 +104,27 @@ const ( // ACME problem type URIs (RFC 8555 §6.7). const ( - ProblemAccountDoesNotExist = "urn:ietf:params:acme:error:accountDoesNotExist" - ProblemAlreadyRevoked = "urn:ietf:params:acme:error:alreadyRevoked" - ProblemBadCSR = "urn:ietf:params:acme:error:badCSR" - ProblemBadNonce = "urn:ietf:params:acme:error:badNonce" - ProblemBadPublicKey = "urn:ietf:params:acme:error:badPublicKey" - ProblemBadRevocationReason = "urn:ietf:params:acme:error:badRevocationReason" - ProblemBadSignatureAlg = "urn:ietf:params:acme:error:badSignatureAlgorithm" - ProblemCAA = "urn:ietf:params:acme:error:caa" - ProblemConnection = "urn:ietf:params:acme:error:connection" - ProblemDNS = "urn:ietf:params:acme:error:dns" + ProblemAccountDoesNotExist = "urn:ietf:params:acme:error:accountDoesNotExist" + ProblemAlreadyRevoked = "urn:ietf:params:acme:error:alreadyRevoked" + ProblemBadCSR = "urn:ietf:params:acme:error:badCSR" + ProblemBadNonce = "urn:ietf:params:acme:error:badNonce" + ProblemBadPublicKey = "urn:ietf:params:acme:error:badPublicKey" + ProblemBadRevocationReason = "urn:ietf:params:acme:error:badRevocationReason" + ProblemBadSignatureAlg = "urn:ietf:params:acme:error:badSignatureAlgorithm" + ProblemCAA = "urn:ietf:params:acme:error:caa" + ProblemConnection = "urn:ietf:params:acme:error:connection" + ProblemDNS = "urn:ietf:params:acme:error:dns" ProblemExternalAccountRequired = "urn:ietf:params:acme:error:externalAccountRequired" - ProblemIncorrectResponse = "urn:ietf:params:acme:error:incorrectResponse" - ProblemInvalidContact = "urn:ietf:params:acme:error:invalidContact" - ProblemMalformed = "urn:ietf:params:acme:error:malformed" - ProblemOrderNotReady = "urn:ietf:params:acme:error:orderNotReady" - ProblemRateLimited = "urn:ietf:params:acme:error:rateLimited" - ProblemRejectedIdentifier = "urn:ietf:params:acme:error:rejectedIdentifier" - ProblemServerInternal = "urn:ietf:params:acme:error:serverInternal" - ProblemTLS = "urn:ietf:params:acme:error:tls" - ProblemUnauthorized = "urn:ietf:params:acme:error:unauthorized" - ProblemUnsupportedContact = "urn:ietf:params:acme:error:unsupportedContact" - ProblemUnsupportedIdentifier = "urn:ietf:params:acme:error:unsupportedIdentifier" - ProblemUserActionRequired = "urn:ietf:params:acme:error:userActionRequired" + ProblemIncorrectResponse = "urn:ietf:params:acme:error:incorrectResponse" + ProblemInvalidContact = "urn:ietf:params:acme:error:invalidContact" + ProblemMalformed = "urn:ietf:params:acme:error:malformed" + ProblemOrderNotReady = "urn:ietf:params:acme:error:orderNotReady" + ProblemRateLimited = "urn:ietf:params:acme:error:rateLimited" + ProblemRejectedIdentifier = "urn:ietf:params:acme:error:rejectedIdentifier" + ProblemServerInternal = "urn:ietf:params:acme:error:serverInternal" + ProblemTLS = "urn:ietf:params:acme:error:tls" + ProblemUnauthorized = "urn:ietf:params:acme:error:unauthorized" + ProblemUnsupportedContact = "urn:ietf:params:acme:error:unsupportedContact" + ProblemUnsupportedIdentifier = "urn:ietf:params:acme:error:unsupportedIdentifier" + ProblemUserActionRequired = "urn:ietf:params:acme:error:userActionRequired" ) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 57ec1c5..90bfd6c 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -36,9 +36,8 @@ type cachedClaims struct { type Authenticator struct { client *mcias.Client logger *slog.Logger - - mu sync.RWMutex - cache map[string]*cachedClaims // keyed by SHA-256(token) + cache map[string]*cachedClaims + mu sync.RWMutex } // NewAuthenticator creates a new authenticator with the given MCIAS client. diff --git a/internal/barrier/barrier.go b/internal/barrier/barrier.go index 76bfec3..05f6aa7 100644 --- a/internal/barrier/barrier.go +++ b/internal/barrier/barrier.go @@ -39,8 +39,8 @@ type Barrier interface { // AESGCMBarrier implements Barrier using AES-256-GCM encryption. type AESGCMBarrier struct { db *sql.DB + mek []byte mu sync.RWMutex - mek []byte // nil when sealed } // NewAESGCMBarrier creates a new AES-GCM barrier backed by the given database. @@ -151,7 +151,7 @@ func (b *AESGCMBarrier) List(ctx context.Context, prefix string) ([]string, erro if err != nil { return nil, fmt.Errorf("barrier: list %q: %w", prefix, err) } - defer rows.Close() + defer func() { _ = rows.Close() }() var paths []string for rows.Next() { diff --git a/internal/barrier/barrier_test.go b/internal/barrier/barrier_test.go index 7bc40f0..fb6f4d8 100644 --- a/internal/barrier/barrier_test.go +++ b/internal/barrier/barrier_test.go @@ -2,6 +2,7 @@ package barrier import ( "context" + "errors" "path/filepath" "testing" @@ -78,7 +79,7 @@ func TestBarrierGetNotFound(t *testing.T) { b.Unseal(mek) _, err := b.Get(ctx, "nonexistent") - if err != ErrNotFound { + if !errors.Is(err, ErrNotFound) { t.Fatalf("expected ErrNotFound, got: %v", err) } } @@ -96,7 +97,7 @@ func TestBarrierDelete(t *testing.T) { t.Fatalf("Delete: %v", err) } _, err := b.Get(ctx, "test/delete-me") - if err != ErrNotFound { + if !errors.Is(err, ErrNotFound) { t.Fatalf("expected ErrNotFound after delete, got: %v", err) } } @@ -127,16 +128,16 @@ func TestBarrierSealedOperations(t *testing.T) { defer cleanup() ctx := context.Background() - if _, err := b.Get(ctx, "test"); err != ErrSealed { + if _, err := b.Get(ctx, "test"); !errors.Is(err, ErrSealed) { t.Fatalf("Get when sealed: expected ErrSealed, got: %v", err) } - if err := b.Put(ctx, "test", []byte("data")); err != ErrSealed { + if err := b.Put(ctx, "test", []byte("data")); !errors.Is(err, ErrSealed) { t.Fatalf("Put when sealed: expected ErrSealed, got: %v", err) } - if err := b.Delete(ctx, "test"); err != ErrSealed { + if err := b.Delete(ctx, "test"); !errors.Is(err, ErrSealed) { t.Fatalf("Delete when sealed: expected ErrSealed, got: %v", err) } - if _, err := b.List(ctx, "test"); err != ErrSealed { + if _, err := b.List(ctx, "test"); !errors.Is(err, ErrSealed) { t.Fatalf("List when sealed: expected ErrSealed, got: %v", err) } } diff --git a/internal/config/config.go b/internal/config/config.go index ba63b4a..10713d2 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -12,10 +12,10 @@ import ( type Config struct { Server ServerConfig `toml:"server"` Web WebConfig `toml:"web"` - Database DatabaseConfig `toml:"database"` MCIAS MCIASConfig `toml:"mcias"` - Seal SealConfig `toml:"seal"` + Database DatabaseConfig `toml:"database"` Log LogConfig `toml:"log"` + Seal SealConfig `toml:"seal"` } // ServerConfig holds HTTP/gRPC server settings. @@ -66,7 +66,7 @@ type LogConfig struct { // Load reads and parses a TOML config file. func Load(path string) (*Config, error) { - data, err := os.ReadFile(path) + data, err := os.ReadFile(path) //nolint:gosec if err != nil { return nil, fmt.Errorf("config: read file: %w", err) } diff --git a/internal/crypto/crypto_test.go b/internal/crypto/crypto_test.go index e3c4cb1..4a4f3c4 100644 --- a/internal/crypto/crypto_test.go +++ b/internal/crypto/crypto_test.go @@ -2,6 +2,7 @@ package crypto import ( "bytes" + "errors" "testing" ) @@ -60,7 +61,7 @@ func TestDecryptWrongKey(t *testing.T) { ciphertext, _ := Encrypt(key1, plaintext) _, err := Decrypt(key2, ciphertext) - if err != ErrDecryptionFailed { + if !errors.Is(err, ErrDecryptionFailed) { t.Fatalf("expected ErrDecryptionFailed, got: %v", err) } } @@ -68,7 +69,7 @@ func TestDecryptWrongKey(t *testing.T) { func TestDecryptInvalidCiphertext(t *testing.T) { key, _ := GenerateKey() _, err := Decrypt(key, []byte("short")) - if err != ErrInvalidCiphertext { + if !errors.Is(err, ErrInvalidCiphertext) { t.Fatalf("expected ErrInvalidCiphertext, got: %v", err) } } diff --git a/internal/db/db.go b/internal/db/db.go index eda3f6e..45858cb 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -14,11 +14,11 @@ import ( func Open(path string) (*sql.DB, error) { // Ensure the file has restrictive permissions if it doesn't exist yet. if _, err := os.Stat(path); os.IsNotExist(err) { - f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0600) + f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0600) //nolint:gosec if err != nil { return nil, fmt.Errorf("db: create file: %w", err) } - f.Close() + _ = f.Close() } db, err := sql.Open("sqlite", path) @@ -34,7 +34,7 @@ func Open(path string) (*sql.DB, error) { } for _, p := range pragmas { if _, err := db.Exec(p); err != nil { - db.Close() + _ = db.Close() return nil, fmt.Errorf("db: pragma %q: %w", p, err) } } diff --git a/internal/db/migrate.go b/internal/db/migrate.go index 04043c0..43347c2 100644 --- a/internal/db/migrate.go +++ b/internal/db/migrate.go @@ -55,11 +55,11 @@ func Migrate(db *sql.DB) error { return fmt.Errorf("db: begin migration %d: %w", version, err) } if _, err := tx.Exec(migrations[i]); err != nil { - tx.Rollback() + _ = tx.Rollback() return fmt.Errorf("db: migration %d: %w", version, err) } if _, err := tx.Exec("INSERT INTO schema_migrations (version) VALUES (?)", version); err != nil { - tx.Rollback() + _ = tx.Rollback() return fmt.Errorf("db: record migration %d: %w", version, err) } if err := tx.Commit(); err != nil { diff --git a/internal/engine/ca/ca.go b/internal/engine/ca/ca.go index 1bc627c..1fb33eb 100644 --- a/internal/engine/ca/ca.go +++ b/internal/engine/ca/ca.go @@ -42,13 +42,13 @@ type issuerState struct { // CAEngine implements the CA (PKI) engine. type CAEngine struct { - mu sync.RWMutex barrier barrier.Barrier - mountPath string + rootKey crypto.PrivateKey config *CAConfig rootCert *x509.Certificate - rootKey crypto.PrivateKey issuers map[string]*issuerState + mountPath string + mu sync.RWMutex } // NewCAEngine creates a new CA engine instance. @@ -788,13 +788,13 @@ func (e *CAEngine) handleIssue(ctx context.Context, req *engine.Request) (*engin return &engine.Response{ Data: map[string]interface{}{ - "serial": serial, - "cert_pem": string(leafCertPEM), - "key_pem": string(leafKeyPEM), - "chain_pem": string(chainPEM), - "cn": cn, - "sans": allSANs, - "issued_by": req.CallerInfo.Username, + "serial": serial, + "cert_pem": string(leafCertPEM), + "key_pem": string(leafKeyPEM), + "chain_pem": string(chainPEM), + "cn": cn, + "sans": allSANs, + "issued_by": req.CallerInfo.Username, "expires_at": leafCert.NotAfter, }, }, nil diff --git a/internal/engine/ca/ca_test.go b/internal/engine/ca/ca_test.go index 63f099e..4be551b 100644 --- a/internal/engine/ca/ca_test.go +++ b/internal/engine/ca/ca_test.go @@ -4,6 +4,7 @@ import ( "context" "crypto/x509" "encoding/pem" + "errors" "strings" "sync" "testing" @@ -15,8 +16,8 @@ import ( // memBarrier is an in-memory barrier for testing. type memBarrier struct { - mu sync.RWMutex data map[string][]byte + mu sync.RWMutex } func newMemBarrier() *memBarrier { @@ -82,7 +83,7 @@ func setupEngine(t *testing.T) (*CAEngine, *memBarrier) { ctx := context.Background() config := map[string]interface{}{ - "organization": "TestOrg", + "organization": "TestOrg", "key_algorithm": "ecdsa", "key_size": float64(256), "root_expiry": "87600h", @@ -133,7 +134,7 @@ func TestInitializeWithImportedRoot(t *testing.T) { ctx := context.Background() config := map[string]interface{}{ - "organization": "ImportOrg", + "organization": "ImportOrg", "root_cert_pem": string(rootPEM), "root_key_pem": string(srcKeyPEM), } @@ -272,7 +273,7 @@ func TestCreateIssuerRejectsNonAdmin(t *testing.T) { if err == nil { t.Fatal("expected error for non-admin create-issuer") } - if err != ErrForbidden { + if !errors.Is(err, ErrForbidden) { t.Errorf("expected ErrForbidden, got: %v", err) } } @@ -289,7 +290,7 @@ func TestCreateIssuerRejectsNilCallerInfo(t *testing.T) { } _, err := eng.HandleRequest(ctx, req) - if err != ErrUnauthorized { + if !errors.Is(err, ErrUnauthorized) { t.Errorf("expected ErrUnauthorized, got: %v", err) } } @@ -427,7 +428,7 @@ func TestIssueRejectsNilCallerInfo(t *testing.T) { "common_name": "test.example.com", }, }) - if err != ErrUnauthorized { + if !errors.Is(err, ErrUnauthorized) { t.Errorf("expected ErrUnauthorized, got: %v", err) } } @@ -746,7 +747,7 @@ func TestImportRootRequiresAdmin(t *testing.T) { "key_pem": "fake", }, }) - if err != ErrForbidden { + if !errors.Is(err, ErrForbidden) { t.Errorf("expected ErrForbidden, got: %v", err) } } @@ -798,7 +799,7 @@ func TestPublicMethods(t *testing.T) { // Test nonexistent issuer. _, err = eng.GetIssuerCertPEM("nonexistent") - if err != ErrIssuerNotFound { + if !errors.Is(err, ErrIssuerNotFound) { t.Errorf("expected ErrIssuerNotFound, got: %v", err) } } diff --git a/internal/engine/ca/types.go b/internal/engine/ca/types.go index 1b52ea0..c1ce9af 100644 --- a/internal/engine/ca/types.go +++ b/internal/engine/ca/types.go @@ -6,32 +6,32 @@ import "time" type CAConfig struct { Organization string `json:"organization"` Country string `json:"country,omitempty"` - KeyAlgorithm string `json:"key_algorithm"` // "ecdsa", "rsa", "ed25519" - KeySize int `json:"key_size"` // e.g. 384 for ECDSA, 4096 for RSA - RootExpiry string `json:"root_expiry"` // e.g. "87600h" (10 years) + KeyAlgorithm string `json:"key_algorithm"` + RootExpiry string `json:"root_expiry"` + KeySize int `json:"key_size"` } // IssuerConfig is per-issuer configuration stored in the barrier. type IssuerConfig struct { + CreatedAt time.Time `json:"created_at"` Name string `json:"name"` KeyAlgorithm string `json:"key_algorithm"` - KeySize int `json:"key_size"` - Expiry string `json:"expiry"` // issuer cert expiry, e.g. "26280h" (3 years) - MaxTTL string `json:"max_ttl"` // max leaf cert TTL, e.g. "8760h" (1 year) + Expiry string `json:"expiry"` + MaxTTL string `json:"max_ttl"` CreatedBy string `json:"created_by"` - CreatedAt time.Time `json:"created_at"` + KeySize int `json:"key_size"` } // CertRecord is metadata for an issued certificate, stored in the barrier. // The private key is NOT stored. type CertRecord struct { + IssuedAt time.Time `json:"issued_at"` + ExpiresAt time.Time `json:"expires_at"` Serial string `json:"serial"` Issuer string `json:"issuer"` CN string `json:"cn"` - SANs []string `json:"sans,omitempty"` Profile string `json:"profile"` CertPEM string `json:"cert_pem"` IssuedBy string `json:"issued_by"` - IssuedAt time.Time `json:"issued_at"` - ExpiresAt time.Time `json:"expires_at"` + SANs []string `json:"sans,omitempty"` } diff --git a/internal/engine/engine.go b/internal/engine/engine.go index 2b2821f..a059a78 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -39,10 +39,10 @@ type CallerInfo struct { // Request is a request to an engine. type Request struct { - Operation string - Path string Data map[string]interface{} CallerInfo *CallerInfo + Operation string + Path string } // Response is a response from an engine. @@ -69,19 +69,19 @@ type Factory func() Engine // Mount represents a mounted engine instance. type Mount struct { + Engine Engine `json:"-"` Name string `json:"name"` Type EngineType `json:"type"` MountPath string `json:"mount_path"` - Engine Engine `json:"-"` } // Registry manages mounted engine instances. type Registry struct { - mu sync.RWMutex + barrier barrier.Barrier mounts map[string]*Mount factories map[EngineType]Factory - barrier barrier.Barrier logger *slog.Logger + mu sync.RWMutex } // NewRegistry creates a new engine registry. diff --git a/internal/engine/engine_test.go b/internal/engine/engine_test.go index 30e5cde..be4fb86 100644 --- a/internal/engine/engine_test.go +++ b/internal/engine/engine_test.go @@ -2,6 +2,7 @@ package engine import ( "context" + "errors" "log/slog" "testing" @@ -31,10 +32,12 @@ func (m *mockEngine) HandleRequest(_ context.Context, _ *Request) (*Response, er type mockBarrier struct{} -func (m *mockBarrier) Unseal(_ []byte) error { return nil } -func (m *mockBarrier) Seal() error { return nil } -func (m *mockBarrier) IsSealed() bool { return false } -func (m *mockBarrier) Get(_ context.Context, _ string) ([]byte, error) { return nil, barrier.ErrNotFound } +func (m *mockBarrier) Unseal(_ []byte) error { return nil } +func (m *mockBarrier) Seal() error { return nil } +func (m *mockBarrier) IsSealed() bool { return false } +func (m *mockBarrier) Get(_ context.Context, _ string) ([]byte, error) { + return nil, barrier.ErrNotFound +} func (m *mockBarrier) Put(_ context.Context, _ string, _ []byte) error { return nil } func (m *mockBarrier) Delete(_ context.Context, _ string) error { return nil } func (m *mockBarrier) List(_ context.Context, _ string) ([]string, error) { return nil, nil } @@ -59,7 +62,7 @@ func TestRegistryMountUnmount(t *testing.T) { } // Duplicate mount should fail. - if err := reg.Mount(ctx, "default", EngineTypeTransit, nil); err != ErrMountExists { + if err := reg.Mount(ctx, "default", EngineTypeTransit, nil); !errors.Is(err, ErrMountExists) { t.Fatalf("expected ErrMountExists, got: %v", err) } @@ -75,7 +78,7 @@ func TestRegistryMountUnmount(t *testing.T) { func TestRegistryUnmountNotFound(t *testing.T) { reg := NewRegistry(&mockBarrier{}, slog.Default()) - if err := reg.Unmount(context.Background(), "nonexistent"); err != ErrMountNotFound { + if err := reg.Unmount(context.Background(), "nonexistent"); !errors.Is(err, ErrMountNotFound) { t.Fatalf("expected ErrMountNotFound, got: %v", err) } } @@ -106,7 +109,7 @@ func TestRegistryHandleRequest(t *testing.T) { } _, err = reg.HandleRequest(ctx, "nonexistent", &Request{}) - if err != ErrMountNotFound { + if !errors.Is(err, ErrMountNotFound) { t.Fatalf("expected ErrMountNotFound, got: %v", err) } } diff --git a/internal/grpcserver/auth.go b/internal/grpcserver/auth.go index 5f2e687..9b34ca6 100644 --- a/internal/grpcserver/auth.go +++ b/internal/grpcserver/auth.go @@ -31,7 +31,7 @@ func (as *authServer) Logout(ctx context.Context, _ *pb.LogoutRequest) (*pb.Logo Token: token, }) if err == nil { - as.s.auth.Logout(client) + _ = as.s.auth.Logout(client) } return &pb.LogoutResponse{}, nil } @@ -53,4 +53,3 @@ func (as *authServer) TokenInfo(ctx context.Context, _ *pb.TokenInfoRequest) (*p IsAdmin: ti.IsAdmin, }, nil } - diff --git a/internal/grpcserver/policy.go b/internal/grpcserver/policy.go index 09581fc..6b0b75a 100644 --- a/internal/grpcserver/policy.go +++ b/internal/grpcserver/policy.go @@ -76,7 +76,7 @@ func pbToRule(r *pb.PolicyRule) *policy.Rule { func ruleToPB(r *policy.Rule) *pb.PolicyRule { return &pb.PolicyRule{ Id: r.ID, - Priority: int32(r.Priority), + Priority: int32(r.Priority), //nolint:gosec Effect: string(r.Effect), Usernames: r.Usernames, Roles: r.Roles, diff --git a/internal/grpcserver/server.go b/internal/grpcserver/server.go index 518b018..9ac1cad 100644 --- a/internal/grpcserver/server.go +++ b/internal/grpcserver/server.go @@ -22,16 +22,15 @@ import ( // GRPCServer wraps the gRPC server and all service implementations. type GRPCServer struct { - cfg *config.Config - sealMgr *seal.Manager - auth *auth.Authenticator - policy *policy.Engine - engines *engine.Registry - logger *slog.Logger - + cfg *config.Config + sealMgr *seal.Manager + auth *auth.Authenticator + policy *policy.Engine + engines *engine.Registry + logger *slog.Logger srv *grpc.Server - mu sync.Mutex acmeHandlers map[string]*internacme.Handler + mu sync.Mutex } // New creates a new GRPCServer. diff --git a/internal/grpcserver/system.go b/internal/grpcserver/system.go index 13c5c1b..282d0c7 100644 --- a/internal/grpcserver/system.go +++ b/internal/grpcserver/system.go @@ -2,6 +2,7 @@ package grpcserver import ( "context" + "errors" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -31,32 +32,28 @@ func (ss *systemServer) Init(ctx context.Context, req *pb.InitRequest) (*pb.Init Threads: ss.s.cfg.Seal.Argon2Threads, } if err := ss.s.sealMgr.Initialize(ctx, []byte(req.Password), params); err != nil { - switch err { - case seal.ErrAlreadyInitialized: + if errors.Is(err, seal.ErrAlreadyInitialized) { return nil, status.Error(codes.AlreadyExists, "already initialized") - default: - ss.s.logger.Error("grpc: init failed", "error", err) - return nil, status.Error(codes.Internal, "initialization failed") } + ss.s.logger.Error("grpc: init failed", "error", err) + return nil, status.Error(codes.Internal, "initialization failed") } return &pb.InitResponse{State: ss.s.sealMgr.State().String()}, nil } func (ss *systemServer) Unseal(ctx context.Context, req *pb.UnsealRequest) (*pb.UnsealResponse, error) { if err := ss.s.sealMgr.Unseal([]byte(req.Password)); err != nil { - switch err { - case seal.ErrNotInitialized: + if errors.Is(err, seal.ErrNotInitialized) { return nil, status.Error(codes.FailedPrecondition, "not initialized") - case seal.ErrInvalidPassword: + } else if errors.Is(err, seal.ErrInvalidPassword) { return nil, status.Error(codes.Unauthenticated, "invalid password") - case seal.ErrRateLimited: + } else if errors.Is(err, seal.ErrRateLimited) { return nil, status.Error(codes.ResourceExhausted, "too many attempts, try again later") - case seal.ErrNotSealed: + } else if errors.Is(err, seal.ErrNotSealed) { return nil, status.Error(codes.FailedPrecondition, "already unsealed") - default: - ss.s.logger.Error("grpc: unseal failed", "error", err) - return nil, status.Error(codes.Internal, "unseal failed") } + ss.s.logger.Error("grpc: unseal failed", "error", err) + return nil, status.Error(codes.Internal, "unseal failed") } if err := ss.s.engines.UnsealAll(ctx); err != nil { diff --git a/internal/policy/policy.go b/internal/policy/policy.go index e1421b0..4c09985 100644 --- a/internal/policy/policy.go +++ b/internal/policy/policy.go @@ -25,20 +25,20 @@ const ( // Rule is a policy rule stored in the barrier. type Rule struct { ID string `json:"id"` - Priority int `json:"priority"` Effect Effect `json:"effect"` - Usernames []string `json:"usernames,omitempty"` // match specific users - Roles []string `json:"roles,omitempty"` // match roles - Resources []string `json:"resources,omitempty"` // glob patterns for engine mounts/paths - Actions []string `json:"actions,omitempty"` // e.g., "read", "write", "admin" + Usernames []string `json:"usernames,omitempty"` + Roles []string `json:"roles,omitempty"` + Resources []string `json:"resources,omitempty"` + Actions []string `json:"actions,omitempty"` + Priority int `json:"priority"` } // Request represents an authorization request. type Request struct { Username string + Resource string + Action string Roles []string - Resource string // e.g., "engine/transit/default/encrypt" - Action string // e.g., "write" } // Engine evaluates policy rules from the barrier. diff --git a/internal/seal/seal.go b/internal/seal/seal.go index 276e278..61a6617 100644 --- a/internal/seal/seal.go +++ b/internal/seal/seal.go @@ -50,18 +50,15 @@ var ( // Manager manages the seal/unseal lifecycle. type Manager struct { - db *sql.DB - barrier *barrier.AESGCMBarrier - logger *slog.Logger - - mu sync.RWMutex - state ServiceState - mek []byte // nil when sealed - - // Rate limiting for unseal attempts. - unsealAttempts int lastAttempt time.Time lockoutUntil time.Time + db *sql.DB + barrier *barrier.AESGCMBarrier + logger *slog.Logger + mek []byte + state ServiceState + unsealAttempts int + mu sync.RWMutex } // NewManager creates a new seal manager. @@ -205,10 +202,10 @@ func (m *Manager) Unseal(password []byte) error { // Read seal config. var ( - encryptedMEK []byte - salt []byte - argTime, argMem uint32 - argThreads uint8 + encryptedMEK []byte + salt []byte + argTime, argMem uint32 + argThreads uint8 ) err := m.db.QueryRow(` SELECT encrypted_mek, kdf_salt, argon2_time, argon2_memory, argon2_threads @@ -256,7 +253,7 @@ func (m *Manager) Seal() error { crypto.Zeroize(m.mek) m.mek = nil } - m.barrier.Seal() + _ = m.barrier.Seal() m.state = StateSealed m.logger.Debug("service sealed") return nil diff --git a/internal/seal/seal_test.go b/internal/seal/seal_test.go index 5d635b5..df5cfec 100644 --- a/internal/seal/seal_test.go +++ b/internal/seal/seal_test.go @@ -2,6 +2,7 @@ package seal import ( "context" + "errors" "log/slog" "path/filepath" "testing" @@ -75,7 +76,7 @@ func TestSealWrongPassword(t *testing.T) { mgr.Seal() err := mgr.Unseal([]byte("wrong")) - if err != ErrInvalidPassword { + if !errors.Is(err, ErrInvalidPassword) { t.Fatalf("expected ErrInvalidPassword, got: %v", err) } } @@ -89,7 +90,7 @@ func TestSealDoubleInitialize(t *testing.T) { mgr.Initialize(context.Background(), []byte("password"), params) err := mgr.Initialize(context.Background(), []byte("password"), params) - if err != ErrAlreadyInitialized { + if !errors.Is(err, ErrAlreadyInitialized) { t.Fatalf("expected ErrAlreadyInitialized, got: %v", err) } } @@ -121,13 +122,13 @@ func TestSealCheckInitializedPersists(t *testing.T) { func TestSealStateString(t *testing.T) { tests := []struct { - state ServiceState want string + state ServiceState }{ - {StateUninitialized, "uninitialized"}, - {StateSealed, "sealed"}, - {StateInitializing, "initializing"}, - {StateUnsealed, "unsealed"}, + {want: "uninitialized", state: StateUninitialized}, + {want: "sealed", state: StateSealed}, + {want: "initializing", state: StateInitializing}, + {want: "unsealed", state: StateUnsealed}, } for _, tt := range tests { if got := tt.state.String(); got != tt.want { diff --git a/internal/server/routes.go b/internal/server/routes.go index c62d0b6..50a17cb 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -71,7 +71,7 @@ func (s *Server) handleInit(w http.ResponseWriter, r *http.Request) { Threads: s.cfg.Seal.Argon2Threads, } if err := s.seal.Initialize(r.Context(), []byte(req.Password), params); err != nil { - if err == seal.ErrAlreadyInitialized { + if errors.Is(err, seal.ErrAlreadyInitialized) { http.Error(w, `{"error":"already initialized"}`, http.StatusConflict) return } @@ -95,16 +95,15 @@ func (s *Server) handleUnseal(w http.ResponseWriter, r *http.Request) { } if err := s.seal.Unseal([]byte(req.Password)); err != nil { - switch err { - case seal.ErrNotInitialized: + if errors.Is(err, seal.ErrNotInitialized) { http.Error(w, `{"error":"not initialized"}`, http.StatusPreconditionFailed) - case seal.ErrInvalidPassword: + } else if errors.Is(err, seal.ErrInvalidPassword) { http.Error(w, `{"error":"invalid password"}`, http.StatusUnauthorized) - case seal.ErrRateLimited: + } else if errors.Is(err, seal.ErrRateLimited) { http.Error(w, `{"error":"too many attempts, try again later"}`, http.StatusTooManyRequests) - case seal.ErrNotSealed: + } else if errors.Is(err, seal.ErrNotSealed) { http.Error(w, `{"error":"already unsealed"}`, http.StatusConflict) - default: + } else { s.logger.Error("unseal failed", "error", err) http.Error(w, `{"error":"unseal failed"}`, http.StatusInternalServerError) } @@ -174,7 +173,7 @@ func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) { Token: token, }) if err == nil { - s.auth.Logout(client) + _ = s.auth.Logout(client) } // Clear cookie. @@ -207,9 +206,9 @@ func (s *Server) handleEngineMounts(w http.ResponseWriter, r *http.Request) { func (s *Server) handleEngineMount(w http.ResponseWriter, r *http.Request) { var req struct { + Config map[string]interface{} `json:"config"` Name string `json:"name"` Type string `json:"type"` - Config map[string]interface{} `json:"config"` } if err := readJSON(r, &req); err != nil { http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest) @@ -245,10 +244,10 @@ func (s *Server) handleEngineUnmount(w http.ResponseWriter, r *http.Request) { func (s *Server) handleEngineRequest(w http.ResponseWriter, r *http.Request) { var req struct { + Data map[string]interface{} `json:"data"` Mount string `json:"mount"` Operation string `json:"operation"` Path string `json:"path"` - Data map[string]interface{} `json:"data"` } if err := readJSON(r, &req); err != nil { http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest) @@ -383,7 +382,7 @@ func (s *Server) handlePKIRoot(w http.ResponseWriter, r *http.Request) { } w.Header().Set("Content-Type", "application/x-pem-file") - w.Write(certPEM) + _, _ = w.Write(certPEM) //nolint:gosec } func (s *Server) handlePKIChain(w http.ResponseWriter, r *http.Request) { @@ -411,7 +410,7 @@ func (s *Server) handlePKIChain(w http.ResponseWriter, r *http.Request) { } w.Header().Set("Content-Type", "application/x-pem-file") - w.Write(chainPEM) + _, _ = w.Write(chainPEM) //nolint:gosec } func (s *Server) handlePKIIssuer(w http.ResponseWriter, r *http.Request) { @@ -435,7 +434,7 @@ func (s *Server) handlePKIIssuer(w http.ResponseWriter, r *http.Request) { } w.Header().Set("Content-Type", "application/x-pem-file") - w.Write(certPEM) + _, _ = w.Write(certPEM) //nolint:gosec } func (s *Server) getCAEngine(mountName string) (*ca.CAEngine, error) { @@ -456,11 +455,11 @@ func (s *Server) getCAEngine(mountName string) (*ca.CAEngine, error) { func writeJSON(w http.ResponseWriter, status int, v interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) - json.NewEncoder(w).Encode(v) + _ = json.NewEncoder(w).Encode(v) } func readJSON(r *http.Request, v interface{}) error { - defer r.Body.Close() + defer func() { _ = r.Body.Close() }() body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20)) // 1MB limit if err != nil { return err diff --git a/internal/server/server.go b/internal/server/server.go index 50669ad..6869f3c 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -23,18 +23,17 @@ import ( // Server is the Metacrypt HTTP server. type Server struct { - cfg *config.Config - seal *seal.Manager - auth *auth.Authenticator - policy *policy.Engine - engines *engine.Registry - httpSrv *http.Server - grpcSrv *grpc.Server - logger *slog.Logger - version string - - acmeMu sync.Mutex + cfg *config.Config + seal *seal.Manager + auth *auth.Authenticator + policy *policy.Engine + engines *engine.Registry + httpSrv *http.Server + grpcSrv *grpc.Server + logger *slog.Logger acmeHandlers map[string]*internacme.Handler + version string + acmeMu sync.Mutex } // New creates a new server. diff --git a/internal/webserver/client.go b/internal/webserver/client.go index c8b059a..b9fe574 100644 --- a/internal/webserver/client.go +++ b/internal/webserver/client.go @@ -27,7 +27,7 @@ type VaultClient struct { func NewVaultClient(addr, caCertPath string) (*VaultClient, error) { tlsCfg := &tls.Config{MinVersion: tls.VersionTLS12} if caCertPath != "" { - pemData, err := os.ReadFile(caCertPath) + pemData, err := os.ReadFile(caCertPath) //nolint:gosec if err != nil { return nil, fmt.Errorf("webserver: read CA cert: %w", err) } diff --git a/internal/webserver/routes.go b/internal/webserver/routes.go index ded53ed..44d5e24 100644 --- a/internal/webserver/routes.go +++ b/internal/webserver/routes.go @@ -81,6 +81,7 @@ func (ws *WebServer) handleInit(w http.ResponseWriter, r *http.Request) { } ws.renderTemplate(w, "init.html", nil) case http.MethodPost: + r.Body = http.MaxBytesReader(w, r.Body, 1<<20) r.ParseForm() password := r.FormValue("password") if password == "" { @@ -111,6 +112,7 @@ func (ws *WebServer) handleUnseal(w http.ResponseWriter, r *http.Request) { } ws.renderTemplate(w, "unseal.html", nil) case http.MethodPost: + r.Body = http.MaxBytesReader(w, r.Body, 1<<20) r.ParseForm() password := r.FormValue("password") if err := ws.vault.Unseal(r.Context(), password); err != nil { @@ -137,6 +139,7 @@ func (ws *WebServer) handleLogin(w http.ResponseWriter, r *http.Request) { case http.MethodGet: ws.renderTemplate(w, "login.html", nil) case http.MethodPost: + r.Body = http.MaxBytesReader(w, r.Body, 1<<20) r.ParseForm() token, err := ws.vault.Login(r.Context(), r.FormValue("username"), @@ -182,7 +185,9 @@ func (ws *WebServer) handleDashboardMountCA(w http.ResponseWriter, r *http.Reque return } + r.Body = http.MaxBytesReader(w, r.Body, 1<<20) if err := r.ParseMultipartForm(1 << 20); err != nil { + r.Body = http.MaxBytesReader(w, r.Body, 1<<20) r.ParseForm() } @@ -283,7 +288,9 @@ func (ws *WebServer) handleImportRoot(w http.ResponseWriter, r *http.Request) { return } + r.Body = http.MaxBytesReader(w, r.Body, 1<<20) if err := r.ParseMultipartForm(1 << 20); err != nil { + r.Body = http.MaxBytesReader(w, r.Body, 1<<20) r.ParseForm() } @@ -334,6 +341,7 @@ func (ws *WebServer) handleCreateIssuer(w http.ResponseWriter, r *http.Request) return } + r.Body = http.MaxBytesReader(w, r.Body, 1<<20) r.ParseForm() name := r.FormValue("name") if name == "" { diff --git a/internal/webserver/server.go b/internal/webserver/server.go index d4a298a..11c3fc8 100644 --- a/internal/webserver/server.go +++ b/internal/webserver/server.go @@ -14,8 +14,8 @@ import ( "github.com/go-chi/chi/v5" - webui "git.wntrmute.dev/kyle/metacrypt/web" "git.wntrmute.dev/kyle/metacrypt/internal/config" + webui "git.wntrmute.dev/kyle/metacrypt/web" ) // WebServer is the standalone web UI server.