Certy is a Go library for managing Let's Encrypt SSL/TLS certificates with automatic renewal and custom certificate support.
- Automatic Let's Encrypt Certificate Management: Issue and renew SSL certificates automatically
- HTTP-01 Challenge Support: Built-in ACME challenge handler for domain verification
- Custom Certificate Support: Add your own certificates alongside Let's Encrypt ones
- Thread-Safe Operations: Concurrent certificate issuance with proper locking
- Renewal:
IssueCertrenews certificates within 30 days of expiry and is a no-op otherwise, so it can be called periodically - Staging Environment Support: Test with Let's Encrypt staging servers first
go get github.com/kintsdev/certypackage main
import (
"context"
"log"
"github.com/kintsdev/certy"
)
func main() {
// Create a new certificate manager
manager := certy.NewManager(
"your-email@example.com", // Your email for Let's Encrypt
"./certs", // Directory to store certificates
true, // Use staging environment first
)
// Issue a certificate for a domain.
// HTTPHandler must be serving port 80 for the domain while this runs.
err := manager.IssueCert(context.Background(), "example.com")
if err != nil {
log.Fatalf("Failed to issue certificate: %v", err)
}
}package main
import (
"crypto/tls"
"fmt"
"log"
"net/http"
"github.com/kintsdev/certy"
)
func main() {
manager := certy.NewManager("your-email@example.com", "./certs", false)
// HTTP-01 challenges are validated over plain HTTP on port 80
go func() {
log.Fatal(http.ListenAndServe(":80", manager.HTTPHandler(nil)))
}()
server := &http.Server{
Addr: ":443",
TLSConfig: &tls.Config{
GetCertificate: manager.GetCert, // Automatically selects certificates
},
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello from %s!", r.Host)
}),
}
log.Fatal(server.ListenAndServeTLS("", ""))
}// Add a custom certificate
err := manager.AddCustomCert(
"custom.example.com",
certPEMData,
keyPEMData,
)
if err != nil {
log.Printf("Failed to add custom certificate: %v", err)
}The main struct for managing certificates.
type Manager struct {
Email string // Email for Let's Encrypt account
Location string // Directory to store certificates
Staging bool // Use staging environment
Logger Logger // Optional, anything with Printf (e.g. *log.Logger)
}NewManager(email, location string, staging bool) *Manager- Create a new managerIssueCert(ctx context.Context, domain string) error- Issue or renew a Let's Encrypt certificate (no-op if still valid)AddCustomCert(domain, certData, keyData string) error- Add a custom certificate (key must match the certificate)GetCert(hello *tls.ClientHelloInfo) (*tls.Certificate, error)- Get TLS certificate for domain (use astls.Config.GetCertificate)InvalidateCertCache(domain string)- Force the nextGetCertto reload from diskHTTPHandler(fallback http.Handler) http.Handler- ACME challenge handler for port 80 (nil fallback responds 404)GetAcmeFileData(domain string) (*DomainAcme, error)- Get ACME data for domainGetChallengeToken(domain string) (string, error)- Get challenge token for domain
Represents certificate data for a domain.
type DomainAcme struct {
Sans []string // Subject Alternative Names
IssuerData IssuerData // Let's Encrypt issuer information
AccountKey rsaPrivateKeyJSON // Account private key (embeds *rsa.PrivateKey)
CertFile string // Certificate file path
KeyFile string // Private key file path
ExpireDate time.Time // Certificate expiry date
IssueDate time.Time // Certificate issue date
CustomCert bool // Whether this is a custom certificate
}RenewRequired() bool- Check if renewal is needed (30 days before expiry)Expired() bool- Check if certificate is expiredIsNull() bool- Check if ACME data is empty
Certy creates the following directory structure:
certs/
├── account-key.pem # Let's Encrypt account key, shared by all domains
├── example.com/
│ ├── example.com-acme.json # ACME data and metadata
│ ├── example.com-cert.crt # Certificate chain
│ └── example.com-key.pem # Private key
└── another-domain.com/
├── another-domain.com-acme.json
├── another-domain.com-cert.crt
└── another-domain.com-key.pem
Let's Encrypt enforces rate limits. Certy reuses a single ACME account for all domains to stay well within the account creation limits. Always test with staging first!
- Private keys, the account key and
*-acme.json(which contains the account key) are stored with 0600 permissions - Certificates are stored with 0644 permissions, directories with 0755
- Files are written atomically (temp file + rename), so a crash never leaves a partial file
- Account keys are RSA 4096-bit
- Certificate keys are ECDSA P-256
- Certificates are renewed 30 days before expiry
All methods return proper errors that should be checked:
err := manager.IssueCert(ctx, "example.com")
if err != nil {
switch {
case strings.Contains(err.Error(), "already in progress"):
log.Println("Certificate issuance already in progress")
case strings.Contains(err.Error(), "challenge validation failed"):
log.Println("Domain verification failed")
default:
log.Printf("Unexpected error: %v", err)
}
}Run the test suite:
go test ./...See the example/ directory for complete working examples.
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests for new functionality
- Ensure all tests pass
- Submit a pull request
This project is licensed under the MIT License - see the LICENSE file for details.
For issues and questions, please open an issue on GitHub.
