87 lines
2.3 KiB
Go
87 lines
2.3 KiB
Go
package vault
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"time"
|
||
|
||
vault "github.com/hashicorp/vault-client-go"
|
||
)
|
||
|
||
type PKIClient struct {
|
||
client *vault.Client
|
||
mount string // e.g. "pki_iot"
|
||
role string // e.g. "device"
|
||
}
|
||
|
||
type SignResponse struct {
|
||
Certificate string `json:"certificate"`
|
||
IssuingCA string `json:"issuing_ca"`
|
||
CAChain []string `json:"ca_chain"`
|
||
PrivateKey string `json:"private_key,omitempty"`
|
||
PrivateKeyType string `json:"private_key_type,omitempty"`
|
||
SerialNumber string `json:"serial_number"`
|
||
}
|
||
|
||
func NewPKI(addr, token, mount, role string, timeout time.Duration) (*PKIClient, error) {
|
||
client, err := vault.New(
|
||
vault.WithAddress(addr),
|
||
vault.WithRequestTimeout(timeout),
|
||
)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("vault new: %w", err)
|
||
}
|
||
if err := client.SetToken(token); err != nil {
|
||
return nil, fmt.Errorf("set token: %w", err)
|
||
}
|
||
return &PKIClient{client: client, mount: mount, role: role}, nil
|
||
}
|
||
|
||
// SignCSR calls: /v1/<mount>/sign/<role>
|
||
func (p *PKIClient) SignCSR(ctx context.Context, csrPEM, uriSAN string, ttl string) (*SignResponse, error) {
|
||
if p.client == nil {
|
||
return nil, fmt.Errorf("vault client is nil")
|
||
}
|
||
path := fmt.Sprintf("/%s/sign/%s", p.mount, p.role)
|
||
req := map[string]any{
|
||
"csr": csrPEM,
|
||
"uri_sans": uriSAN, // e.g. "urn:device:<GUID>"
|
||
}
|
||
if ttl != "" {
|
||
req["ttl"] = ttl // e.g. "720h"
|
||
}
|
||
|
||
resp, err := p.client.Write(ctx, path, req)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if resp == nil || resp.Data == nil {
|
||
return nil, fmt.Errorf("vault sign: empty response")
|
||
}
|
||
|
||
// resp.Data contains the fields we need
|
||
var out SignResponse
|
||
if err := mapToStruct(resp.Data, &out); err != nil {
|
||
return nil, err
|
||
}
|
||
return &out, nil
|
||
}
|
||
|
||
// RebuildCRL triggers CRL regeneration (rotate).
|
||
// HCP: POST /v1/<mount>/crl/rotate or read /crl to force render; we’ll call rotate when available,
|
||
// else read to ensure CRL is (re)generated.
|
||
func (p *PKIClient) RebuildCRL(ctx context.Context) error {
|
||
_, _ = p.client.Write(ctx, fmt.Sprintf("/%s/crl/rotate", p.mount), nil) // best effort
|
||
_, err := p.client.Read(ctx, fmt.Sprintf("/%s/crl", p.mount))
|
||
return err
|
||
}
|
||
|
||
func mapToStruct(m map[string]any, out any) error {
|
||
b, err := json.Marshal(m)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return json.Unmarshal(b, out)
|
||
}
|