Go library providing certificate management capabilities for both server and client TLS configurations.
The library supports:
- Server certificate loading with manual reload via
ReloadCertificate()(e.g., on SIGHUP) - Client certificate loading for gRPC and TLS connections
- DNS-based SAN identity extraction with CN fallback (RFC 1123/6125)
- Flexible certificate fetching via majordomo service
- Thread-safe operations for concurrent access
This library is used by Attestant projects such as Vouch (Ethereum validator client) and Dirk (distributed remote keymanager) for certificate management in Ethereum staking infrastructure.
| Package | Description |
|---|---|
server/standard |
Server certificate manager with reload support |
client/standard |
Client certificate manager with optional CA pool |
credentials |
gRPC credential helpers (NewGRPCClientCredentials, NewServerTLSConfig) |
san |
X.509 SAN identity extraction and DNS name validation |
testing |
Pre-generated test certificates and mock majordomo |
- Go 1.25.5 or later
- go-majordomo for certificate fetching
- gRPC-Go (required by the
credentialspackage)
go-certmanager is a standard Go module which can be installed with:
go get github.com/attestantio/go-certmanagerCertificate data is fetched via go-majordomo, which supports pluggable "confidants" for files, HTTP endpoints, secret vaults, etc. You must create a majordomo service and pass it to certificate managers via WithMajordomo().
Setting up a file-based majordomo service:
import (
"github.com/wealdtech/go-majordomo"
fsc "github.com/wealdtech/go-majordomo/confidants/fs"
)
confidant, err := fsc.New(ctx)
if err != nil {
return err
}
majordomoSvc, err := majordomo.New(ctx,
majordomo.WithConfidants(map[string]majordomo.Confidant{"file": confidant}),
)
if err != nil {
return err
}
// Use majordomoSvc with certificate managers (see below)Certificate URIs follow the majordomo format: file:///path/to/cert.pem, https://vault.example.com/secret/cert, etc.
The server package provides certificate management for TLS servers with manual reloading capabilities. Use this for long-running services that need to reload certificates without restarting.
import servercert "github.com/attestantio/go-certmanager/server/standard"
certMgr, err := servercert.New(ctx,
servercert.WithMajordomo(majordomoSvc),
servercert.WithCertPEMURI("file:///path/to/server.crt"),
servercert.WithCertKeyURI("file:///path/to/server.key"),
servercert.WithLoadTimeout(30*time.Second), // Optional: timeout for certificate fetch operations
)
if err != nil {
return err
}
// Use in TLS server config
tlsConfig, err := certMgr.GetTLSConfig(ctx)
// Trigger reload (e.g., on SIGHUP)
if err := certMgr.ReloadCertificate(ctx); err != nil {
log.Warn().Err(err).Msg("Certificate reload failed")
}For peer-to-peer scenarios where the same certificate is used for both server and client roles, use GetClientTLSConfig() to get a static certificate config suitable for client connections:
// Use the same cert manager for client connections
clientTLSConfig, err := certMgr.GetClientTLSConfig(ctx)
conn, err := grpc.NewClient("peer:port",
grpc.WithTransportCredentials(credentials.NewTLS(clientTLSConfig)))This is useful for peer-to-peer communication where a single certificate serves both roles.
Important:
GetClientTLSConfig()returns a point-in-time snapshot — it will not reflect subsequentReloadCertificate()calls. After a SIGHUP reload, callers must re-fetch the client TLS config and re-establish connections, as gRPC does not support in-place credential replacement.
Recommended pattern for SIGHUP handlers:
// In your SIGHUP handler:
if err := certMgr.ReloadCertificate(ctx); err != nil {
log.Warn().Err(err).Msg("Certificate reload failed")
return
}
// Re-fetch client TLS config after successful reload
newClientTLSConfig, err := certMgr.GetClientTLSConfig(ctx)
if err != nil {
log.Error().Err(err).Msg("Failed to get updated client TLS config")
return
}
// gRPC does not support in-place credential replacement;
// close and re-establish the connection with the new config.
oldConn.Close()
conn, err = grpc.NewClient("peer:port",
grpc.WithTransportCredentials(credentials.NewTLS(newClientTLSConfig)))The client package provides certificate loading for client connections.
import clientcert "github.com/attestantio/go-certmanager/client/standard"
certMgr, err := clientcert.New(ctx,
clientcert.WithMajordomo(majordomoSvc),
clientcert.WithCertPEMURI("file:///path/to/client.crt"),
clientcert.WithCertKeyURI("file:///path/to/client.key"),
clientcert.WithCACertURI("file:///path/to/ca.crt"), // Optional: CA for server verification
clientcert.WithLoadTimeout(30*time.Second), // Optional: timeout for certificate fetch operations
)
if err != nil {
return err
}
// Get TLS config for client connections
tlsConfig, err := certMgr.GetTLSConfig(ctx)The credentials package provides helpers for setting up TLS in gRPC services.
Client credentials from a client certificate manager:
import (
clientcert "github.com/attestantio/go-certmanager/client/standard"
certcreds "github.com/attestantio/go-certmanager/credentials"
)
clientCertMgr, err := clientcert.New(ctx,
clientcert.WithMajordomo(majordomoSvc),
clientcert.WithCertPEMURI("file:///path/to/client.crt"),
clientcert.WithCertKeyURI("file:///path/to/client.key"),
)
if err != nil {
return err
}
creds, err := certcreds.NewGRPCClientCredentials(ctx, clientCertMgr)
if err != nil {
return err
}
conn, err := grpc.NewClient("server:9091", grpc.WithTransportCredentials(creds))Server TLS with mutual authentication (client certificate verification):
import (
servercert "github.com/attestantio/go-certmanager/server/standard"
certcreds "github.com/attestantio/go-certmanager/credentials"
grpccreds "google.golang.org/grpc/credentials"
)
serverCertMgr, err := servercert.New(ctx,
servercert.WithMajordomo(majordomoSvc),
servercert.WithCertPEMURI("file:///path/to/server.crt"),
servercert.WithCertKeyURI("file:///path/to/server.key"),
)
if err != nil {
return err
}
tlsCfg, err := certcreds.NewServerTLSConfig(ctx, serverCertMgr, caCertPEM)
if err != nil {
return err
}
grpcServer := grpc.NewServer(grpc.Creds(grpccreds.NewTLS(tlsCfg)))The san package extracts DNS-based identity from X.509 certificates with CN fallback. DNS names are validated against RFC 1123 and RFC 6125; invalid names are skipped.
import "github.com/attestantio/go-certmanager/san"
// Extract primary identity from certificate
identity, source := san.ExtractIdentity(cert)
// source indicates: IdentitySourceSANDNS, IdentitySourceCN, or IdentitySourceUnknown
// Convenience wrapper that returns just the identity string
name := san.IdentityString(cert)
// Extract all DNS Subject Alternative Names (returned as a CertificateSANs struct)
allSANs := san.ExtractAllSANs(cert)
// Access: allSANs.DNSNames
// Validate a DNS name against RFC 1123 and RFC 6125
if err := san.ValidateDNSName("example.com"); err != nil {
log.Error().Err(err).Msg("Invalid DNS name")
}Contributions are welcome. Please see CONTRIBUTING.md for guidelines.
Apache-2.0 - see LICENSE for the full text.