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
- Automatic Renewal: Certificates are renewed 30 days before expiry
- Staging Environment Support: Test with Let's Encrypt staging servers first
go get github.com/kintsdev/certypackage main
import (
"log"
"github.com/kintsdev/certy"
)
func main() {
// Create a new certificate manager
manager := certy.NewManager(
"[email protected]", // Your email for Let's Encrypt
"./certs", // Directory to store certificates
true, // Use staging environment first
)
// Issue a certificate for a domain
err := manager.IssueCert("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("[email protected]", "./certs", false)
// Create server with automatic certificate management
server := &http.Server{
Addr: ":8443",
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)
}),
}
// Wrap with ACME challenge handler for Let's Encrypt verification
handler := manager.HTTPHandler(server.Handler)
server.Handler = handler
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
}NewManager(email, location string, staging bool) *Manager- Create a new managerIssueCert(domain string) error- Issue a Let's Encrypt certificateAddCustomCert(domain, certData, keyData string) error- Add a custom certificateGetCert(hello *tls.ClientHelloInfo) (*tls.Certificate, error)- Get TLS certificate for domainHTTPHandler(fallback http.Handler) http.Handler- ACME challenge handlerGetAcmeFileData(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 *rsa.PrivateKey // Account private key
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/
├── 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
CERTY_EMAIL: Your email for Let's Encrypt (optional, can be set in code)CERTY_LOCATION: Certificate storage directory (optional, can be set in code)CERTY_STAGING: Use staging environment (optional, defaults to false)
- Staging: 300 new orders per 3 hours, 300 new registrations per 1 hour
- Production: 300 new orders per 3 hours, 300 new registrations per 3 hours
Always test with staging first!
- Private keys are stored with 0644 permissions (readable by owner only)
- Directories are created with 0755 permissions
- Account keys are RSA 4096-bit
- Certificate keys are ECDSA P-256
- Certificates expire after 88 days (renewed 30 days before expiry)
All methods return proper errors that should be checked:
err := manager.IssueCert("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 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.
This version includes several important fixes:
- Proper error handling: All errors are now properly returned and handled
- Thread safety: Added mutex protection for concurrent operations
- File path security: Using
filepath.Joinfor secure path construction - Resource cleanup: Proper file handle management and cleanup
- Logic fixes: Corrected certificate renewal logic and validation
- Code organization: Split large functions into smaller, testable functions
- Input validation: Added proper validation for all inputs
- Documentation: Comprehensive comments and examples
For issues and questions, please open an issue on GitHub.
