137 lines
4.5 KiB
Go
137 lines
4.5 KiB
Go
|
|
package rotator
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"crypto/x509"
|
||
|
|
"fmt"
|
||
|
|
"net/http"
|
||
|
|
"net/url"
|
||
|
|
"strings"
|
||
|
|
)
|
||
|
|
|
||
|
|
type opnsenseClient struct {
|
||
|
|
baseURL string
|
||
|
|
apiKey string
|
||
|
|
apiSecret string
|
||
|
|
client *http.Client
|
||
|
|
}
|
||
|
|
|
||
|
|
type activationStatus struct {
|
||
|
|
Status string `json:"status"`
|
||
|
|
Phase string `json:"phase"`
|
||
|
|
Transaction string `json:"transaction"`
|
||
|
|
PreviousRefID string `json:"previous_refid"`
|
||
|
|
CandidateRefID string `json:"candidate_refid"`
|
||
|
|
RefID string `json:"refid"`
|
||
|
|
Deadline int64 `json:"deadline"`
|
||
|
|
Message string `json:"message"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type addCertificateResponse struct {
|
||
|
|
Result string `json:"result"`
|
||
|
|
UUID string `json:"uuid"`
|
||
|
|
Validations map[string]string `json:"validations"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type getCertificateResponse struct {
|
||
|
|
Certificate struct {
|
||
|
|
RefID string `json:"refid"`
|
||
|
|
} `json:"cert"`
|
||
|
|
}
|
||
|
|
|
||
|
|
func (o opnsenseClient) authenticatedRequest(ctx context.Context, method, path string, body any, output any) (*http.Response, error) {
|
||
|
|
endpoint, err := url.JoinPath(o.baseURL, path)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
client := roundTripperClient(o.client, func(request *http.Request) {
|
||
|
|
request.SetBasicAuth(o.apiKey, o.apiSecret)
|
||
|
|
})
|
||
|
|
return jsonRequest(ctx, client, method, endpoint, body, nil, output)
|
||
|
|
}
|
||
|
|
|
||
|
|
func roundTripperClient(source *http.Client, mutate func(*http.Request)) *http.Client {
|
||
|
|
clone := *source
|
||
|
|
clone.Transport = mutatingRoundTripper{next: source.Transport, mutate: mutate}
|
||
|
|
return &clone
|
||
|
|
}
|
||
|
|
|
||
|
|
type mutatingRoundTripper struct {
|
||
|
|
next http.RoundTripper
|
||
|
|
mutate func(*http.Request)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m mutatingRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
|
||
|
|
clone := request.Clone(request.Context())
|
||
|
|
m.mutate(clone)
|
||
|
|
return m.next.RoundTrip(clone)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (o opnsenseClient) status(ctx context.Context) (activationStatus, *x509.Certificate, error) {
|
||
|
|
var status activationStatus
|
||
|
|
response, err := o.authenticatedRequest(ctx, http.MethodGet, "/api/webguicertificate/service/status", nil, &status)
|
||
|
|
if err != nil {
|
||
|
|
return activationStatus{}, nil, err
|
||
|
|
}
|
||
|
|
if status.Status == "failed" {
|
||
|
|
return activationStatus{}, nil, fmt.Errorf("OPNsense activation status failed: %s", status.Message)
|
||
|
|
}
|
||
|
|
if response.TLS == nil || len(response.TLS.PeerCertificates) == 0 {
|
||
|
|
return activationStatus{}, nil, fmt.Errorf("OPNsense response has no TLS peer certificate")
|
||
|
|
}
|
||
|
|
return status, response.TLS.PeerCertificates[0], nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (o opnsenseClient) stage(ctx context.Context, certificate signedCertificate) (string, error) {
|
||
|
|
var added addCertificateResponse
|
||
|
|
description := "Vault-managed OPNsense WebGUI " + strings.ToLower(certificate.leaf.SerialNumber.Text(16))
|
||
|
|
body := map[string]any{"cert": map[string]any{
|
||
|
|
"action": "import",
|
||
|
|
"descr": description,
|
||
|
|
"cert_type": "server_cert",
|
||
|
|
"private_key_location": "firewall",
|
||
|
|
"key_type": "prime256v1",
|
||
|
|
"digest": "sha256",
|
||
|
|
"lifetime": "90",
|
||
|
|
"country": "KR",
|
||
|
|
"crt_payload": string(certificate.chain),
|
||
|
|
"prv_payload": string(certificate.key),
|
||
|
|
}}
|
||
|
|
if _, err := o.authenticatedRequest(ctx, http.MethodPost, "/api/trust/cert/add", body, &added); err != nil {
|
||
|
|
return "", err
|
||
|
|
}
|
||
|
|
if added.Result != "saved" || added.UUID == "" {
|
||
|
|
return "", fmt.Errorf("OPNsense rejected certificate: %v", added.Validations)
|
||
|
|
}
|
||
|
|
|
||
|
|
var stored getCertificateResponse
|
||
|
|
if _, err := o.authenticatedRequest(ctx, http.MethodGet, "/api/trust/cert/get/"+added.UUID, nil, &stored); err != nil {
|
||
|
|
return "", err
|
||
|
|
}
|
||
|
|
if stored.Certificate.RefID == "" {
|
||
|
|
return "", fmt.Errorf("OPNsense stored certificate has no reference")
|
||
|
|
}
|
||
|
|
return stored.Certificate.RefID, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (o opnsenseClient) activate(ctx context.Context, reference string) (activationStatus, error) {
|
||
|
|
var status activationStatus
|
||
|
|
if _, err := o.authenticatedRequest(ctx, http.MethodPost, "/api/webguicertificate/service/activate", map[string]string{"refid": reference}, &status); err != nil {
|
||
|
|
return activationStatus{}, err
|
||
|
|
}
|
||
|
|
if status.Status != "pending" || status.Transaction == "" {
|
||
|
|
return activationStatus{}, fmt.Errorf("OPNsense activation failed: %s", status.Message)
|
||
|
|
}
|
||
|
|
return status, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (o opnsenseClient) confirm(ctx context.Context, transaction string) error {
|
||
|
|
var status activationStatus
|
||
|
|
if _, err := o.authenticatedRequest(ctx, http.MethodPost, "/api/webguicertificate/service/confirm", map[string]string{"transaction": transaction}, &status); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if status.Status != "confirmed" {
|
||
|
|
return fmt.Errorf("OPNsense confirmation failed: %s", status.Message)
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|