acme

package module
v1.0.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 5, 2025 License: Apache-2.0 Imports: 19 Imported by: 0

README

acme - Go ACME Client

A simplified and decoupled Let's Encrypt client for Go, based on ACME V2. This client aims to be framework-agnostic and returns certificate data directly without writing to filesystem or coupling to specific web servers.

Features

  • 🔒 DNS-01 validation only - Perfect for wildcard certificates
  • 🚀 Zero dependencies except hashicorp/go-retryablehttp for reliability
  • 🌍 Production ready - Built for high-scale, multi-tenant environments
  • 📦 Decoupled design - Returns certificate data, doesn't manage files
  • 🔄 Automatic retries - Robust HTTP client with exponential backoff
  • High performance - Optimized for concurrent certificate generation

Why This Package?

This package is extremely useful when you need to dynamically fetch and install certificates in:

  • Multi-tenant SaaS platforms - Generate certificates for customer domains
  • CDN/Edge deployments - Secure video delivery and API endpoints
  • Containerized environments - Certificate management without filesystem coupling
  • Wildcard certificates - *.yourdomain.com support via DNS validation
  • API integrations - Programmatic certificate lifecycle management

Almost all existing ACME clients are coupled to specific web servers or fixed domain sets. This client provides complete flexibility for dynamic certificate management.

Installation

go get github.com/mdsohelmia/acme

Requirements

  • Go 1.21+
  • DNS provider with API access (for TXT record creation)
  • Valid email address for Let's Encrypt account

Quick Start

Basic Usage
package main

import (
    "fmt"
    "log"

    "github.com/mdsohelmia/acme"
)

func main() {
    // Configure the client
    config := &acme.ClientConfig{
        Username:  "[email protected]",
        Mode:      acme.ModeStaging,  // Use ModeStaging for testing
        KeyLength: 4096,
        BasePath:  "data/le",
    }

    // Create client
    client, err := acme.NewClient(config)
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    // Create order for domains
    domains := []string{"*.yourdomain.com", "yourdomain.com"}
    order, err := client.CreateOrder(domains)
    if err != nil {
        log.Fatal(err)
    }

    // Get authorizations
    auths, err := client.Authorize(order)
    if err != nil {
        log.Fatal(err)
    }

    // Process DNS challenges
    for _, auth := range auths {
        txtRecord := auth.GetTxtRecord()
        if txtRecord == nil {
            continue
        }

        fmt.Printf("Create DNS TXT record:\n")
        fmt.Printf("Name: %s\n", txtRecord.GetName())
        fmt.Printf("Value: %s\n", txtRecord.GetValue())

        // Create the DNS record in your DNS provider
        // Wait for user confirmation or automate via DNS API

        // Validate the challenge
        if client.SelfTest(auth) {
            dnsChallenge := auth.GetDNSChallenge()
            client.Validate(dnsChallenge, 15)
        }
    }

    // Get certificate when ready
    if client.IsReady(order) {
        cert, err := client.GetCertificate(order)
        if err != nil {
            log.Fatal(err)
        }

        // Use the certificate data
        certPEM := cert.GetCertificate()        // Full chain
        keyPEM := cert.GetPrivateKey()          // Private key
        intermediatePEM := cert.GetIntermediate() // Intermediate only
    }
}

API Reference

Client Configuration
type ClientConfig struct {
    Username  string  // Email for Let's Encrypt account (required)
    Mode      string  // acme.ModeLive or acme.ModeStaging
    KeyLength int     // RSA key length (default: 4096)
    BasePath  string  // Directory for account storage (default: "le")
    SourceIP  string  // Optional: bind to specific IP
}

Modes:

  • acme.ModeStaging - Let's Encrypt staging environment (for testing)
  • acme.ModeLive - Let's Encrypt production environment
Client Methods
NewClient(config *ClientConfig) (*Client, error)

Creates a new ACME client and initializes the Let's Encrypt account.

CreateOrder(domains []string) (*Order, error)

Creates a new certificate order for the specified domains. Supports wildcards with DNS validation.

// Single domain
order, err := client.CreateOrder([]string{"example.com"})

// Multiple domains
order, err := client.CreateOrder([]string{"example.com", "www.example.com"})

// Wildcard (DNS validation required)
order, err := client.CreateOrder([]string{"*.example.com", "example.com"})
Authorize(order *Order) ([]*Authorization, error)

Retrieves authorizations for the order. Each domain gets one authorization with DNS challenges.

SelfTest(auth *Authorization) bool

Tests if the DNS TXT record is properly configured and propagated.

Validate(challenge *Challenge, maxAttempts int) bool

Submits the challenge to Let's Encrypt for validation.

IsReady(order *Order) bool

Checks if the order is ready for certificate generation.

GetCertificate(order *Order) (*Certificate, error)

Generates and retrieves the final certificate.

Data Types
Order

Represents a certificate order.

order.GetID() string              // Order ID
order.GetStatus() string          // Order status
order.GetDomains() []string       // Domains in order
order.GetExpiresAt() time.Time    // Order expiration
Authorization

Represents domain authorization.

auth.GetDomain() string                    // Domain being authorized
auth.GetTxtRecord() *Record               // DNS TXT record details
auth.GetDNSChallenge() *Challenge         // DNS challenge
auth.GetExpires() time.Time               // Authorization expiration
Record

DNS TXT record information.

record.GetName() string    // Record name (e.g., "_acme-challenge.example.com")
record.GetValue() string   // Record value (challenge token)
Certificate

Final certificate with all components.

cert.GetCertificate() string         // Full certificate chain
cert.GetCertificate(false) string    // Domain certificate only
cert.GetPrivateKey() string          // Private key
cert.GetIntermediate() string        // Intermediate certificate
cert.GetExpiryDate() time.Time       // Certificate expiration
cert.GetCSR() string                 // Certificate signing request

Advanced Usage

Production Configuration
config := &acme.ClientConfig{
    Username:  "[email protected]",
    Mode:      acme.ModeLive,        // Production
    KeyLength: 4096,
    BasePath:  "/etc/ssl/acme",      // Secure storage
}
Automated DNS Integration
func automatedCertificateGeneration(domains []string) (*acme.Certificate, error) {
    client, err := acme.NewClient(config)
    if err != nil {
        return nil, err
    }
    defer client.Close()

    order, err := client.CreateOrder(domains)
    if err != nil {
        return nil, err
    }

    auths, err := client.Authorize(order)
    if err != nil {
        return nil, err
    }

    // Automated DNS record creation
    for _, auth := range auths {
        txtRecord := auth.GetTxtRecord()

        // Create DNS record via your provider's API
        err := createDNSRecord(txtRecord.GetName(), txtRecord.GetValue())
        if err != nil {
            return nil, err
        }

        // Wait for DNS propagation
        if !waitForDNSPropagation(txtRecord, 300) {
            return nil, fmt.Errorf("DNS propagation timeout")
        }

        // Validate
        dnsChallenge := auth.GetDNSChallenge()
        if !client.Validate(dnsChallenge, 15) {
            return nil, fmt.Errorf("validation failed for %s", auth.GetDomain())
        }
    }

    // Get certificate
    return client.GetCertificate(order)
}
Concurrent Certificate Generation
func generateMultipleCertificates(domainSets [][]string) error {
    var wg sync.WaitGroup
    results := make(chan result, len(domainSets))

    for _, domains := range domainSets {
        wg.Add(1)
        go func(domains []string) {
            defer wg.Done()
            cert, err := automatedCertificateGeneration(domains)
            results <- result{domains: domains, cert: cert, err: err}
        }(domains)
    }

    wg.Wait()
    close(results)

    // Process results
    for result := range results {
        if result.err != nil {
            log.Printf("Failed to generate cert for %v: %v", result.domains, result.err)
        } else {
            deployCertificate(result.domains, result.cert)
        }
    }

    return nil
}
Certificate Deployment Examples
Nginx Deployment
func deployToNginx(cert *acme.Certificate, domains []string) error {
    // Save certificate files
    err := os.WriteFile("/etc/nginx/ssl/fullchain.crt",
        []byte(cert.GetCertificate()), 0644)
    if err != nil {
        return err
    }

    err = os.WriteFile("/etc/nginx/ssl/private.key",
        []byte(cert.GetPrivateKey()), 0600)
    if err != nil {
        return err
    }

    // Reload Nginx
    return exec.Command("nginx", "-s", "reload").Run()
}
CDN Deployment
func deployCDN(cert *acme.Certificate, domains []string) error {
    // Most CDNs want separate certificate and key
    domainCert := cert.GetCertificate(false)  // Domain cert only
    privateKey := cert.GetPrivateKey()
    intermediate := cert.GetIntermediate()

    return cdnAPI.UploadCertificate(domains[0], domainCert, privateKey, intermediate)
}
Load Balancer Deployment
func deployLoadBalancer(cert *acme.Certificate, domains []string) error {
    // Load balancers typically want the full chain
    fullChain := cert.GetCertificate(true)
    privateKey := cert.GetPrivateKey()

    return loadBalancerAPI.UpdateSSL(domains, fullChain, privateKey)
}

Error Handling

The client provides detailed error information:

cert, err := client.GetCertificate(order)
if err != nil {
    switch {
    case strings.Contains(err.Error(), "DNS"):
        log.Printf("DNS validation failed: %v", err)
        // Handle DNS issues
    case strings.Contains(err.Error(), "timeout"):
        log.Printf("Request timeout: %v", err)
        // Handle timeout issues
    default:
        log.Printf("Certificate generation failed: %v", err)
        // Handle other errors
    }
}

Best Practices

Security
  • Protect private keys: Store with 0600 permissions
  • Secure account storage: Use appropriate file permissions for BasePath
  • Certificate rotation: Renew certificates 30 days before expiration
  • Validate inputs: Always validate domain names before processing
Performance
  • Concurrent processing: Generate multiple certificates in parallel
  • DNS optimization: Cache DNS provider connections
  • Retry logic: The client includes automatic retries for reliability
  • Resource cleanup: Always call client.Close() when done
Monitoring
  • Certificate expiry: Monitor expiration dates
  • Validation failures: Log and alert on DNS validation issues
  • Rate limiting: Be aware of Let's Encrypt rate limits
  • Health checks: Verify certificate validity regularly

Rate Limits

Let's Encrypt has the following rate limits:

  • Certificates per Registered Domain: 50 per week
  • Duplicate Certificate: 5 per week
  • Failed Validations: 5 failures per account, per hostname, per hour
  • New Orders: 300 per account per 3 hours

Plan your certificate generation accordingly.

Examples

Multi-tenant SaaS Platform
func generateCustomerCertificate(customerDomain string) error {
    domains := []string{customerDomain, fmt.Sprintf("www.%s", customerDomain)}

    cert, err := automatedCertificateGeneration(domains)
    if err != nil {
        return err
    }

    // Deploy to customer's CDN edge
    return deployToCustomerCDN(customerDomain, cert)
}
Wildcard Certificate for Microservices
func generateWildcardCert(baseDomain string) error {
    domains := []string{
        fmt.Sprintf("*.%s", baseDomain),
        baseDomain,
    }

    cert, err := automatedCertificateGeneration(domains)
    if err != nil {
        return err
    }

    // Deploy to all microservices
    services := []string{"api", "auth", "cdn", "admin"}
    for _, service := range services {
        subdomain := fmt.Sprintf("%s.%s", service, baseDomain)
        deployToService(subdomain, cert)
    }

    return nil
}

Contributing

Contributions are welcome! Please ensure:

  • Tests pass for all changes
  • Documentation is updated
  • Code follows Go conventions
  • DNS validation works across providers

License

Apache 2.0 License - same as the original PHP yaac project.

Support


Built for high-performance, production-grade certificate management in Go.

Documentation

Index

Constants

View Source
const (
	// Live and staging URLs
	DirectoryLive    = "https://acme-v02.api.letsencrypt.org/directory"
	DirectoryStaging = "https://acme-staging-v02.api.letsencrypt.org/directory"

	// Modes
	ModeLive    = "live"
	ModeStaging = "staging"

	// Directory endpoints
	DirectoryNewAccount = "newAccount"
	DirectoryNewNonce   = "newNonce"
	DirectoryNewOrder   = "newOrder"

	// Validation types - DNS only
	ValidationDNS = "dns-01"
)

Variables

This section is empty.

Functions

This section is empty.

Types

type Account

type Account struct {
	// contains filtered or unexported fields
}

Account represents a Let's Encrypt account

func NewAccount

func NewAccount(contact []string, createdAt time.Time, isValid bool, accountURL string) *Account

NewAccount creates a new Account

func (*Account) GetAccountURL

func (a *Account) GetAccountURL() string

GetAccountURL returns the account URL

func (*Account) GetContact

func (a *Account) GetContact() []string

GetContact returns the contact information

func (*Account) GetCreatedAt

func (a *Account) GetCreatedAt() time.Time

GetCreatedAt returns the account creation date

func (*Account) GetID

func (a *Account) GetID() string

GetID returns the account ID from the URL

func (*Account) IsValid

func (a *Account) IsValid() bool

IsValid returns whether the account is valid

type AccountResponse

type AccountResponse struct {
	Contact   []string `json:"contact"`
	CreatedAt string   `json:"createdAt"`
	Status    string   `json:"status"`
}

AccountResponse represents the ACME account response

type Authorization

type Authorization struct {
	// contains filtered or unexported fields
}

Authorization represents an ACME authorization

func NewAuthorization

func NewAuthorization(domain, expires, digest string) (*Authorization, error)

NewAuthorization creates a new Authorization

func (*Authorization) AddChallenge

func (a *Authorization) AddChallenge(challenge *Challenge)

AddChallenge adds a challenge to the authorization

func (*Authorization) GetChallenges

func (a *Authorization) GetChallenges() []*Challenge

GetChallenges returns all challenges

func (*Authorization) GetDNSChallenge

func (a *Authorization) GetDNSChallenge() *Challenge

GetDNSChallenge returns the DNS challenge (only DNS validation supported)

func (*Authorization) GetDomain

func (a *Authorization) GetDomain() string

GetDomain returns the domain being authorized

func (*Authorization) GetExpires

func (a *Authorization) GetExpires() time.Time

GetExpires returns the expiry time

func (*Authorization) GetTxtRecord

func (a *Authorization) GetTxtRecord() *Record

GetTxtRecord returns the TXT record for DNS validation

type AuthorizationResponse

type AuthorizationResponse struct {
	Identifier map[string]string   `json:"identifier"`
	Status     string              `json:"status"`
	Expires    string              `json:"expires"`
	Challenges []ChallengeResponse `json:"challenges"`
}

AuthorizationResponse represents the ACME authorization response

type Certificate

type Certificate struct {
	// contains filtered or unexported fields
}

Certificate represents an issued certificate

func NewCertificate

func NewCertificate(privateKeyPEM, csr, chain string) (*Certificate, error)

NewCertificate creates a new Certificate

func (*Certificate) GetCSR

func (c *Certificate) GetCSR() string

GetCSR returns the certificate signing request

func (*Certificate) GetCertificate

func (c *Certificate) GetCertificate(asChain ...bool) string

GetCertificate returns the certificate, optionally as a chain

func (*Certificate) GetExpiryDate

func (c *Certificate) GetExpiryDate() time.Time

GetExpiryDate returns the certificate expiry date

func (*Certificate) GetIntermediate

func (c *Certificate) GetIntermediate() string

GetIntermediate returns the intermediate certificate

func (*Certificate) GetPrivateKey

func (c *Certificate) GetPrivateKey() string

GetPrivateKey returns the private key

type Challenge

type Challenge struct {
	// contains filtered or unexported fields
}

Challenge represents an ACME challenge

func NewChallenge

func NewChallenge(authorizationURL, challengeType, status, url, token string) *Challenge

NewChallenge creates a new Challenge

func (*Challenge) GetAuthorizationURL

func (c *Challenge) GetAuthorizationURL() string

GetAuthorizationURL returns the authorization URL

func (*Challenge) GetStatus

func (c *Challenge) GetStatus() string

GetStatus returns the challenge status

func (*Challenge) GetToken

func (c *Challenge) GetToken() string

GetToken returns the challenge token

func (*Challenge) GetType

func (c *Challenge) GetType() string

GetType returns the challenge type

func (*Challenge) GetURL

func (c *Challenge) GetURL() string

GetURL returns the challenge URL

type ChallengeResponse

type ChallengeResponse struct {
	Type   string `json:"type"`
	Status string `json:"status"`
	URL    string `json:"url"`
	Token  string `json:"token"`
}

ChallengeResponse represents the ACME challenge response

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client represents the ACME client

func NewClient

func NewClient(config *ClientConfig) (*Client, error)

NewClient creates a new ACME client

func (*Client) Authorize

func (c *Client) Authorize(order *Order) ([]*Authorization, error)

Authorize obtains authorizations for an order

func (*Client) Close

func (c *Client) Close() error

Close cleans up the client

func (*Client) CreateOrder

func (c *Client) CreateOrder(domains []string) (*Order, error)

CreateOrder creates a new ACME order

func (*Client) GetAccount

func (c *Client) GetAccount() (*Account, error)

GetAccount retrieves account information (public method)

func (*Client) GetCertificate

func (c *Client) GetCertificate(order *Order) (*Certificate, error)

GetCertificate retrieves the certificate for an order

func (*Client) GetOrder

func (c *Client) GetOrder(id string) (*Order, error)

GetOrder retrieves an existing order by ID (public method)

func (*Client) IsReady

func (c *Client) IsReady(order *Order) bool

IsReady checks if an order is ready for finalization

func (*Client) SelfTest

func (c *Client) SelfTest(authorization *Authorization) bool

SelfTest performs a DNS self-test for the authorization

func (*Client) SetMaxAttempts added in v1.0.2

func (c *Client) SetMaxAttempts(attempts int)

SetMaxAttempts sets the maximum number of attempts for a request

func (*Client) Validate

func (c *Client) Validate(challenge *Challenge, maxAttempts int) bool

Validate validates a DNS challenge

type ClientConfig

type ClientConfig struct {
	Username  string
	Mode      string
	KeyLength int
	BasePath  string
	SourceIP  string
}

ClientConfig holds configuration for the ACME client

type DirectoryResponse

type DirectoryResponse struct {
	NewAccount string `json:"newAccount"`
	NewNonce   string `json:"newNonce"`
	NewOrder   string `json:"newOrder"`
	RevokeCert string `json:"revokeCert"`
	KeyChange  string `json:"keyChange"`
}

DirectoryResponse represents the ACME directory structure

type JWK

type JWK struct {
	E   string `json:"e"`
	Kty string `json:"kty"`
	N   string `json:"n"`
}

JWK represents a JSON Web Key

type JWKHeader

type JWKHeader struct {
	Alg   string `json:"alg"`
	JWK   *JWK   `json:"jwk,omitempty"`
	KID   string `json:"kid,omitempty"`
	Nonce string `json:"nonce"`
	URL   string `json:"url"`
}

JWKHeader represents the JSON Web Key header

type JWS

type JWS struct {
	Protected string `json:"protected"`
	Payload   string `json:"payload"`
	Signature string `json:"signature"`
}

JWS represents a JSON Web Signature

type Order

type Order struct {
	// contains filtered or unexported fields
}

Order represents an ACME order

func NewOrder

func NewOrder(domains []string, url, status, expiresAt string, identifiers []map[string]string, authorizations []string, finalizeURL string) (*Order, error)

NewOrder creates a new Order

func (*Order) GetAuthorizationURLs

func (o *Order) GetAuthorizationURLs() []string

GetAuthorizationURLs returns the authorization URLs

func (*Order) GetDomains

func (o *Order) GetDomains() []string

GetDomains returns the domains

func (*Order) GetExpiresAt

func (o *Order) GetExpiresAt() time.Time

GetExpiresAt returns the expiry time

func (*Order) GetFinalizeURL

func (o *Order) GetFinalizeURL() string

GetFinalizeURL returns the finalize URL

func (*Order) GetID

func (o *Order) GetID() string

GetID returns the order ID

func (*Order) GetIdentifiers

func (o *Order) GetIdentifiers() []map[string]string

GetIdentifiers returns the identifiers

func (*Order) GetStatus

func (o *Order) GetStatus() string

GetStatus returns the order status

func (*Order) GetURL

func (o *Order) GetURL() string

GetURL returns the order URL

type OrderResponse

type OrderResponse struct {
	Status         string              `json:"status"`
	Expires        string              `json:"expires"`
	Identifiers    []map[string]string `json:"identifiers"`
	Authorizations []string            `json:"authorizations"`
	Finalize       string              `json:"finalize"`
	Certificate    string              `json:"certificate,omitempty"`
}

OrderResponse represents the ACME order response

type Record

type Record struct {
	// contains filtered or unexported fields
}

Record represents a DNS TXT record for DNS validation

func NewRecord

func NewRecord(name, value string) *Record

NewRecord creates a new Record

func (*Record) GetName

func (r *Record) GetName() string

GetName returns the record name

func (*Record) GetValue

func (r *Record) GetValue() string

GetValue returns the record value

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL