Fix gosec, govet, and errorlint linter errors

Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
2026-03-15 10:04:12 -07:00
parent dd31e440e6
commit fbaf79a8a0
35 changed files with 236 additions and 232 deletions

View File

@@ -1 +1 @@
[{"lang":"en","usageCount":2}] [{"lang":"en","usageCount":4}]

View File

@@ -43,7 +43,7 @@ func runInit(cmd *cobra.Command, args []string) error {
if err != nil { if err != nil {
return err return err
} }
defer database.Close() defer func() { _ = database.Close() }()
if err := db.Migrate(database); err != nil { if err := db.Migrate(database); err != nil {
return err return err

View File

@@ -29,5 +29,5 @@ func initConfig() {
} }
viper.AutomaticEnv() viper.AutomaticEnv()
viper.SetEnvPrefix("METACRYPT") viper.SetEnvPrefix("METACRYPT")
viper.ReadInConfig() _ = viper.ReadInConfig()
} }

View File

@@ -50,7 +50,7 @@ func runServer(cmd *cobra.Command, args []string) error {
if err != nil { if err != nil {
return err return err
} }
defer database.Close() defer func() { _ = database.Close() }()
if err := db.Migrate(database); err != nil { if err := db.Migrate(database); err != nil {
return err return err

View File

@@ -40,7 +40,7 @@ func runSnapshot(cmd *cobra.Command, args []string) error {
if err != nil { if err != nil {
return err return err
} }
defer database.Close() defer func() { _ = database.Close() }()
if err := sqliteBackup(database, snapshotOutput); err != nil { if err := sqliteBackup(database, snapshotOutput); err != nil {
return err return err

View File

@@ -26,7 +26,7 @@ var (
func init() { func init() {
statusCmd.Flags().StringVar(&statusAddr, "addr", "", "server address (e.g., https://localhost:8443)") 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.Flags().StringVar(&statusCACert, "ca-cert", "", "path to CA certificate for TLS verification")
statusCmd.MarkFlagRequired("addr") _ = statusCmd.MarkFlagRequired("addr")
rootCmd.AddCommand(statusCmd) rootCmd.AddCommand(statusCmd)
} }
@@ -34,7 +34,7 @@ func runStatus(cmd *cobra.Command, args []string) error {
tlsCfg := &tls.Config{MinVersion: tls.VersionTLS12} tlsCfg := &tls.Config{MinVersion: tls.VersionTLS12}
if statusCACert != "" { if statusCACert != "" {
pem, err := os.ReadFile(statusCACert) pem, err := os.ReadFile(statusCACert) //nolint:gosec
if err != nil { if err != nil {
return fmt.Errorf("read CA cert: %w", err) return fmt.Errorf("read CA cert: %w", err)
} }
@@ -53,7 +53,7 @@ func runStatus(cmd *cobra.Command, args []string) error {
if err != nil { if err != nil {
return err return err
} }
defer resp.Body.Close() defer func() { _ = resp.Body.Close() }()
var status struct { var status struct {
State string `json:"state"` State string `json:"state"`

View File

@@ -19,12 +19,12 @@ import (
// directoryResponse is the ACME directory object (RFC 8555 §7.1.1). // directoryResponse is the ACME directory object (RFC 8555 §7.1.1).
type directoryResponse struct { type directoryResponse struct {
Meta *directoryMeta `json:"meta,omitempty"`
NewNonce string `json:"newNonce"` NewNonce string `json:"newNonce"`
NewAccount string `json:"newAccount"` NewAccount string `json:"newAccount"`
NewOrder string `json:"newOrder"` NewOrder string `json:"newOrder"`
RevokeCert string `json:"revokeCert"` RevokeCert string `json:"revokeCert"`
KeyChange string `json:"keyChange"` KeyChange string `json:"keyChange"`
Meta *directoryMeta `json:"meta,omitempty"`
} }
type directoryMeta struct { type directoryMeta struct {
@@ -49,7 +49,7 @@ func (h *Handler) handleDirectory(w http.ResponseWriter, r *http.Request) {
}, },
} }
w.Header().Set("Content-Type", "application/json") 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. // handleNewNonce serves HEAD and GET /acme/{mount}/new-nonce.
@@ -65,9 +65,9 @@ func (h *Handler) handleNewNonce(w http.ResponseWriter, r *http.Request) {
// newAccountPayload is the payload for the new-account request. // newAccountPayload is the payload for the new-account request.
type newAccountPayload struct { type newAccountPayload struct {
TermsOfServiceAgreed bool `json:"termsOfServiceAgreed"`
Contact []string `json:"contact,omitempty"` Contact []string `json:"contact,omitempty"`
ExternalAccountBinding json.RawMessage `json:"externalAccountBinding,omitempty"` ExternalAccountBinding json.RawMessage `json:"externalAccountBinding,omitempty"`
TermsOfServiceAgreed bool `json:"termsOfServiceAgreed"`
OnlyReturnExisting bool `json:"onlyReturnExisting"` OnlyReturnExisting bool `json:"onlyReturnExisting"`
} }
@@ -172,9 +172,9 @@ func (h *Handler) handleNewAccount(w http.ResponseWriter, r *http.Request) {
// newOrderPayload is the payload for the new-order request. // newOrderPayload is the payload for the new-order request.
type newOrderPayload struct { type newOrderPayload struct {
Identifiers []Identifier `json:"identifiers"`
NotBefore string `json:"notBefore,omitempty"` NotBefore string `json:"notBefore,omitempty"`
NotAfter string `json:"notAfter,omitempty"` NotAfter string `json:"notAfter,omitempty"`
Identifiers []Identifier `json:"identifiers"`
} }
// handleNewOrder handles POST /acme/{mount}/new-order. // 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)) h.writeJSON(w, http.StatusOK, h.challengeToWire(chall))
// Launch validation goroutine. // 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}. // 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.Status = StatusValid
order.CertID = certID order.CertID = certID
orderData, _ := json.Marshal(order) 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)) 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) h.addNonceHeader(w)
w.Header().Set("Content-Type", "application/pem-certificate-chain") w.Header().Set("Content-Type", "application/pem-certificate-chain")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
w.Write([]byte(cert.CertPEM)) _, _ = w.Write([]byte(cert.CertPEM))
} }
// handleRevokeCert handles POST /acme/{mount}/revoke-cert. // 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 { if issuedCert.SerialNumber.Cmp(targetCert.SerialNumber) == 0 {
cert.Revoked = true cert.Revoked = true
updated, _ := json.Marshal(cert) 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) h.addNonceHeader(w)
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
return return
@@ -859,7 +859,7 @@ func readBody(r *http.Request) ([]byte, error) {
if r.Body == nil { if r.Body == nil {
return nil, errors.New("empty body") return nil, errors.New("empty body")
} }
defer r.Body.Close() defer func() { _ = r.Body.Close() }()
buf := make([]byte, 0, 4096) buf := make([]byte, 0, 4096)
tmp := make([]byte, 512) tmp := make([]byte, 512)
for { for {

View File

@@ -27,18 +27,18 @@ type JWSHeader struct {
Alg string `json:"alg"` Alg string `json:"alg"`
Nonce string `json:"nonce"` Nonce string `json:"nonce"`
URL string `json:"url"` URL string `json:"url"`
JWK json.RawMessage `json:"jwk,omitempty"` // present for new-account / new-order with new key KID string `json:"kid,omitempty"`
KID string `json:"kid,omitempty"` // present for subsequent requests JWK json.RawMessage `json:"jwk,omitempty"`
} }
// ParsedJWS is the result of parsing a JWS request body. // ParsedJWS is the result of parsing a JWS request body.
// Signature verification is NOT performed here; call VerifyJWS separately. // Signature verification is NOT performed here; call VerifyJWS separately.
type ParsedJWS struct { type ParsedJWS struct {
Header JWSHeader 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 RawBody JWSFlat
Payload []byte
SigningInput []byte
RawSignature []byte
} }
// ParseJWS decodes the flattened JWS from body without verifying the signature. // ParseJWS decodes the flattened JWS from body without verifying the signature.

View File

@@ -13,9 +13,9 @@ const nonceLifetime = 10 * time.Minute
// NonceStore is a thread-safe single-use nonce store with expiry. // NonceStore is a thread-safe single-use nonce store with expiry.
// Nonces are short-lived per RFC 8555 §7.2. // Nonces are short-lived per RFC 8555 §7.2.
type NonceStore struct { type NonceStore struct {
mu sync.Mutex
nonces map[string]time.Time nonces map[string]time.Time
issued int issued int
mu sync.Mutex
} }
// NewNonceStore creates a new nonce store. // NewNonceStore creates a new nonce store.

View File

@@ -15,12 +15,12 @@ import (
// Handler implements the ACME protocol for a single CA mount. // Handler implements the ACME protocol for a single CA mount.
type Handler struct { type Handler struct {
mount string
barrier barrier.Barrier barrier barrier.Barrier
engines *engine.Registry engines *engine.Registry
nonces *NonceStore nonces *NonceStore
baseURL string
logger *slog.Logger logger *slog.Logger
mount string
baseURL string
} }
// NewHandler creates an ACME handler for the given CA mount. // NewHandler creates an ACME handler for the given CA mount.

View File

@@ -4,36 +4,36 @@ import "time"
// Account represents an ACME account (RFC 8555 §7.1.2). // Account represents an ACME account (RFC 8555 §7.1.2).
type Account struct { 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"` 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). // EABCredential is an External Account Binding credential (RFC 8555 §7.3.4).
type EABCredential struct { 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"` 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). // Order represents an ACME certificate order (RFC 8555 §7.1.3).
type Order struct { 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"` ExpiresAt time.Time `json:"expires_at"`
CreatedAt time.Time `json:"created_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. // Identifier is a domain name or IP address in an order.
@@ -44,23 +44,23 @@ type Identifier struct {
// Authorization represents an ACME authorization (RFC 8555 §7.1.4). // Authorization represents an ACME authorization (RFC 8555 §7.1.4).
type Authorization struct { type Authorization struct {
ExpiresAt time.Time `json:"expires_at"`
Identifier Identifier `json:"identifier"`
ID string `json:"id"` ID string `json:"id"`
AccountID string `json:"account_id"` AccountID string `json:"account_id"`
Status string `json:"status"` // "pending","valid","invalid","expired","deactivated","revoked" Status string `json:"status"`
Identifier Identifier `json:"identifier"`
ChallengeIDs []string `json:"challenge_ids"` ChallengeIDs []string `json:"challenge_ids"`
ExpiresAt time.Time `json:"expires_at"`
} }
// Challenge represents an ACME challenge (RFC 8555 §8). // Challenge represents an ACME challenge (RFC 8555 §8).
type Challenge struct { 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"` Error *ProblemDetail `json:"error,omitempty"`
ValidatedAt *time.Time `json:"validated_at,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. // 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. // IssuedCert stores the PEM and metadata for a certificate issued via ACME.
type IssuedCert struct { type IssuedCert struct {
IssuedAt time.Time `json:"issued_at"`
ExpiresAt time.Time `json:"expires_at"`
ID string `json:"id"` ID string `json:"id"`
OrderID string `json:"order_id"` OrderID string `json:"order_id"`
AccountID string `json:"account_id"` AccountID string `json:"account_id"`
CertPEM string `json:"cert_pem"` // full chain PEM CertPEM string `json:"cert_pem"`
IssuedAt time.Time `json:"issued_at"`
ExpiresAt time.Time `json:"expires_at"`
Revoked bool `json:"revoked"` Revoked bool `json:"revoked"`
} }

View File

@@ -36,9 +36,8 @@ type cachedClaims struct {
type Authenticator struct { type Authenticator struct {
client *mcias.Client client *mcias.Client
logger *slog.Logger logger *slog.Logger
cache map[string]*cachedClaims
mu sync.RWMutex mu sync.RWMutex
cache map[string]*cachedClaims // keyed by SHA-256(token)
} }
// NewAuthenticator creates a new authenticator with the given MCIAS client. // NewAuthenticator creates a new authenticator with the given MCIAS client.

View File

@@ -39,8 +39,8 @@ type Barrier interface {
// AESGCMBarrier implements Barrier using AES-256-GCM encryption. // AESGCMBarrier implements Barrier using AES-256-GCM encryption.
type AESGCMBarrier struct { type AESGCMBarrier struct {
db *sql.DB db *sql.DB
mek []byte
mu sync.RWMutex mu sync.RWMutex
mek []byte // nil when sealed
} }
// NewAESGCMBarrier creates a new AES-GCM barrier backed by the given database. // 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 { if err != nil {
return nil, fmt.Errorf("barrier: list %q: %w", prefix, err) return nil, fmt.Errorf("barrier: list %q: %w", prefix, err)
} }
defer rows.Close() defer func() { _ = rows.Close() }()
var paths []string var paths []string
for rows.Next() { for rows.Next() {

View File

@@ -2,6 +2,7 @@ package barrier
import ( import (
"context" "context"
"errors"
"path/filepath" "path/filepath"
"testing" "testing"
@@ -78,7 +79,7 @@ func TestBarrierGetNotFound(t *testing.T) {
b.Unseal(mek) b.Unseal(mek)
_, err := b.Get(ctx, "nonexistent") _, err := b.Get(ctx, "nonexistent")
if err != ErrNotFound { if !errors.Is(err, ErrNotFound) {
t.Fatalf("expected ErrNotFound, got: %v", err) t.Fatalf("expected ErrNotFound, got: %v", err)
} }
} }
@@ -96,7 +97,7 @@ func TestBarrierDelete(t *testing.T) {
t.Fatalf("Delete: %v", err) t.Fatalf("Delete: %v", err)
} }
_, err := b.Get(ctx, "test/delete-me") _, err := b.Get(ctx, "test/delete-me")
if err != ErrNotFound { if !errors.Is(err, ErrNotFound) {
t.Fatalf("expected ErrNotFound after delete, got: %v", err) t.Fatalf("expected ErrNotFound after delete, got: %v", err)
} }
} }
@@ -127,16 +128,16 @@ func TestBarrierSealedOperations(t *testing.T) {
defer cleanup() defer cleanup()
ctx := context.Background() 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) 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) 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) 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) t.Fatalf("List when sealed: expected ErrSealed, got: %v", err)
} }
} }

View File

@@ -12,10 +12,10 @@ import (
type Config struct { type Config struct {
Server ServerConfig `toml:"server"` Server ServerConfig `toml:"server"`
Web WebConfig `toml:"web"` Web WebConfig `toml:"web"`
Database DatabaseConfig `toml:"database"`
MCIAS MCIASConfig `toml:"mcias"` MCIAS MCIASConfig `toml:"mcias"`
Seal SealConfig `toml:"seal"` Database DatabaseConfig `toml:"database"`
Log LogConfig `toml:"log"` Log LogConfig `toml:"log"`
Seal SealConfig `toml:"seal"`
} }
// ServerConfig holds HTTP/gRPC server settings. // ServerConfig holds HTTP/gRPC server settings.
@@ -66,7 +66,7 @@ type LogConfig struct {
// Load reads and parses a TOML config file. // Load reads and parses a TOML config file.
func Load(path string) (*Config, error) { func Load(path string) (*Config, error) {
data, err := os.ReadFile(path) data, err := os.ReadFile(path) //nolint:gosec
if err != nil { if err != nil {
return nil, fmt.Errorf("config: read file: %w", err) return nil, fmt.Errorf("config: read file: %w", err)
} }

View File

@@ -2,6 +2,7 @@ package crypto
import ( import (
"bytes" "bytes"
"errors"
"testing" "testing"
) )
@@ -60,7 +61,7 @@ func TestDecryptWrongKey(t *testing.T) {
ciphertext, _ := Encrypt(key1, plaintext) ciphertext, _ := Encrypt(key1, plaintext)
_, err := Decrypt(key2, ciphertext) _, err := Decrypt(key2, ciphertext)
if err != ErrDecryptionFailed { if !errors.Is(err, ErrDecryptionFailed) {
t.Fatalf("expected ErrDecryptionFailed, got: %v", err) t.Fatalf("expected ErrDecryptionFailed, got: %v", err)
} }
} }
@@ -68,7 +69,7 @@ func TestDecryptWrongKey(t *testing.T) {
func TestDecryptInvalidCiphertext(t *testing.T) { func TestDecryptInvalidCiphertext(t *testing.T) {
key, _ := GenerateKey() key, _ := GenerateKey()
_, err := Decrypt(key, []byte("short")) _, err := Decrypt(key, []byte("short"))
if err != ErrInvalidCiphertext { if !errors.Is(err, ErrInvalidCiphertext) {
t.Fatalf("expected ErrInvalidCiphertext, got: %v", err) t.Fatalf("expected ErrInvalidCiphertext, got: %v", err)
} }
} }

View File

@@ -14,11 +14,11 @@ import (
func Open(path string) (*sql.DB, error) { func Open(path string) (*sql.DB, error) {
// Ensure the file has restrictive permissions if it doesn't exist yet. // Ensure the file has restrictive permissions if it doesn't exist yet.
if _, err := os.Stat(path); os.IsNotExist(err) { 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 { if err != nil {
return nil, fmt.Errorf("db: create file: %w", err) return nil, fmt.Errorf("db: create file: %w", err)
} }
f.Close() _ = f.Close()
} }
db, err := sql.Open("sqlite", path) db, err := sql.Open("sqlite", path)
@@ -34,7 +34,7 @@ func Open(path string) (*sql.DB, error) {
} }
for _, p := range pragmas { for _, p := range pragmas {
if _, err := db.Exec(p); err != nil { if _, err := db.Exec(p); err != nil {
db.Close() _ = db.Close()
return nil, fmt.Errorf("db: pragma %q: %w", p, err) return nil, fmt.Errorf("db: pragma %q: %w", p, err)
} }
} }

View File

@@ -55,11 +55,11 @@ func Migrate(db *sql.DB) error {
return fmt.Errorf("db: begin migration %d: %w", version, err) return fmt.Errorf("db: begin migration %d: %w", version, err)
} }
if _, err := tx.Exec(migrations[i]); err != nil { if _, err := tx.Exec(migrations[i]); err != nil {
tx.Rollback() _ = tx.Rollback()
return fmt.Errorf("db: migration %d: %w", version, err) return fmt.Errorf("db: migration %d: %w", version, err)
} }
if _, err := tx.Exec("INSERT INTO schema_migrations (version) VALUES (?)", version); err != nil { 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) return fmt.Errorf("db: record migration %d: %w", version, err)
} }
if err := tx.Commit(); err != nil { if err := tx.Commit(); err != nil {

View File

@@ -42,13 +42,13 @@ type issuerState struct {
// CAEngine implements the CA (PKI) engine. // CAEngine implements the CA (PKI) engine.
type CAEngine struct { type CAEngine struct {
mu sync.RWMutex
barrier barrier.Barrier barrier barrier.Barrier
mountPath string rootKey crypto.PrivateKey
config *CAConfig config *CAConfig
rootCert *x509.Certificate rootCert *x509.Certificate
rootKey crypto.PrivateKey
issuers map[string]*issuerState issuers map[string]*issuerState
mountPath string
mu sync.RWMutex
} }
// NewCAEngine creates a new CA engine instance. // NewCAEngine creates a new CA engine instance.

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"crypto/x509" "crypto/x509"
"encoding/pem" "encoding/pem"
"errors"
"strings" "strings"
"sync" "sync"
"testing" "testing"
@@ -15,8 +16,8 @@ import (
// memBarrier is an in-memory barrier for testing. // memBarrier is an in-memory barrier for testing.
type memBarrier struct { type memBarrier struct {
mu sync.RWMutex
data map[string][]byte data map[string][]byte
mu sync.RWMutex
} }
func newMemBarrier() *memBarrier { func newMemBarrier() *memBarrier {
@@ -272,7 +273,7 @@ func TestCreateIssuerRejectsNonAdmin(t *testing.T) {
if err == nil { if err == nil {
t.Fatal("expected error for non-admin create-issuer") t.Fatal("expected error for non-admin create-issuer")
} }
if err != ErrForbidden { if !errors.Is(err, ErrForbidden) {
t.Errorf("expected ErrForbidden, got: %v", err) t.Errorf("expected ErrForbidden, got: %v", err)
} }
} }
@@ -289,7 +290,7 @@ func TestCreateIssuerRejectsNilCallerInfo(t *testing.T) {
} }
_, err := eng.HandleRequest(ctx, req) _, err := eng.HandleRequest(ctx, req)
if err != ErrUnauthorized { if !errors.Is(err, ErrUnauthorized) {
t.Errorf("expected ErrUnauthorized, got: %v", err) t.Errorf("expected ErrUnauthorized, got: %v", err)
} }
} }
@@ -427,7 +428,7 @@ func TestIssueRejectsNilCallerInfo(t *testing.T) {
"common_name": "test.example.com", "common_name": "test.example.com",
}, },
}) })
if err != ErrUnauthorized { if !errors.Is(err, ErrUnauthorized) {
t.Errorf("expected ErrUnauthorized, got: %v", err) t.Errorf("expected ErrUnauthorized, got: %v", err)
} }
} }
@@ -746,7 +747,7 @@ func TestImportRootRequiresAdmin(t *testing.T) {
"key_pem": "fake", "key_pem": "fake",
}, },
}) })
if err != ErrForbidden { if !errors.Is(err, ErrForbidden) {
t.Errorf("expected ErrForbidden, got: %v", err) t.Errorf("expected ErrForbidden, got: %v", err)
} }
} }
@@ -798,7 +799,7 @@ func TestPublicMethods(t *testing.T) {
// Test nonexistent issuer. // Test nonexistent issuer.
_, err = eng.GetIssuerCertPEM("nonexistent") _, err = eng.GetIssuerCertPEM("nonexistent")
if err != ErrIssuerNotFound { if !errors.Is(err, ErrIssuerNotFound) {
t.Errorf("expected ErrIssuerNotFound, got: %v", err) t.Errorf("expected ErrIssuerNotFound, got: %v", err)
} }
} }

View File

@@ -6,32 +6,32 @@ import "time"
type CAConfig struct { type CAConfig struct {
Organization string `json:"organization"` Organization string `json:"organization"`
Country string `json:"country,omitempty"` Country string `json:"country,omitempty"`
KeyAlgorithm string `json:"key_algorithm"` // "ecdsa", "rsa", "ed25519" KeyAlgorithm string `json:"key_algorithm"`
KeySize int `json:"key_size"` // e.g. 384 for ECDSA, 4096 for RSA RootExpiry string `json:"root_expiry"`
RootExpiry string `json:"root_expiry"` // e.g. "87600h" (10 years) KeySize int `json:"key_size"`
} }
// IssuerConfig is per-issuer configuration stored in the barrier. // IssuerConfig is per-issuer configuration stored in the barrier.
type IssuerConfig struct { type IssuerConfig struct {
CreatedAt time.Time `json:"created_at"`
Name string `json:"name"` Name string `json:"name"`
KeyAlgorithm string `json:"key_algorithm"` KeyAlgorithm string `json:"key_algorithm"`
KeySize int `json:"key_size"` Expiry string `json:"expiry"`
Expiry string `json:"expiry"` // issuer cert expiry, e.g. "26280h" (3 years) MaxTTL string `json:"max_ttl"`
MaxTTL string `json:"max_ttl"` // max leaf cert TTL, e.g. "8760h" (1 year)
CreatedBy string `json:"created_by"` 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. // CertRecord is metadata for an issued certificate, stored in the barrier.
// The private key is NOT stored. // The private key is NOT stored.
type CertRecord struct { type CertRecord struct {
IssuedAt time.Time `json:"issued_at"`
ExpiresAt time.Time `json:"expires_at"`
Serial string `json:"serial"` Serial string `json:"serial"`
Issuer string `json:"issuer"` Issuer string `json:"issuer"`
CN string `json:"cn"` CN string `json:"cn"`
SANs []string `json:"sans,omitempty"`
Profile string `json:"profile"` Profile string `json:"profile"`
CertPEM string `json:"cert_pem"` CertPEM string `json:"cert_pem"`
IssuedBy string `json:"issued_by"` IssuedBy string `json:"issued_by"`
IssuedAt time.Time `json:"issued_at"` SANs []string `json:"sans,omitempty"`
ExpiresAt time.Time `json:"expires_at"`
} }

View File

@@ -39,10 +39,10 @@ type CallerInfo struct {
// Request is a request to an engine. // Request is a request to an engine.
type Request struct { type Request struct {
Operation string
Path string
Data map[string]interface{} Data map[string]interface{}
CallerInfo *CallerInfo CallerInfo *CallerInfo
Operation string
Path string
} }
// Response is a response from an engine. // Response is a response from an engine.
@@ -69,19 +69,19 @@ type Factory func() Engine
// Mount represents a mounted engine instance. // Mount represents a mounted engine instance.
type Mount struct { type Mount struct {
Engine Engine `json:"-"`
Name string `json:"name"` Name string `json:"name"`
Type EngineType `json:"type"` Type EngineType `json:"type"`
MountPath string `json:"mount_path"` MountPath string `json:"mount_path"`
Engine Engine `json:"-"`
} }
// Registry manages mounted engine instances. // Registry manages mounted engine instances.
type Registry struct { type Registry struct {
mu sync.RWMutex barrier barrier.Barrier
mounts map[string]*Mount mounts map[string]*Mount
factories map[EngineType]Factory factories map[EngineType]Factory
barrier barrier.Barrier
logger *slog.Logger logger *slog.Logger
mu sync.RWMutex
} }
// NewRegistry creates a new engine registry. // NewRegistry creates a new engine registry.

View File

@@ -2,6 +2,7 @@ package engine
import ( import (
"context" "context"
"errors"
"log/slog" "log/slog"
"testing" "testing"
@@ -34,7 +35,9 @@ type mockBarrier struct{}
func (m *mockBarrier) Unseal(_ []byte) error { return nil } func (m *mockBarrier) Unseal(_ []byte) error { return nil }
func (m *mockBarrier) Seal() error { return nil } func (m *mockBarrier) Seal() error { return nil }
func (m *mockBarrier) IsSealed() bool { return false } func (m *mockBarrier) IsSealed() bool { return false }
func (m *mockBarrier) Get(_ context.Context, _ string) ([]byte, error) { return nil, barrier.ErrNotFound } 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) Put(_ context.Context, _ string, _ []byte) error { return nil }
func (m *mockBarrier) Delete(_ context.Context, _ string) 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 } 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. // 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) t.Fatalf("expected ErrMountExists, got: %v", err)
} }
@@ -75,7 +78,7 @@ func TestRegistryMountUnmount(t *testing.T) {
func TestRegistryUnmountNotFound(t *testing.T) { func TestRegistryUnmountNotFound(t *testing.T) {
reg := NewRegistry(&mockBarrier{}, slog.Default()) 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) t.Fatalf("expected ErrMountNotFound, got: %v", err)
} }
} }
@@ -106,7 +109,7 @@ func TestRegistryHandleRequest(t *testing.T) {
} }
_, err = reg.HandleRequest(ctx, "nonexistent", &Request{}) _, err = reg.HandleRequest(ctx, "nonexistent", &Request{})
if err != ErrMountNotFound { if !errors.Is(err, ErrMountNotFound) {
t.Fatalf("expected ErrMountNotFound, got: %v", err) t.Fatalf("expected ErrMountNotFound, got: %v", err)
} }
} }

View File

@@ -31,7 +31,7 @@ func (as *authServer) Logout(ctx context.Context, _ *pb.LogoutRequest) (*pb.Logo
Token: token, Token: token,
}) })
if err == nil { if err == nil {
as.s.auth.Logout(client) _ = as.s.auth.Logout(client)
} }
return &pb.LogoutResponse{}, nil return &pb.LogoutResponse{}, nil
} }
@@ -53,4 +53,3 @@ func (as *authServer) TokenInfo(ctx context.Context, _ *pb.TokenInfoRequest) (*p
IsAdmin: ti.IsAdmin, IsAdmin: ti.IsAdmin,
}, nil }, nil
} }

View File

@@ -76,7 +76,7 @@ func pbToRule(r *pb.PolicyRule) *policy.Rule {
func ruleToPB(r *policy.Rule) *pb.PolicyRule { func ruleToPB(r *policy.Rule) *pb.PolicyRule {
return &pb.PolicyRule{ return &pb.PolicyRule{
Id: r.ID, Id: r.ID,
Priority: int32(r.Priority), Priority: int32(r.Priority), //nolint:gosec
Effect: string(r.Effect), Effect: string(r.Effect),
Usernames: r.Usernames, Usernames: r.Usernames,
Roles: r.Roles, Roles: r.Roles,

View File

@@ -28,10 +28,9 @@ type GRPCServer struct {
policy *policy.Engine policy *policy.Engine
engines *engine.Registry engines *engine.Registry
logger *slog.Logger logger *slog.Logger
srv *grpc.Server srv *grpc.Server
mu sync.Mutex
acmeHandlers map[string]*internacme.Handler acmeHandlers map[string]*internacme.Handler
mu sync.Mutex
} }
// New creates a new GRPCServer. // New creates a new GRPCServer.

View File

@@ -2,6 +2,7 @@ package grpcserver
import ( import (
"context" "context"
"errors"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
@@ -31,33 +32,29 @@ func (ss *systemServer) Init(ctx context.Context, req *pb.InitRequest) (*pb.Init
Threads: ss.s.cfg.Seal.Argon2Threads, Threads: ss.s.cfg.Seal.Argon2Threads,
} }
if err := ss.s.sealMgr.Initialize(ctx, []byte(req.Password), params); err != nil { if err := ss.s.sealMgr.Initialize(ctx, []byte(req.Password), params); err != nil {
switch err { if errors.Is(err, seal.ErrAlreadyInitialized) {
case seal.ErrAlreadyInitialized:
return nil, status.Error(codes.AlreadyExists, "already initialized") return nil, status.Error(codes.AlreadyExists, "already initialized")
default: }
ss.s.logger.Error("grpc: init failed", "error", err) ss.s.logger.Error("grpc: init failed", "error", err)
return nil, status.Error(codes.Internal, "initialization failed") return nil, status.Error(codes.Internal, "initialization failed")
} }
}
return &pb.InitResponse{State: ss.s.sealMgr.State().String()}, nil return &pb.InitResponse{State: ss.s.sealMgr.State().String()}, nil
} }
func (ss *systemServer) Unseal(ctx context.Context, req *pb.UnsealRequest) (*pb.UnsealResponse, error) { func (ss *systemServer) Unseal(ctx context.Context, req *pb.UnsealRequest) (*pb.UnsealResponse, error) {
if err := ss.s.sealMgr.Unseal([]byte(req.Password)); err != nil { if err := ss.s.sealMgr.Unseal([]byte(req.Password)); err != nil {
switch err { if errors.Is(err, seal.ErrNotInitialized) {
case seal.ErrNotInitialized:
return nil, status.Error(codes.FailedPrecondition, "not initialized") 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") 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") 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") return nil, status.Error(codes.FailedPrecondition, "already unsealed")
default: }
ss.s.logger.Error("grpc: unseal failed", "error", err) ss.s.logger.Error("grpc: unseal failed", "error", err)
return nil, status.Error(codes.Internal, "unseal failed") return nil, status.Error(codes.Internal, "unseal failed")
} }
}
if err := ss.s.engines.UnsealAll(ctx); err != nil { if err := ss.s.engines.UnsealAll(ctx); err != nil {
ss.s.logger.Error("grpc: engine unseal failed", "error", err) ss.s.logger.Error("grpc: engine unseal failed", "error", err)

View File

@@ -25,20 +25,20 @@ const (
// Rule is a policy rule stored in the barrier. // Rule is a policy rule stored in the barrier.
type Rule struct { type Rule struct {
ID string `json:"id"` ID string `json:"id"`
Priority int `json:"priority"`
Effect Effect `json:"effect"` Effect Effect `json:"effect"`
Usernames []string `json:"usernames,omitempty"` // match specific users Usernames []string `json:"usernames,omitempty"`
Roles []string `json:"roles,omitempty"` // match roles Roles []string `json:"roles,omitempty"`
Resources []string `json:"resources,omitempty"` // glob patterns for engine mounts/paths Resources []string `json:"resources,omitempty"`
Actions []string `json:"actions,omitempty"` // e.g., "read", "write", "admin" Actions []string `json:"actions,omitempty"`
Priority int `json:"priority"`
} }
// Request represents an authorization request. // Request represents an authorization request.
type Request struct { type Request struct {
Username string Username string
Resource string
Action string
Roles []string Roles []string
Resource string // e.g., "engine/transit/default/encrypt"
Action string // e.g., "write"
} }
// Engine evaluates policy rules from the barrier. // Engine evaluates policy rules from the barrier.

View File

@@ -50,18 +50,15 @@ var (
// Manager manages the seal/unseal lifecycle. // Manager manages the seal/unseal lifecycle.
type Manager struct { type Manager struct {
lastAttempt time.Time
lockoutUntil time.Time
db *sql.DB db *sql.DB
barrier *barrier.AESGCMBarrier barrier *barrier.AESGCMBarrier
logger *slog.Logger logger *slog.Logger
mek []byte
mu sync.RWMutex
state ServiceState state ServiceState
mek []byte // nil when sealed
// Rate limiting for unseal attempts.
unsealAttempts int unsealAttempts int
lastAttempt time.Time mu sync.RWMutex
lockoutUntil time.Time
} }
// NewManager creates a new seal manager. // NewManager creates a new seal manager.
@@ -256,7 +253,7 @@ func (m *Manager) Seal() error {
crypto.Zeroize(m.mek) crypto.Zeroize(m.mek)
m.mek = nil m.mek = nil
} }
m.barrier.Seal() _ = m.barrier.Seal()
m.state = StateSealed m.state = StateSealed
m.logger.Debug("service sealed") m.logger.Debug("service sealed")
return nil return nil

View File

@@ -2,6 +2,7 @@ package seal
import ( import (
"context" "context"
"errors"
"log/slog" "log/slog"
"path/filepath" "path/filepath"
"testing" "testing"
@@ -75,7 +76,7 @@ func TestSealWrongPassword(t *testing.T) {
mgr.Seal() mgr.Seal()
err := mgr.Unseal([]byte("wrong")) err := mgr.Unseal([]byte("wrong"))
if err != ErrInvalidPassword { if !errors.Is(err, ErrInvalidPassword) {
t.Fatalf("expected ErrInvalidPassword, got: %v", err) t.Fatalf("expected ErrInvalidPassword, got: %v", err)
} }
} }
@@ -89,7 +90,7 @@ func TestSealDoubleInitialize(t *testing.T) {
mgr.Initialize(context.Background(), []byte("password"), params) mgr.Initialize(context.Background(), []byte("password"), params)
err := 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) t.Fatalf("expected ErrAlreadyInitialized, got: %v", err)
} }
} }
@@ -121,13 +122,13 @@ func TestSealCheckInitializedPersists(t *testing.T) {
func TestSealStateString(t *testing.T) { func TestSealStateString(t *testing.T) {
tests := []struct { tests := []struct {
state ServiceState
want string want string
state ServiceState
}{ }{
{StateUninitialized, "uninitialized"}, {want: "uninitialized", state: StateUninitialized},
{StateSealed, "sealed"}, {want: "sealed", state: StateSealed},
{StateInitializing, "initializing"}, {want: "initializing", state: StateInitializing},
{StateUnsealed, "unsealed"}, {want: "unsealed", state: StateUnsealed},
} }
for _, tt := range tests { for _, tt := range tests {
if got := tt.state.String(); got != tt.want { if got := tt.state.String(); got != tt.want {

View File

@@ -71,7 +71,7 @@ func (s *Server) handleInit(w http.ResponseWriter, r *http.Request) {
Threads: s.cfg.Seal.Argon2Threads, Threads: s.cfg.Seal.Argon2Threads,
} }
if err := s.seal.Initialize(r.Context(), []byte(req.Password), params); err != nil { 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) http.Error(w, `{"error":"already initialized"}`, http.StatusConflict)
return 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 { if err := s.seal.Unseal([]byte(req.Password)); err != nil {
switch err { if errors.Is(err, seal.ErrNotInitialized) {
case seal.ErrNotInitialized:
http.Error(w, `{"error":"not initialized"}`, http.StatusPreconditionFailed) 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) 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) 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) http.Error(w, `{"error":"already unsealed"}`, http.StatusConflict)
default: } else {
s.logger.Error("unseal failed", "error", err) s.logger.Error("unseal failed", "error", err)
http.Error(w, `{"error":"unseal failed"}`, http.StatusInternalServerError) http.Error(w, `{"error":"unseal failed"}`, http.StatusInternalServerError)
} }
@@ -174,7 +173,7 @@ func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) {
Token: token, Token: token,
}) })
if err == nil { if err == nil {
s.auth.Logout(client) _ = s.auth.Logout(client)
} }
// Clear cookie. // 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) { func (s *Server) handleEngineMount(w http.ResponseWriter, r *http.Request) {
var req struct { var req struct {
Config map[string]interface{} `json:"config"`
Name string `json:"name"` Name string `json:"name"`
Type string `json:"type"` Type string `json:"type"`
Config map[string]interface{} `json:"config"`
} }
if err := readJSON(r, &req); err != nil { if err := readJSON(r, &req); err != nil {
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest) 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) { func (s *Server) handleEngineRequest(w http.ResponseWriter, r *http.Request) {
var req struct { var req struct {
Data map[string]interface{} `json:"data"`
Mount string `json:"mount"` Mount string `json:"mount"`
Operation string `json:"operation"` Operation string `json:"operation"`
Path string `json:"path"` Path string `json:"path"`
Data map[string]interface{} `json:"data"`
} }
if err := readJSON(r, &req); err != nil { if err := readJSON(r, &req); err != nil {
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest) 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.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) { 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.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) { 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.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) { 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{}) { func writeJSON(w http.ResponseWriter, status int, v interface{}) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status) w.WriteHeader(status)
json.NewEncoder(w).Encode(v) _ = json.NewEncoder(w).Encode(v)
} }
func readJSON(r *http.Request, v interface{}) error { 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 body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20)) // 1MB limit
if err != nil { if err != nil {
return err return err

View File

@@ -31,10 +31,9 @@ type Server struct {
httpSrv *http.Server httpSrv *http.Server
grpcSrv *grpc.Server grpcSrv *grpc.Server
logger *slog.Logger logger *slog.Logger
version string
acmeMu sync.Mutex
acmeHandlers map[string]*internacme.Handler acmeHandlers map[string]*internacme.Handler
version string
acmeMu sync.Mutex
} }
// New creates a new server. // New creates a new server.

View File

@@ -27,7 +27,7 @@ type VaultClient struct {
func NewVaultClient(addr, caCertPath string) (*VaultClient, error) { func NewVaultClient(addr, caCertPath string) (*VaultClient, error) {
tlsCfg := &tls.Config{MinVersion: tls.VersionTLS12} tlsCfg := &tls.Config{MinVersion: tls.VersionTLS12}
if caCertPath != "" { if caCertPath != "" {
pemData, err := os.ReadFile(caCertPath) pemData, err := os.ReadFile(caCertPath) //nolint:gosec
if err != nil { if err != nil {
return nil, fmt.Errorf("webserver: read CA cert: %w", err) return nil, fmt.Errorf("webserver: read CA cert: %w", err)
} }

View File

@@ -81,6 +81,7 @@ func (ws *WebServer) handleInit(w http.ResponseWriter, r *http.Request) {
} }
ws.renderTemplate(w, "init.html", nil) ws.renderTemplate(w, "init.html", nil)
case http.MethodPost: case http.MethodPost:
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
r.ParseForm() r.ParseForm()
password := r.FormValue("password") password := r.FormValue("password")
if password == "" { if password == "" {
@@ -111,6 +112,7 @@ func (ws *WebServer) handleUnseal(w http.ResponseWriter, r *http.Request) {
} }
ws.renderTemplate(w, "unseal.html", nil) ws.renderTemplate(w, "unseal.html", nil)
case http.MethodPost: case http.MethodPost:
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
r.ParseForm() r.ParseForm()
password := r.FormValue("password") password := r.FormValue("password")
if err := ws.vault.Unseal(r.Context(), password); err != nil { 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: case http.MethodGet:
ws.renderTemplate(w, "login.html", nil) ws.renderTemplate(w, "login.html", nil)
case http.MethodPost: case http.MethodPost:
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
r.ParseForm() r.ParseForm()
token, err := ws.vault.Login(r.Context(), token, err := ws.vault.Login(r.Context(),
r.FormValue("username"), r.FormValue("username"),
@@ -182,7 +185,9 @@ func (ws *WebServer) handleDashboardMountCA(w http.ResponseWriter, r *http.Reque
return return
} }
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
if err := r.ParseMultipartForm(1 << 20); err != nil { if err := r.ParseMultipartForm(1 << 20); err != nil {
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
r.ParseForm() r.ParseForm()
} }
@@ -283,7 +288,9 @@ func (ws *WebServer) handleImportRoot(w http.ResponseWriter, r *http.Request) {
return return
} }
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
if err := r.ParseMultipartForm(1 << 20); err != nil { if err := r.ParseMultipartForm(1 << 20); err != nil {
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
r.ParseForm() r.ParseForm()
} }
@@ -334,6 +341,7 @@ func (ws *WebServer) handleCreateIssuer(w http.ResponseWriter, r *http.Request)
return return
} }
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
r.ParseForm() r.ParseForm()
name := r.FormValue("name") name := r.FormValue("name")
if name == "" { if name == "" {

View File

@@ -14,8 +14,8 @@ import (
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
webui "git.wntrmute.dev/kyle/metacrypt/web"
"git.wntrmute.dev/kyle/metacrypt/internal/config" "git.wntrmute.dev/kyle/metacrypt/internal/config"
webui "git.wntrmute.dev/kyle/metacrypt/web"
) )
// WebServer is the standalone web UI server. // WebServer is the standalone web UI server.