Fix gosec, govet, and errorlint linter errors
Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
@@ -1 +1 @@
|
|||||||
[{"lang":"en","usageCount":2}]
|
[{"lang":"en","usageCount":4}]
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -29,5 +29,5 @@ func initConfig() {
|
|||||||
}
|
}
|
||||||
viper.AutomaticEnv()
|
viper.AutomaticEnv()
|
||||||
viper.SetEnvPrefix("METACRYPT")
|
viper.SetEnvPrefix("METACRYPT")
|
||||||
viper.ReadInConfig()
|
_ = viper.ReadInConfig()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
|||||||
@@ -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 {
|
||||||
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"`
|
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 {
|
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,10 +65,10 @@ 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"`
|
||||||
OnlyReturnExisting bool `json:"onlyReturnExisting"`
|
TermsOfServiceAgreed bool `json:"termsOfServiceAgreed"`
|
||||||
|
OnlyReturnExisting bool `json:"onlyReturnExisting"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleNewAccount handles POST /acme/{mount}/new-account.
|
// 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.
|
// 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
|
||||||
@@ -726,11 +726,11 @@ func (h *Handler) orderToWire(order *Order) map[string]interface{} {
|
|||||||
authzURLs[i] = h.authzURL(id)
|
authzURLs[i] = h.authzURL(id)
|
||||||
}
|
}
|
||||||
m := map[string]interface{}{
|
m := map[string]interface{}{
|
||||||
"status": order.Status,
|
"status": order.Status,
|
||||||
"expires": order.ExpiresAt.Format(time.RFC3339),
|
"expires": order.ExpiresAt.Format(time.RFC3339),
|
||||||
"identifiers": order.Identifiers,
|
"identifiers": order.Identifiers,
|
||||||
"authorizations": authzURLs,
|
"authorizations": authzURLs,
|
||||||
"finalize": h.finalizeURL(order.ID),
|
"finalize": h.finalizeURL(order.ID),
|
||||||
}
|
}
|
||||||
if order.CertID != "" {
|
if order.CertID != "" {
|
||||||
m["certificate"] = h.certURL(order.CertID)
|
m["certificate"] = h.certURL(order.CertID)
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
RawBody JWSFlat
|
||||||
SigningInput []byte // protected + "." + payload (ASCII; used for signature verification)
|
Payload []byte
|
||||||
RawSignature []byte // decoded signature bytes
|
SigningInput []byte
|
||||||
RawBody JWSFlat
|
RawSignature []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseJWS decodes the flattened JWS from body without verifying the signature.
|
// ParseJWS decodes the flattened JWS from body without verifying the signature.
|
||||||
@@ -77,7 +77,7 @@ func ParseJWS(body []byte) (*ParsedJWS, error) {
|
|||||||
return &ParsedJWS{
|
return &ParsedJWS{
|
||||||
Header: header,
|
Header: header,
|
||||||
Payload: payload,
|
Payload: payload,
|
||||||
SigningInput: signingInput,
|
SigningInput: signingInput,
|
||||||
RawSignature: sig,
|
RawSignature: sig,
|
||||||
RawBody: flat,
|
RawBody: flat,
|
||||||
}, nil
|
}, nil
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -4,63 +4,63 @@ 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.
|
||||||
type Identifier struct {
|
type Identifier struct {
|
||||||
Type string `json:"type"` // "dns" or "ip"
|
Type string `json:"type"` // "dns" or "ip"
|
||||||
Value string `json:"value"`
|
Value string `json:"value"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,27 +104,27 @@ const (
|
|||||||
|
|
||||||
// ACME problem type URIs (RFC 8555 §6.7).
|
// ACME problem type URIs (RFC 8555 §6.7).
|
||||||
const (
|
const (
|
||||||
ProblemAccountDoesNotExist = "urn:ietf:params:acme:error:accountDoesNotExist"
|
ProblemAccountDoesNotExist = "urn:ietf:params:acme:error:accountDoesNotExist"
|
||||||
ProblemAlreadyRevoked = "urn:ietf:params:acme:error:alreadyRevoked"
|
ProblemAlreadyRevoked = "urn:ietf:params:acme:error:alreadyRevoked"
|
||||||
ProblemBadCSR = "urn:ietf:params:acme:error:badCSR"
|
ProblemBadCSR = "urn:ietf:params:acme:error:badCSR"
|
||||||
ProblemBadNonce = "urn:ietf:params:acme:error:badNonce"
|
ProblemBadNonce = "urn:ietf:params:acme:error:badNonce"
|
||||||
ProblemBadPublicKey = "urn:ietf:params:acme:error:badPublicKey"
|
ProblemBadPublicKey = "urn:ietf:params:acme:error:badPublicKey"
|
||||||
ProblemBadRevocationReason = "urn:ietf:params:acme:error:badRevocationReason"
|
ProblemBadRevocationReason = "urn:ietf:params:acme:error:badRevocationReason"
|
||||||
ProblemBadSignatureAlg = "urn:ietf:params:acme:error:badSignatureAlgorithm"
|
ProblemBadSignatureAlg = "urn:ietf:params:acme:error:badSignatureAlgorithm"
|
||||||
ProblemCAA = "urn:ietf:params:acme:error:caa"
|
ProblemCAA = "urn:ietf:params:acme:error:caa"
|
||||||
ProblemConnection = "urn:ietf:params:acme:error:connection"
|
ProblemConnection = "urn:ietf:params:acme:error:connection"
|
||||||
ProblemDNS = "urn:ietf:params:acme:error:dns"
|
ProblemDNS = "urn:ietf:params:acme:error:dns"
|
||||||
ProblemExternalAccountRequired = "urn:ietf:params:acme:error:externalAccountRequired"
|
ProblemExternalAccountRequired = "urn:ietf:params:acme:error:externalAccountRequired"
|
||||||
ProblemIncorrectResponse = "urn:ietf:params:acme:error:incorrectResponse"
|
ProblemIncorrectResponse = "urn:ietf:params:acme:error:incorrectResponse"
|
||||||
ProblemInvalidContact = "urn:ietf:params:acme:error:invalidContact"
|
ProblemInvalidContact = "urn:ietf:params:acme:error:invalidContact"
|
||||||
ProblemMalformed = "urn:ietf:params:acme:error:malformed"
|
ProblemMalformed = "urn:ietf:params:acme:error:malformed"
|
||||||
ProblemOrderNotReady = "urn:ietf:params:acme:error:orderNotReady"
|
ProblemOrderNotReady = "urn:ietf:params:acme:error:orderNotReady"
|
||||||
ProblemRateLimited = "urn:ietf:params:acme:error:rateLimited"
|
ProblemRateLimited = "urn:ietf:params:acme:error:rateLimited"
|
||||||
ProblemRejectedIdentifier = "urn:ietf:params:acme:error:rejectedIdentifier"
|
ProblemRejectedIdentifier = "urn:ietf:params:acme:error:rejectedIdentifier"
|
||||||
ProblemServerInternal = "urn:ietf:params:acme:error:serverInternal"
|
ProblemServerInternal = "urn:ietf:params:acme:error:serverInternal"
|
||||||
ProblemTLS = "urn:ietf:params:acme:error:tls"
|
ProblemTLS = "urn:ietf:params:acme:error:tls"
|
||||||
ProblemUnauthorized = "urn:ietf:params:acme:error:unauthorized"
|
ProblemUnauthorized = "urn:ietf:params:acme:error:unauthorized"
|
||||||
ProblemUnsupportedContact = "urn:ietf:params:acme:error:unsupportedContact"
|
ProblemUnsupportedContact = "urn:ietf:params:acme:error:unsupportedContact"
|
||||||
ProblemUnsupportedIdentifier = "urn:ietf:params:acme:error:unsupportedIdentifier"
|
ProblemUnsupportedIdentifier = "urn:ietf:params:acme:error:unsupportedIdentifier"
|
||||||
ProblemUserActionRequired = "urn:ietf:params:acme:error:userActionRequired"
|
ProblemUserActionRequired = "urn:ietf:params:acme:error:userActionRequired"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -788,13 +788,13 @@ func (e *CAEngine) handleIssue(ctx context.Context, req *engine.Request) (*engin
|
|||||||
|
|
||||||
return &engine.Response{
|
return &engine.Response{
|
||||||
Data: map[string]interface{}{
|
Data: map[string]interface{}{
|
||||||
"serial": serial,
|
"serial": serial,
|
||||||
"cert_pem": string(leafCertPEM),
|
"cert_pem": string(leafCertPEM),
|
||||||
"key_pem": string(leafKeyPEM),
|
"key_pem": string(leafKeyPEM),
|
||||||
"chain_pem": string(chainPEM),
|
"chain_pem": string(chainPEM),
|
||||||
"cn": cn,
|
"cn": cn,
|
||||||
"sans": allSANs,
|
"sans": allSANs,
|
||||||
"issued_by": req.CallerInfo.Username,
|
"issued_by": req.CallerInfo.Username,
|
||||||
"expires_at": leafCert.NotAfter,
|
"expires_at": leafCert.NotAfter,
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -82,7 +83,7 @@ func setupEngine(t *testing.T) (*CAEngine, *memBarrier) {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
config := map[string]interface{}{
|
config := map[string]interface{}{
|
||||||
"organization": "TestOrg",
|
"organization": "TestOrg",
|
||||||
"key_algorithm": "ecdsa",
|
"key_algorithm": "ecdsa",
|
||||||
"key_size": float64(256),
|
"key_size": float64(256),
|
||||||
"root_expiry": "87600h",
|
"root_expiry": "87600h",
|
||||||
@@ -133,7 +134,7 @@ func TestInitializeWithImportedRoot(t *testing.T) {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
config := map[string]interface{}{
|
config := map[string]interface{}{
|
||||||
"organization": "ImportOrg",
|
"organization": "ImportOrg",
|
||||||
"root_cert_pem": string(rootPEM),
|
"root_cert_pem": string(rootPEM),
|
||||||
"root_key_pem": string(srcKeyPEM),
|
"root_key_pem": string(srcKeyPEM),
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"`
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package engine
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -31,10 +32,12 @@ func (m *mockEngine) HandleRequest(_ context.Context, _ *Request) (*Response, er
|
|||||||
|
|
||||||
type mockBarrier struct{}
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -22,16 +22,15 @@ import (
|
|||||||
|
|
||||||
// GRPCServer wraps the gRPC server and all service implementations.
|
// GRPCServer wraps the gRPC server and all service implementations.
|
||||||
type GRPCServer struct {
|
type GRPCServer struct {
|
||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
sealMgr *seal.Manager
|
sealMgr *seal.Manager
|
||||||
auth *auth.Authenticator
|
auth *auth.Authenticator
|
||||||
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.
|
||||||
|
|||||||
@@ -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,32 +32,28 @@ 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)
|
|
||||||
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
|
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)
|
|
||||||
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 {
|
if err := ss.s.engines.UnsealAll(ctx); err != nil {
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -50,18 +50,15 @@ var (
|
|||||||
|
|
||||||
// Manager manages the seal/unseal lifecycle.
|
// Manager manages the seal/unseal lifecycle.
|
||||||
type Manager struct {
|
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
|
lastAttempt time.Time
|
||||||
lockoutUntil 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.
|
// NewManager creates a new seal manager.
|
||||||
@@ -205,10 +202,10 @@ func (m *Manager) Unseal(password []byte) error {
|
|||||||
|
|
||||||
// Read seal config.
|
// Read seal config.
|
||||||
var (
|
var (
|
||||||
encryptedMEK []byte
|
encryptedMEK []byte
|
||||||
salt []byte
|
salt []byte
|
||||||
argTime, argMem uint32
|
argTime, argMem uint32
|
||||||
argThreads uint8
|
argThreads uint8
|
||||||
)
|
)
|
||||||
err := m.db.QueryRow(`
|
err := m.db.QueryRow(`
|
||||||
SELECT encrypted_mek, kdf_salt, argon2_time, argon2_memory, argon2_threads
|
SELECT encrypted_mek, kdf_salt, argon2_time, argon2_memory, argon2_threads
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -23,18 +23,17 @@ import (
|
|||||||
|
|
||||||
// Server is the Metacrypt HTTP server.
|
// Server is the Metacrypt HTTP server.
|
||||||
type Server struct {
|
type Server struct {
|
||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
seal *seal.Manager
|
seal *seal.Manager
|
||||||
auth *auth.Authenticator
|
auth *auth.Authenticator
|
||||||
policy *policy.Engine
|
policy *policy.Engine
|
||||||
engines *engine.Registry
|
engines *engine.Registry
|
||||||
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.
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 == "" {
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user