Add ACME (RFC 8555) server and Go client library
Implements full ACME protocol support in Metacrypt:
- internal/acme: core types, JWS verification (ES256/384/512 + RS256),
nonce store, per-mount handler, all RFC 8555 protocol endpoints,
HTTP-01 and DNS-01 challenge validation, EAB management
- internal/server/acme.go: management REST routes (EAB create, config,
list accounts/orders) + ACME protocol route dispatch
- proto/metacrypt/v1/acme.proto: ACMEService (CreateEAB, SetConfig,
ListAccounts, ListOrders) — protocol endpoints are HTTP-only per RFC
- clients/go: new Go module with MCIAS-auth bootstrap, ACME account
registration, certificate issuance/renewal, HTTP-01 and DNS-01
challenge providers
- .claude/launch.json: dev server configuration
EAB is required for all account creation; MCIAS-authenticated users
obtain a single-use KID + HMAC-SHA256 key via POST /v1/acme/{mount}/eab.
This commit is contained in:
65
clients/go/challenge.go
Normal file
65
clients/go/challenge.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package metacryptclient
|
||||
|
||||
import "time"
|
||||
|
||||
// HTTP01Provider handles HTTP-01 ACME challenge fulfillment.
|
||||
// The caller is responsible for serving the challenge file over HTTP on port 80.
|
||||
type HTTP01Provider struct {
|
||||
// WriteFile is called to create the challenge response file.
|
||||
// path is the URL path, e.g. "/.well-known/acme-challenge/{token}".
|
||||
// content is the key authorization string to serve as the response body.
|
||||
WriteFile func(path, content string) error
|
||||
|
||||
// RemoveFile is called after the challenge is complete (success or failure).
|
||||
// path is the same path passed to WriteFile.
|
||||
RemoveFile func(path string) error
|
||||
}
|
||||
|
||||
// DNS01Provider handles DNS-01 ACME challenge fulfillment.
|
||||
// The caller is responsible for managing DNS TXT records.
|
||||
type DNS01Provider struct {
|
||||
// SetRecord is called to create the TXT record.
|
||||
// name is the fully-qualified DNS name, e.g. "_acme-challenge.example.com.".
|
||||
// value is the base64url-encoded SHA-256 key authorization to set as the TXT value.
|
||||
SetRecord func(name, value string) error
|
||||
|
||||
// RemoveRecord is called after the challenge is complete (success or failure).
|
||||
// name is the same name passed to SetRecord.
|
||||
RemoveRecord func(name string) error
|
||||
|
||||
// PropagationWait is the duration to wait after SetRecord before notifying
|
||||
// the ACME server to validate. Allows time for DNS propagation.
|
||||
// A value of 0 means no wait.
|
||||
PropagationWait time.Duration
|
||||
}
|
||||
|
||||
// CertificateRequest specifies what certificate to obtain.
|
||||
type CertificateRequest struct {
|
||||
// Domains is the list of DNS names to include as SANs.
|
||||
// The first domain is used as the common name.
|
||||
Domains []string
|
||||
|
||||
// KeyType controls the type and size of the certificate key.
|
||||
// Valid values: "EC256" (default), "EC384", "RSA2048", "RSA4096".
|
||||
KeyType string
|
||||
|
||||
// HTTP01 provides HTTP-01 challenge fulfillment. Either HTTP01 or DNS01
|
||||
// (or both) must be set.
|
||||
HTTP01 *HTTP01Provider
|
||||
|
||||
// DNS01 provides DNS-01 challenge fulfillment. Either HTTP01 or DNS01
|
||||
// (or both) must be set.
|
||||
DNS01 *DNS01Provider
|
||||
}
|
||||
|
||||
// Certificate is the result of a successful ACME certificate issuance.
|
||||
type Certificate struct {
|
||||
// CertPEM is the PEM-encoded certificate chain (leaf + intermediates).
|
||||
CertPEM []byte
|
||||
|
||||
// KeyPEM is the PEM-encoded private key for the certificate.
|
||||
KeyPEM []byte
|
||||
|
||||
// ExpiresAt is the expiry time of the leaf certificate.
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
301
clients/go/client.go
Normal file
301
clients/go/client.go
Normal file
@@ -0,0 +1,301 @@
|
||||
// Package metacryptclient provides a Go client for obtaining certificates
|
||||
// from a Metacrypt CA via ACME (RFC 8555) with MCIAS authentication.
|
||||
//
|
||||
// Typical usage:
|
||||
//
|
||||
// cfg := metacryptclient.Config{
|
||||
// MetacryptURL: "https://metacrypt.example.com",
|
||||
// MCIASURL: "https://mcias.example.com:8443",
|
||||
// Mount: "pki",
|
||||
// }
|
||||
// c := metacryptclient.New(cfg)
|
||||
// if err := c.Login(ctx, username, password, ""); err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// cert, err := c.ObtainCertificate(ctx, &metacryptclient.CertificateRequest{
|
||||
// Domains: []string{"myservice.example.com"},
|
||||
// HTTP01: &metacryptclient.HTTP01Provider{...},
|
||||
// })
|
||||
package metacryptclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/acme"
|
||||
)
|
||||
|
||||
// Config holds configuration for the Metacrypt ACME client.
|
||||
type Config struct {
|
||||
// MetacryptURL is the base URL of the Metacrypt server,
|
||||
// e.g. "https://metacrypt.example.com".
|
||||
MetacryptURL string
|
||||
|
||||
// MCIASURL is the base URL of the MCIAS identity server,
|
||||
// e.g. "https://mcias.example.com:8443".
|
||||
MCIASURL string
|
||||
|
||||
// CACertPath is an optional path to a PEM-encoded CA certificate for
|
||||
// verifying TLS connections to Metacrypt and MCIAS.
|
||||
CACertPath string
|
||||
|
||||
// Mount is the name of the CA engine mount to obtain certificates from,
|
||||
// e.g. "pki".
|
||||
Mount string
|
||||
}
|
||||
|
||||
// Client is the Metacrypt ACME client.
|
||||
// Create one with New, authenticate with Login, then use ObtainCertificate.
|
||||
type Client struct {
|
||||
cfg Config
|
||||
httpClient *http.Client
|
||||
token string // MCIAS bearer token
|
||||
tokenExp time.Time
|
||||
accountKey crypto.Signer
|
||||
acmeClient *acme.Client
|
||||
}
|
||||
|
||||
// New creates a new Metacrypt client with the given configuration.
|
||||
// Call Login before calling ObtainCertificate.
|
||||
func New(cfg Config) *Client {
|
||||
return &Client{
|
||||
cfg: cfg,
|
||||
httpClient: buildHTTPClient(cfg.CACertPath),
|
||||
}
|
||||
}
|
||||
|
||||
// Login authenticates with MCIAS and stores the bearer token.
|
||||
// totpCode may be empty if TOTP is not configured.
|
||||
func (c *Client) Login(ctx context.Context, username, password, totpCode string) error {
|
||||
token, exp, err := loginMCIAS(ctx, c.httpClient, c.cfg.MCIASURL, username, password, totpCode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("metacrypt: MCIAS login: %w", err)
|
||||
}
|
||||
c.token = token
|
||||
c.tokenExp = exp
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterAccount registers a new ACME account using EAB credentials fetched
|
||||
// from Metacrypt. If accountKey is nil, a fresh EC P-256 key is generated.
|
||||
//
|
||||
// This method is called automatically by ObtainCertificate if no account key
|
||||
// has been registered yet.
|
||||
func (c *Client) RegisterAccount(ctx context.Context, contact []string) error {
|
||||
if c.token == "" {
|
||||
return fmt.Errorf("metacrypt: not authenticated; call Login first")
|
||||
}
|
||||
|
||||
// Fetch EAB credentials from Metacrypt.
|
||||
kid, hmacKey, err := fetchEAB(ctx, c.httpClient, c.cfg.MetacryptURL, c.cfg.Mount, c.token)
|
||||
if err != nil {
|
||||
return fmt.Errorf("metacrypt: fetch EAB credentials: %w", err)
|
||||
}
|
||||
|
||||
// Generate account key if not provided.
|
||||
if c.accountKey == nil {
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("metacrypt: generate account key: %w", err)
|
||||
}
|
||||
c.accountKey = key
|
||||
}
|
||||
|
||||
c.acmeClient = &acme.Client{
|
||||
Key: c.accountKey,
|
||||
HTTPClient: c.httpClient,
|
||||
DirectoryURL: c.directoryURL(),
|
||||
}
|
||||
|
||||
// Register with EAB.
|
||||
hmacKeyB64 := base64.RawURLEncoding.EncodeToString(hmacKey)
|
||||
acct := &acme.Account{
|
||||
Contact: contact,
|
||||
ExternalAccountBinding: &acme.ExternalAccountBinding{KID: kid, Key: []byte(hmacKeyB64)},
|
||||
}
|
||||
if _, err := c.acmeClient.Register(ctx, acct, acme.AcceptTOS); err != nil {
|
||||
return fmt.Errorf("metacrypt: ACME account registration: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ObtainCertificate runs the full ACME flow to obtain a certificate for the
|
||||
// given request. It handles account registration (with EAB), order placement,
|
||||
// challenge fulfillment, finalization, and certificate download.
|
||||
func (c *Client) ObtainCertificate(ctx context.Context, req *CertificateRequest) (*Certificate, error) {
|
||||
if len(req.Domains) == 0 {
|
||||
return nil, fmt.Errorf("metacrypt: at least one domain is required")
|
||||
}
|
||||
if req.HTTP01 == nil && req.DNS01 == nil {
|
||||
return nil, fmt.Errorf("metacrypt: HTTP01 or DNS01 provider required")
|
||||
}
|
||||
|
||||
// Auto-register account if needed.
|
||||
if c.acmeClient == nil {
|
||||
if err := c.RegisterAccount(ctx, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Generate certificate key.
|
||||
certKey, err := generateKey(req.KeyType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("metacrypt: generate certificate key: %w", err)
|
||||
}
|
||||
|
||||
// Build ACME identifiers.
|
||||
authzIDs := make([]acme.AuthzID, len(req.Domains))
|
||||
for i, d := range req.Domains {
|
||||
authzIDs[i] = acme.AuthzID{Type: "dns", Value: d}
|
||||
}
|
||||
|
||||
// Place order.
|
||||
order, err := c.acmeClient.AuthorizeOrder(ctx, authzIDs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("metacrypt: place order: %w", err)
|
||||
}
|
||||
|
||||
// Fulfill authorizations.
|
||||
for _, authzURL := range order.AuthzURLs {
|
||||
authz, err := c.acmeClient.GetAuthorization(ctx, authzURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("metacrypt: get authorization: %w", err)
|
||||
}
|
||||
if authz.Status == acme.StatusValid {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := c.fulfillAuthorization(ctx, authz, req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for order to be ready.
|
||||
order, err = c.acmeClient.WaitOrder(ctx, order.URI)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("metacrypt: wait for order ready: %w", err)
|
||||
}
|
||||
|
||||
// Create and submit CSR.
|
||||
csr, err := buildCSR(certKey, req.Domains)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("metacrypt: build CSR: %w", err)
|
||||
}
|
||||
|
||||
// Finalize order and download certificate chain.
|
||||
certDER, _, err := c.acmeClient.CreateOrderCert(ctx, order.FinalizeURL, csr, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("metacrypt: finalize order: %w", err)
|
||||
}
|
||||
|
||||
certPEM := encodeCertChainPEM(certDER)
|
||||
keyPEM, err := encodeKeyPEM(certKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("metacrypt: encode key: %w", err)
|
||||
}
|
||||
|
||||
// Parse expiry from the leaf certificate.
|
||||
expiresAt, err := parseCertExpiry(certDER[0])
|
||||
if err != nil {
|
||||
expiresAt = time.Now().Add(90 * 24 * time.Hour) // fallback
|
||||
}
|
||||
|
||||
return &Certificate{
|
||||
CertPEM: certPEM,
|
||||
KeyPEM: keyPEM,
|
||||
ExpiresAt: expiresAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RenewIfNeeded checks whether the certificate expires within threshold and
|
||||
// re-runs ObtainCertificate if so. Returns the (possibly new) certificate and
|
||||
// whether renewal occurred.
|
||||
func (c *Client) RenewIfNeeded(ctx context.Context, cert *Certificate, threshold time.Duration, req *CertificateRequest) (*Certificate, bool, error) {
|
||||
if time.Until(cert.ExpiresAt) > threshold {
|
||||
return cert, false, nil
|
||||
}
|
||||
newCert, err := c.ObtainCertificate(ctx, req)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return newCert, true, nil
|
||||
}
|
||||
|
||||
// directoryURL returns the ACME directory URL for the configured mount.
|
||||
func (c *Client) directoryURL() string {
|
||||
return c.cfg.MetacryptURL + "/acme/" + c.cfg.Mount + "/directory"
|
||||
}
|
||||
|
||||
// fulfillAuthorization selects the appropriate challenge and fulfills it.
|
||||
func (c *Client) fulfillAuthorization(ctx context.Context, authz *acme.Authorization, req *CertificateRequest) error {
|
||||
// Prefer HTTP-01, then DNS-01.
|
||||
var selected *acme.Challenge
|
||||
for _, ch := range authz.Challenges {
|
||||
if ch.Type == "http-01" && req.HTTP01 != nil {
|
||||
selected = ch
|
||||
break
|
||||
}
|
||||
}
|
||||
if selected == nil {
|
||||
for _, ch := range authz.Challenges {
|
||||
if ch.Type == "dns-01" && req.DNS01 != nil {
|
||||
selected = ch
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if selected == nil {
|
||||
return fmt.Errorf("metacrypt: no supported challenge type for %s", authz.Identifier.Value)
|
||||
}
|
||||
|
||||
switch selected.Type {
|
||||
case "http-01":
|
||||
keyAuth, err := c.acmeClient.HTTP01ChallengeResponse(selected.Token)
|
||||
if err != nil {
|
||||
return fmt.Errorf("metacrypt: HTTP-01 key auth: %w", err)
|
||||
}
|
||||
path := c.acmeClient.HTTP01ChallengePath(selected.Token)
|
||||
if err := req.HTTP01.WriteFile(path, keyAuth); err != nil {
|
||||
return fmt.Errorf("metacrypt: HTTP-01 write file: %w", err)
|
||||
}
|
||||
defer req.HTTP01.RemoveFile(path)
|
||||
|
||||
case "dns-01":
|
||||
txt, err := c.acmeClient.DNS01ChallengeRecord(selected.Token)
|
||||
if err != nil {
|
||||
return fmt.Errorf("metacrypt: DNS-01 record: %w", err)
|
||||
}
|
||||
name := "_acme-challenge." + authz.Identifier.Value + "."
|
||||
if err := req.DNS01.SetRecord(name, txt); err != nil {
|
||||
return fmt.Errorf("metacrypt: DNS-01 set record: %w", err)
|
||||
}
|
||||
defer req.DNS01.RemoveRecord(name)
|
||||
if req.DNS01.PropagationWait > 0 {
|
||||
timer := time.NewTimer(req.DNS01.PropagationWait)
|
||||
select {
|
||||
case <-timer.C:
|
||||
case <-ctx.Done():
|
||||
timer.Stop()
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Notify server to validate.
|
||||
if _, err := c.acmeClient.Accept(ctx, selected); err != nil {
|
||||
return fmt.Errorf("metacrypt: accept challenge: %w", err)
|
||||
}
|
||||
|
||||
// Wait for authorization to be valid.
|
||||
if _, err := c.acmeClient.WaitAuthorization(ctx, authz.URI); err != nil {
|
||||
return fmt.Errorf("metacrypt: wait authorization: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
5
clients/go/go.mod
Normal file
5
clients/go/go.mod
Normal file
@@ -0,0 +1,5 @@
|
||||
module git.wntrmute.dev/kyle/metacrypt/clients/go
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require golang.org/x/crypto v0.49.0
|
||||
2
clients/go/go.sum
Normal file
2
clients/go/go.sum
Normal file
@@ -0,0 +1,2 @@
|
||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
167
clients/go/metacrypt.go
Normal file
167
clients/go/metacrypt.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package metacryptclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// loginMCIAS authenticates against the MCIAS server and returns a bearer token.
|
||||
func loginMCIAS(ctx context.Context, client *http.Client, mciasURL, username, password, totpCode string) (token string, exp time.Time, err error) {
|
||||
body, _ := json.Marshal(map[string]string{
|
||||
"username": username,
|
||||
"password": password,
|
||||
"totp_code": totpCode,
|
||||
})
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, mciasURL+"/v1/auth/login", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return "", time.Time{}, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", time.Time{}, fmt.Errorf("MCIAS login request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", time.Time{}, fmt.Errorf("MCIAS login: unexpected status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Token string `json:"token"`
|
||||
ExpiresAt string `json:"expires_at"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return "", time.Time{}, fmt.Errorf("MCIAS login: decode response: %w", err)
|
||||
}
|
||||
if result.Token == "" {
|
||||
return "", time.Time{}, fmt.Errorf("MCIAS login: empty token in response")
|
||||
}
|
||||
|
||||
var expTime time.Time
|
||||
if result.ExpiresAt != "" {
|
||||
expTime, _ = time.Parse(time.RFC3339, result.ExpiresAt)
|
||||
}
|
||||
return result.Token, expTime, nil
|
||||
}
|
||||
|
||||
// fetchEAB calls POST /v1/acme/{mount}/eab on Metacrypt and returns the EAB kid and HMAC key.
|
||||
func fetchEAB(ctx context.Context, client *http.Client, metacryptURL, mount, token string) (kid string, hmacKey []byte, err error) {
|
||||
url := metacryptURL + "/v1/acme/" + mount + "/eab"
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, http.NoBody)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("fetch EAB: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return "", nil, fmt.Errorf("fetch EAB: unexpected status %d: %s", resp.StatusCode, body)
|
||||
}
|
||||
|
||||
var result struct {
|
||||
KID string `json:"kid"`
|
||||
HMACKey []byte `json:"hmac_key"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return "", nil, fmt.Errorf("fetch EAB: decode response: %w", err)
|
||||
}
|
||||
return result.KID, result.HMACKey, nil
|
||||
}
|
||||
|
||||
// buildHTTPClient creates an HTTP client that optionally trusts a custom CA.
|
||||
func buildHTTPClient(caCertPath string) *http.Client {
|
||||
tlsCfg := &tls.Config{MinVersion: tls.VersionTLS12}
|
||||
|
||||
if caCertPath != "" {
|
||||
pool := x509.NewCertPool()
|
||||
if data, err := os.ReadFile(caCertPath); err == nil {
|
||||
pool.AppendCertsFromPEM(data)
|
||||
tlsCfg.RootCAs = pool
|
||||
}
|
||||
}
|
||||
|
||||
return &http.Client{
|
||||
Transport: &http.Transport{TLSClientConfig: tlsCfg},
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
// generateKey generates a certificate key of the requested type.
|
||||
// Supported types: "EC256" (default), "EC384", "RSA2048", "RSA4096".
|
||||
func generateKey(keyType string) (crypto.Signer, error) {
|
||||
switch keyType {
|
||||
case "", "EC256":
|
||||
return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
case "EC384":
|
||||
return ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
||||
case "RSA2048":
|
||||
return rsa.GenerateKey(rand.Reader, 2048)
|
||||
case "RSA4096":
|
||||
return rsa.GenerateKey(rand.Reader, 4096)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported key type: %s", keyType)
|
||||
}
|
||||
}
|
||||
|
||||
// buildCSR creates a DER-encoded certificate signing request.
|
||||
func buildCSR(key crypto.Signer, domains []string) ([]byte, error) {
|
||||
if len(domains) == 0 {
|
||||
return nil, fmt.Errorf("at least one domain required")
|
||||
}
|
||||
template := &x509.CertificateRequest{
|
||||
Subject: pkix.Name{CommonName: domains[0]},
|
||||
DNSNames: domains,
|
||||
}
|
||||
return x509.CreateCertificateRequest(rand.Reader, template, key)
|
||||
}
|
||||
|
||||
// encodeCertChainPEM encodes a chain of DER-encoded certificates to PEM.
|
||||
func encodeCertChainPEM(chain [][]byte) []byte {
|
||||
var buf bytes.Buffer
|
||||
for _, der := range chain {
|
||||
pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: der})
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// encodeKeyPEM encodes a private key to PEM (PKCS8 format).
|
||||
func encodeKeyPEM(key crypto.Signer) ([]byte, error) {
|
||||
der, err := x509.MarshalPKCS8PrivateKey(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: der}), nil
|
||||
}
|
||||
|
||||
// parseCertExpiry parses the expiry time from a DER-encoded certificate.
|
||||
func parseCertExpiry(der []byte) (time.Time, error) {
|
||||
cert, err := x509.ParseCertificate(der)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
return cert.NotAfter, nil
|
||||
}
|
||||
Reference in New Issue
Block a user