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
| Capability | Description |
|---|---|
secret_generation | Issues X.509 certificates signed by the CA |
secret_validation | Verifies a certificate was issued by this CA and is not expired |
Configuration
| Parameter | Required | Description | Example |
|---|---|---|---|
ca_certificate | Optional | PEM-encoded CA certificate. If omitted, a self-signed CA is generated at bootstrap | -----BEGIN CERTIFICATE-----... |
ca_private_key | Optional | PEM-encoded CA private key. Required if ca_certificate is provided | -----BEGIN EC PRIVATE KEY-----... |
default_ttl | Optional | Default certificate TTL. Defaults to 30d (30 days) | 90d |
max_ttl | Optional | Maximum allowed TTL. Defaults to 365d (1 year) | 365d |
organization | Optional | Organization name in issued certificates. Defaults to Arcan PKI | Example 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
| Role | Extended Key Usage | Max TTL | Use Case |
|---|---|---|---|
server | ServerAuth | 90 days | TLS server certificates (HTTPS, gRPC) |
client | ClientAuth | 30 days | Mutual TLS client certificates |
both | ServerAuth + ClientAuth | 90 days | Services 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:
- Generates a new ECDSA P-256 key pair for the leaf certificate
- Builds an X.509 certificate template with the requested CN, SANs, and role-specific ExtKeyUsage
- Parses SANs to separate DNS names from IP addresses
- Sets a random serial number, NotBefore (now), and NotAfter (now + TTL)
- Signs the certificate with the CA private key
- 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
parseDurationhelper 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(notDNSNames) for correct X.509 validation.