internal/rest/tls.go
Rama main · réplica de lectura del repositorio original.
package rest
import (
"context"
"errors"
"net/http"
"time"
"github.com/danielgtaylor/huma/v2"
"github.com/sarathsp06/sparrow/internal/tlsrevocation"
"github.com/sarathsp06/sparrow/pkg/safetransport"
)
// TLSRevocation is the server's CRL checker as the TLS endpoints see it
// (*tlsrevocation.Refresher; nil when SPARROW_TLS_REVOCATION=off).
type TLSRevocation interface {
Mode() string
Status() []safetransport.CRLStatus
RequestRefresh(ctx context.Context) error
}
type crlOut struct {
URL string `json:"url" doc:"CRL distribution point, from an intermediate in a receiver's chain."`
Issuer string `json:"issuer" doc:"Common name of the CA that signs this CRL."`
FetchedAt *time.Time `json:"fetched_at,omitempty" doc:"When this instance last downloaded it successfully."`
NextUpdate *time.Time `json:"next_update,omitempty" doc:"When the CA promises a newer CRL. Past it, the CRL is downloaded again before use."`
Entries int `json:"entries" doc:"Revoked certificates it lists."`
Error string `json:"error,omitempty" doc:"Why the last download or check failed. In soft mode such a CRL gives no answer and connections are allowed."`
}
type listCrlsOutput struct {
Body struct {
Mode string `json:"mode" enum:"soft,hard,off" doc:"SPARROW_TLS_REVOCATION: soft allows a connection whose CRL is unavailable, hard refuses it, off skips the check."`
CRLs []crlOut `json:"crls" doc:"CRLs this instance has cached, by URL. Each instance keeps its own cache."`
}
}
type refreshCrlsOutput struct{}
// registerTLSRoutes registers the CRL status and refresh endpoints. rev may
// be nil (revocation off).
func registerTLSRoutes(api huma.API, rev TLSRevocation) {
if rev == nil {
rev = (*tlsrevocation.Refresher)(nil)
}
huma.Register(api, huma.Operation{
OperationID: "listCrls",
Method: http.MethodGet,
Path: "/v1/tls/crls",
Summary: "List cached CRLs",
Description: "Shows the CRLs the instance that answers has cached for checking intermediates in receivers' TLS chains, with when each was fetched and any error. " +
"CRLs are downloaded the first time a chain needs them, refreshed in the background by the next handshake once older than SPARROW_CRL_MAX_AGE, and on demand with refreshCrls. " +
"Each instance keeps its own cache, so behind a load balancer this shows one instance.",
Tags: []string{"Server"},
}, func(_ context.Context, _ *struct{}) (*listCrlsOutput, error) {
out := &listCrlsOutput{}
out.Body.Mode = rev.Mode()
out.Body.CRLs = []crlOut{}
for _, s := range rev.Status() {
c := crlOut{URL: s.URL, Issuer: s.Issuer, Entries: s.Entries, Error: s.Error}
if !s.FetchedAt.IsZero() {
c.FetchedAt = &s.FetchedAt
}
if !s.NextUpdate.IsZero() {
c.NextUpdate = &s.NextUpdate
}
out.Body.CRLs = append(out.Body.CRLs, c)
}
return out, nil
})
huma.Register(api, huma.Operation{
OperationID: "refreshCrls",
Method: http.MethodPost,
Path: "/v1/tls/crls:refresh",
Summary: "Refresh cached CRLs on every instance",
Description: "Asks every instance (through Postgres NOTIFY) to download its cached CRLs now, for when a CA revokes an intermediate and deliveries should stop trusting it before SPARROW_CRL_MAX_AGE passes. Returns 202 once the request is sent; check listCrls for the result. One request a minute on each server: more answer 429. 409 when SPARROW_TLS_REVOCATION=off.",
DefaultStatus: http.StatusAccepted,
Errors: []int{409, 429},
Tags: []string{"Server"},
}, func(ctx context.Context, _ *struct{}) (*refreshCrlsOutput, error) {
err := rev.RequestRefresh(ctx)
switch {
case errors.Is(err, tlsrevocation.ErrDisabled):
return nil, huma.Error409Conflict(err.Error())
case errors.Is(err, tlsrevocation.ErrTooSoon):
return nil, huma.Error429TooManyRequests(err.Error())
case err != nil:
return nil, mapError(ctx, err, "failed to request a CRL refresh")
}
return &refreshCrlsOutput{}, nil
})
}