Skip to main content

PKI -- TLS Certificate Authority

Issues TLS certificates on demand, signed by an internal CA. Each generate call creates an ECDSA P-256 key pair and an X.509 certificate with the requested SANs. Certificates are signed by the configured CA and returned immediately -- no CSR workflow needed.

Capabilities

CapabilityDescription
secret_generationIssues X.509 certificates signed by the CA
secret_validationVerifies a certificate was issued by this CA and is not expired

Configuration

ParameterRequiredDescriptionExample
ca_certificateOptionalPEM-encoded CA certificate. If omitted, a self-signed CA is generated at bootstrap-----BEGIN CERTIFICATE-----...
ca_private_keyOptionalPEM-encoded CA private key. Required if ca_certificate is provided-----BEGIN EC PRIVATE KEY-----...
default_ttlOptionalDefault certificate TTL. Defaults to 30d (30 days)90d
max_ttlOptionalMaximum allowed TTL. Defaults to 365d (1 year)365d
organizationOptionalOrganization name in issued certificates. Defaults to Arcan PKIExample Corp

When both ca_certificate and ca_private_key are omitted, the engine generates a self-signed ECDSA P-256 CA at bootstrap. The CA certificate is returned in every generate response so clients can add it to their trust store.

Roles

RoleExtended Key UsageMax TTLUse Case
serverServerAuth90 daysTLS server certificates (HTTPS, gRPC)
clientClientAuth30 daysMutual TLS client certificates
bothServerAuth + ClientAuth90 daysServices that act as both client and server

Operations

generate

Creates a key pair and issues a signed X.509 certificate.

Request:

{
"method": "generate",
"params": {
"common_name": "api.example.com",
"san": ["api.example.com", "api-internal.example.com", "10.0.1.5"],
"role": "server",
"ttl": "30d"
}
}

What happens internally:

  1. Generates a new ECDSA P-256 key pair for the leaf certificate
  2. Builds an X.509 certificate template with the requested CN, SANs, and role-specific ExtKeyUsage
  3. Parses SANs to separate DNS names from IP addresses
  4. Sets a random serial number, NotBefore (now), and NotAfter (now + TTL)
  5. Signs the certificate with the CA private key
  6. PEM-encodes and returns the certificate, private key, and CA certificate

Response:

{
"data": {
"certificate": "-----BEGIN CERTIFICATE-----\nMIIB...\n-----END CERTIFICATE-----",
"private_key": "-----BEGIN EC PRIVATE KEY-----\nMHQC...\n-----END EC PRIVATE KEY-----",
"ca_certificate": "-----BEGIN CERTIFICATE-----\nMIIB...\n-----END CERTIFICATE-----",
"common_name": "api.example.com",
"san": ["api.example.com", "api-internal.example.com", "10.0.1.5"],
"serial": "a1b2c3d4e5f6",
"not_before": "2026-04-02T10:00:00Z",
"expires_at": "2026-05-02T10:00:00Z"
}
}

validate

Verifies a certificate was issued by this CA and is still valid.

Request:

{
"method": "validate",
"params": {
"certificate": "-----BEGIN CERTIFICATE-----\nMIIB...\n-----END CERTIFICATE-----"
}
}

Response (valid):

{
"data": {
"valid": true,
"message": "cn=api.example.com serial=a1b2c3d4e5f6 expires=2026-05-02T10:00:00Z"
}
}

Response (invalid):

{
"data": {
"valid": false,
"message": "certificate has expired"
}
}

Complete Go Plugin Source

package main

import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/hex"
"encoding/json"
"encoding/pem"
"fmt"
"math/big"
"net"
"os"
"strings"
"time"

"getarcan.dev/arcan/sdk"
)

type PKIEngine struct {
caCert *x509.Certificate
caKey *ecdsa.PrivateKey
caPEM string
org string
maxTTL time.Duration
}

type generateParams struct {
CommonName string `json:"common_name"`
SAN []string `json:"san"`
Role string `json:"role"`
TTL string `json:"ttl"`
}

type validateParams struct {
Certificate string `json:"certificate"`
}

type roleConfig struct {
ExtKeyUsage []x509.ExtKeyUsage
MaxTTL time.Duration
}

var roles = map[string]roleConfig{
"server": {ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, MaxTTL: 90 * 24 * time.Hour},
"client": {ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, MaxTTL: 30 * 24 * time.Hour},
"both": {ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, MaxTTL: 90 * 24 * time.Hour},
}

func (e *PKIEngine) Describe() sdk.Descriptor {
return sdk.Descriptor{
Name: "pki",
Version: "0.1.0",
DisplayName: "PKI",
Description: "Issues TLS certificates signed by an internal CA",
Capabilities: []string{"secret_generation", "secret_validation"},
}
}

func (e *PKIEngine) Ping() error {
if e.caCert == nil || e.caKey == nil {
return fmt.Errorf("CA not loaded")
}
if time.Now().UTC().After(e.caCert.NotAfter) {
return fmt.Errorf("CA certificate has expired")
}
return nil
}

func (e *PKIEngine) Generate(params json.RawMessage) (*sdk.SecretResult, error) {
var p generateParams
if err := json.Unmarshal(params, &p); err != nil {
return nil, fmt.Errorf("invalid params: %w", err)
}
if p.CommonName == "" {
return nil, fmt.Errorf("common_name is required")
}

role, ok := roles[p.Role]
if !ok {
p.Role = "server"
role = roles["server"]
}

ttl := 30 * 24 * time.Hour
if p.TTL != "" {
parsed, err := parseDuration(p.TTL)
if err != nil {
return nil, fmt.Errorf("invalid ttl: %w", err)
}
ttl = parsed
}
if ttl > role.MaxTTL {
return nil, fmt.Errorf("ttl %s exceeds max %s for role %s", ttl, role.MaxTTL, p.Role)
}

// Generate leaf key pair
leafKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, fmt.Errorf("generating key: %w", err)
}

// Parse SANs into DNS names and IPs
var dnsNames []string
var ipAddrs []net.IP
for _, san := range p.SAN {
if ip := net.ParseIP(san); ip != nil {
ipAddrs = append(ipAddrs, ip)
} else {
dnsNames = append(dnsNames, san)
}
}

serialBytes := make([]byte, 16)
if _, err := rand.Read(serialBytes); err != nil {
return nil, fmt.Errorf("generating serial: %w", err)
}
serial := new(big.Int).SetBytes(serialBytes)

now := time.Now().UTC()
template := &x509.Certificate{
SerialNumber: serial,
Subject: pkix.Name{
CommonName: p.CommonName,
Organization: []string{e.org},
},
DNSNames: dnsNames,
IPAddresses: ipAddrs,
NotBefore: now,
NotAfter: now.Add(ttl),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
ExtKeyUsage: role.ExtKeyUsage,
BasicConstraintsValid: true,
}

certDER, err := x509.CreateCertificate(rand.Reader, template, e.caCert, &leafKey.PublicKey, e.caKey)
if err != nil {
return nil, fmt.Errorf("signing certificate: %w", err)
}

certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
keyDER, err := x509.MarshalECPrivateKey(leafKey)
if err != nil {
return nil, fmt.Errorf("marshalling key: %w", err)
}
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER})

return &sdk.SecretResult{
Data: map[string]any{
"certificate": string(certPEM),
"private_key": string(keyPEM),
"ca_certificate": e.caPEM,
"common_name": p.CommonName,
"san": p.SAN,
"serial": hex.EncodeToString(serialBytes),
"not_before": now.Format(time.RFC3339),
"expires_at": now.Add(ttl).Format(time.RFC3339),
},
}, nil
}

func (e *PKIEngine) Validate(params json.RawMessage) (*sdk.ValidationResult, error) {
var p validateParams
if err := json.Unmarshal(params, &p); err != nil {
return nil, fmt.Errorf("invalid params: %w", err)
}

block, _ := pem.Decode([]byte(p.Certificate))
if block == nil {
return &sdk.ValidationResult{Valid: false, Message: "invalid PEM data"}, nil
}

cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return &sdk.ValidationResult{Valid: false, Message: fmt.Sprintf("parse error: %v", err)}, nil
}

roots := x509.NewCertPool()
roots.AddCert(e.caCert)

if _, err := cert.Verify(x509.VerifyOptions{Roots: roots}); err != nil {
return &sdk.ValidationResult{Valid: false, Message: err.Error()}, nil
}

return &sdk.ValidationResult{
Valid: true,
Message: fmt.Sprintf("cn=%s serial=%s expires=%s",
cert.Subject.CommonName,
cert.SerialNumber.Text(16),
cert.NotAfter.UTC().Format(time.RFC3339)),
}, nil
}

// parseDuration handles "30d" style durations in addition to Go's time.ParseDuration.
func parseDuration(s string) (time.Duration, error) {
if strings.HasSuffix(s, "d") {
s = strings.TrimSuffix(s, "d")
var days int
if _, err := fmt.Sscanf(s, "%d", &days); err != nil {
return 0, err
}
return time.Duration(days) * 24 * time.Hour, nil
}
return time.ParseDuration(s)
}

func generateCA(org string) (*x509.Certificate, *ecdsa.PrivateKey, string, error) {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, nil, "", err
}

serial, _ := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
now := time.Now().UTC()

template := &x509.Certificate{
SerialNumber: serial,
Subject: pkix.Name{
CommonName: "Arcan PKI CA",
Organization: []string{org},
},
NotBefore: now,
NotAfter: now.Add(10 * 365 * 24 * time.Hour),
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
BasicConstraintsValid: true,
IsCA: true,
MaxPathLen: 1,
}

certDER, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
if err != nil {
return nil, nil, "", err
}

cert, err := x509.ParseCertificate(certDER)
if err != nil {
return nil, nil, "", err
}

certPEM := string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}))
return cert, key, certPEM, nil
}

func main() {
org := os.Getenv("ARCAN_PKI_ORGANIZATION")
if org == "" {
org = "Arcan PKI"
}

var caCert *x509.Certificate
var caKey *ecdsa.PrivateKey
var caPEM string

certData := os.Getenv("ARCAN_PKI_CA_CERT")
keyData := os.Getenv("ARCAN_PKI_CA_KEY")

if certData != "" && keyData != "" {
// Load provided CA
certBlock, _ := pem.Decode([]byte(certData))
if certBlock == nil {
fmt.Fprintf(os.Stderr, "invalid CA certificate PEM\n")
os.Exit(1)
}
var err error
caCert, err = x509.ParseCertificate(certBlock.Bytes)
if err != nil {
fmt.Fprintf(os.Stderr, "parsing CA certificate: %v\n", err)
os.Exit(1)
}

keyBlock, _ := pem.Decode([]byte(keyData))
if keyBlock == nil {
fmt.Fprintf(os.Stderr, "invalid CA key PEM\n")
os.Exit(1)
}
caKey, err = x509.ParseECPrivateKey(keyBlock.Bytes)
if err != nil {
fmt.Fprintf(os.Stderr, "parsing CA key: %v\n", err)
os.Exit(1)
}
caPEM = certData
} else {
// Generate ephemeral CA
var err error
caCert, caKey, caPEM, err = generateCA(org)
if err != nil {
fmt.Fprintf(os.Stderr, "generating CA: %v\n", err)
os.Exit(1)
}
fmt.Fprintf(os.Stderr, "WARNING: using ephemeral CA -- set ARCAN_PKI_CA_CERT and ARCAN_PKI_CA_KEY for persistence\n")
}

eng := &PKIEngine{
caCert: caCert,
caKey: caKey,
caPEM: caPEM,
org: org,
maxTTL: 365 * 24 * time.Hour,
}
sdk.Serve(eng)
}

Build & Install

mkdir -p engines/pki && cd engines/pki
go mod init getarcan.dev/engines/pki
go get getarcan.dev/arcan/sdk

# Build (no external dependencies -- Go stdlib only)
go build -o arcan-engine-pki .

# Install
cp arcan-engine-pki ~/.arcan/plugins/

Usage

# Start with ephemeral CA (development)
echo '{"method":"ping","params":{}}' | ./arcan-engine-pki
# => {"data":{"status":"healthy"}}

# Issue a server certificate
echo '{"method":"generate","params":{"common_name":"api.example.com","san":["api.example.com","10.0.1.5"],"role":"server","ttl":"30d"}}' | ./arcan-engine-pki
# => {"data":{"certificate":"-----BEGIN CERTIFICATE-----\n...","private_key":"-----BEGIN EC PRIVATE KEY-----\n...","ca_certificate":"-----BEGIN CERTIFICATE-----\n...",...}}

# Validate a certificate
echo '{"method":"validate","params":{"certificate":"-----BEGIN CERTIFICATE-----\n..."}}' | ./arcan-engine-pki
# => {"data":{"valid":true,"message":"cn=api.example.com serial=... expires=..."}}

# Use the certificate with a Go server
# tls.LoadX509KeyPair("cert.pem", "key.pem")

Using issued certificates

# Save outputs to files
echo "$CERTIFICATE" > server.crt
echo "$PRIVATE_KEY" > server.key
echo "$CA_CERTIFICATE" > ca.crt

# Start a TLS server (e.g., nginx)
# ssl_certificate /etc/nginx/server.crt;
# ssl_certificate_key /etc/nginx/server.key;

# Client trusts the CA
curl --cacert ca.crt https://api.example.com

Mutual TLS example

# Issue client certificate
echo '{"method":"generate","params":{"common_name":"worker-1","san":["worker-1.internal"],"role":"client","ttl":"7d"}}' | ./arcan-engine-pki

# Client connects with cert
curl --cacert ca.crt --cert client.crt --key client.key https://api.example.com

Security Notes

  • The CA private key is the root of trust. In production, load it from a KMS or hardware security module -- never from an environment variable in plaintext.
  • ECDSA P-256 is used for all generated key pairs (leaf and CA). It provides 128-bit security equivalent with smaller keys and faster operations than RSA.
  • Serial numbers are 128-bit random values from crypto/rand, providing collision resistance without a centralized counter.
  • The engine does not maintain a Certificate Revocation List (CRL) or OCSP responder. Use short TTLs (hours to days) to minimize the revocation window, or build CRL support as an extension.
  • All timestamps use UTC (time.Now().UTC()).
  • The parseDuration helper supports day-based durations (30d, 90d) in addition to Go's standard duration format (24h, 720h).
  • The CA certificate has MaxPathLen: 1, preventing issued certificates from acting as intermediate CAs.
  • IP addresses in the SAN list are automatically detected and placed in IPAddresses (not DNSNames) for correct X.509 validation.