mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 16:21:30 +00:00
3d15a3e5af
Production hardening II Phase 1.
The OCSP responder previously ignored the request's nonce extension
entirely, leaving relying parties vulnerable to replay attacks. RFC
6960 §4.4.1 defines the OPTIONAL id-pkix-ocsp-nonce extension (OID
1.3.6.1.5.5.7.48.1.2): when present in the request, the responder
MUST echo the same value in the response; when absent, no nonce in
the response (back-compat with relying parties that don't send one).
NEW internal/service/ocsp_nonce.go: ParseOCSPRequestNonce walks raw
DER (golang.org/x/crypto/ocsp.Request doesn't expose the request's
extensions field — the library only exposes IssuerNameHash +
IssuerKeyHash + SerialNumber). Returns one of three states:
- (nil, false, nil) — no nonce extension in request
- (nonce, true, nil) — well-formed nonce, ≤ MaxOCSPNonceLength (32)
- (nil, false, ErrOCSPNonceMalformed) — empty or oversized
NEW internal/service/ocsp_counters.go: sync/atomic counter table for
OCSP request lifecycle (request_get/post, request_success/invalid,
nonce_echoed, nonce_malformed, rate_limited, ...). Mirrors the EST/
SCEP counter pattern; Phase 8 wires these into /metrics/prometheus.
CertSrv types extended:
- internal/connector/issuer/interface.go::OCSPSignRequest gains
Nonce []byte field.
- internal/service/renewal.go::OCSPSignRequest (the service-layer
duplicate used by ca_operations.go) gains the same field.
- internal/service/issuer_adapter.go bridges the two.
Service path: CAOperationsSvc.GetOCSPResponseWithNonce(ctx, issuerID,
serialHex, nonce) is the new entry point that plumbs the nonce
through every signing site (good / revoked / unknown / short-lived).
The legacy GetOCSPResponse becomes a nil-nonce wrapper for back-
compat — every existing caller (tests, the GET handler) sees no
behavior change.
CertificateService gains the same WithNonce variant; the handler
interface adds it to the contract. MockCertificateService in tests
extended with the new method (delegates to the legacy fn when no
override is set, so existing tests that don't care about the nonce
keep working).
Local issuer's SignOCSPResponse appends the id-pkix-ocsp-nonce
extension (non-Critical per RFC 6960 §4.4) to the response template's
ExtraExtensions when req.Nonce != nil. The extnValue is the nonce
bytes wrapped in an OCTET STRING per RFC 6960 §4.4.1.
POST OCSP handler (HandleOCSPPost):
- After ocsp.ParseRequest succeeds, calls ParseOCSPRequestNonce on
the raw body to extract the optional nonce.
- On ErrOCSPNonceMalformed (empty or > 32 bytes): writes an
'unauthorized' OCSP response (status 6 per RFC 6960 §2.3) using
the canonical ocsp.UnauthorizedErrorResponse from x/crypto/ocsp.
Does NOT echo malicious bytes back.
- On well-formed nonce: passes it through GetOCSPResponseWithNonce.
- On no nonce: nil passed through; back-compat preserved.
GET OCSP handler unchanged — the GET form has no body to carry a
nonce extension.
6 new tests in internal/service/ocsp_nonce_test.go pin every
documented failure mode + the 32-byte boundary. The test fixture
builds an OCSPRequest via golang.org/x/crypto/ocsp.CreateRequest then
splices in a [2] EXPLICIT Extensions element by hand (the library
doesn't expose extension construction either).
Pre-commit verification: gofmt clean, go vet clean across affected
packages, go test -short -count=1 green for service/ + handler/ +
connector/issuer/local/. No new env vars introduced (Phase 1 is
always-on per RFC; no operator opt-out).
133 lines
5.4 KiB
Go
133 lines
5.4 KiB
Go
package issuer
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"math/big"
|
|
"time"
|
|
)
|
|
|
|
// Connector defines the interface for certificate issuance operations.
|
|
type Connector interface {
|
|
// ValidateConfig validates the issuer configuration.
|
|
ValidateConfig(ctx context.Context, config json.RawMessage) error
|
|
|
|
// IssueCertificate issues a new certificate.
|
|
IssueCertificate(ctx context.Context, request IssuanceRequest) (*IssuanceResult, error)
|
|
|
|
// RenewCertificate renews an existing certificate.
|
|
RenewCertificate(ctx context.Context, request RenewalRequest) (*IssuanceResult, error)
|
|
|
|
// RevokeCertificate revokes a certificate.
|
|
RevokeCertificate(ctx context.Context, request RevocationRequest) error
|
|
|
|
// GetOrderStatus retrieves the status of an issuance or renewal order.
|
|
GetOrderStatus(ctx context.Context, orderID string) (*OrderStatus, error)
|
|
|
|
// GenerateCRL generates a DER-encoded X.509 CRL signed by this issuer.
|
|
// Returns nil if the issuer does not support CRL generation (e.g., ACME).
|
|
GenerateCRL(ctx context.Context, revokedCerts []RevokedCertEntry) ([]byte, error)
|
|
|
|
// SignOCSPResponse signs an OCSP response for the given certificate serial.
|
|
// Returns nil if the issuer does not support OCSP (e.g., ACME).
|
|
SignOCSPResponse(ctx context.Context, req OCSPSignRequest) ([]byte, error)
|
|
|
|
// GetCACertPEM returns the PEM-encoded CA certificate chain for this issuer.
|
|
// Used by the EST /cacerts endpoint. Returns empty string if not available.
|
|
GetCACertPEM(ctx context.Context) (string, error)
|
|
|
|
// GetRenewalInfo retrieves ACME Renewal Information (ARI) per RFC 9773 for a certificate.
|
|
// certPEM is the PEM-encoded certificate. Returns nil, nil if the CA does not support ARI.
|
|
GetRenewalInfo(ctx context.Context, certPEM string) (*RenewalInfoResult, error)
|
|
}
|
|
|
|
// RenewalInfoResult holds the ACME ARI response from a CA.
|
|
type RenewalInfoResult struct {
|
|
SuggestedWindowStart time.Time
|
|
SuggestedWindowEnd time.Time
|
|
RetryAfter time.Time
|
|
ExplanationURL string
|
|
}
|
|
|
|
// IssuanceRequest contains the parameters for issuing a new certificate.
|
|
type IssuanceRequest struct {
|
|
CommonName string `json:"common_name"`
|
|
SANs []string `json:"sans"`
|
|
CSRPEM string `json:"csr_pem"`
|
|
EKUs []string `json:"ekus,omitempty"` // e.g., "serverAuth", "clientAuth", "emailProtection"
|
|
MaxTTLSeconds int `json:"max_ttl_seconds,omitempty"` // 0 = no cap (use issuer default)
|
|
// MustStaple, when true, instructs the issuer to add the RFC 7633
|
|
// must-staple extension (id-pe-tlsfeature) to the issued cert.
|
|
// Plumbed from CertificateProfile.MustStaple at the service layer.
|
|
// Issuers that don't support extension injection (Vault, EJBCA, etc.)
|
|
// silently ignore this — must-staple is a local-issuer-only feature
|
|
// in V2 since upstream connectors enforce their own extension policy.
|
|
MustStaple bool `json:"must_staple,omitempty"`
|
|
}
|
|
|
|
// IssuanceResult contains the result of a successful certificate issuance.
|
|
type IssuanceResult struct {
|
|
CertPEM string `json:"cert_pem"`
|
|
ChainPEM string `json:"chain_pem"`
|
|
Serial string `json:"serial"`
|
|
NotBefore time.Time `json:"not_before"`
|
|
NotAfter time.Time `json:"not_after"`
|
|
OrderID string `json:"order_id"`
|
|
}
|
|
|
|
// RenewalRequest contains the parameters for renewing a certificate.
|
|
type RenewalRequest struct {
|
|
CommonName string `json:"common_name"`
|
|
SANs []string `json:"sans"`
|
|
CSRPEM string `json:"csr_pem"`
|
|
EKUs []string `json:"ekus,omitempty"` // e.g., "serverAuth", "clientAuth", "emailProtection"
|
|
MaxTTLSeconds int `json:"max_ttl_seconds,omitempty"` // 0 = no cap (use issuer default)
|
|
OrderID *string `json:"order_id,omitempty"`
|
|
// MustStaple — same semantics as IssuanceRequest.MustStaple. The
|
|
// renewal pipeline plumbs through the same CertificateProfile.MustStaple
|
|
// field so renewed certs match their initial-issuance extension set.
|
|
MustStaple bool `json:"must_staple,omitempty"`
|
|
}
|
|
|
|
// RevocationRequest contains the parameters for revoking a certificate.
|
|
type RevocationRequest struct {
|
|
Serial string `json:"serial"`
|
|
Reason *string `json:"reason,omitempty"`
|
|
}
|
|
|
|
// OrderStatus contains the status of a pending issuance or renewal order.
|
|
type OrderStatus struct {
|
|
OrderID string `json:"order_id"`
|
|
Status string `json:"status"`
|
|
Message *string `json:"message,omitempty"`
|
|
CertPEM *string `json:"cert_pem,omitempty"`
|
|
ChainPEM *string `json:"chain_pem,omitempty"`
|
|
Serial *string `json:"serial,omitempty"`
|
|
NotBefore *time.Time `json:"not_before,omitempty"`
|
|
NotAfter *time.Time `json:"not_after,omitempty"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
// RevokedCertEntry represents a revoked certificate for CRL generation.
|
|
type RevokedCertEntry struct {
|
|
SerialNumber *big.Int
|
|
RevokedAt time.Time
|
|
ReasonCode int
|
|
}
|
|
|
|
// OCSPSignRequest contains the parameters for signing an OCSP response.
|
|
type OCSPSignRequest struct {
|
|
CertSerial *big.Int
|
|
CertStatus int // 0=good, 1=revoked, 2=unknown
|
|
RevokedAt time.Time
|
|
RevocationReason int
|
|
ThisUpdate time.Time
|
|
NextUpdate time.Time
|
|
// Nonce — RFC 6960 §4.4.1 OCSP-nonce extension echo. When non-nil,
|
|
// the responder MUST include this value in the response's
|
|
// singleExtensions field. When nil, the response MUST NOT carry
|
|
// a nonce extension (back-compat with relying parties that don't
|
|
// understand it). Production hardening II Phase 1.
|
|
Nonce []byte
|
|
}
|