tildepages/certificates.go

365 lines
9.8 KiB
Go

package main
import (
"bytes"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"github.com/OrlovEvgeny/go-mcache"
"github.com/akrylysov/pogreb/fs"
"github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/challenge/tlsalpn01"
"github.com/go-acme/lego/v4/providers/dns"
"log"
"math/big"
"os"
"strings"
"time"
"github.com/akrylysov/pogreb"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/registration"
)
// tlsConfig contains the configuration for generating, serving and cleaning up Let's Encrypt certificates.
var tlsConfig = &tls.Config{
// check DNS name & get certificate from Let's Encrypt
GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
if os.Getenv("ACME_ACCEPT_TERMS") != "true" {
return FallbackCertificate(), nil
}
sni := strings.ToLower(strings.TrimSpace(info.ServerName))
sniBytes := []byte(sni)
if len(sni) < 1 {
return nil, errors.New("missing sni")
}
if info.SupportedProtos != nil {
for _, proto := range info.SupportedProtos {
if proto == tlsalpn01.ACMETLS1Protocol {
challenge, ok := challengeCache.Get(sni)
if !ok {
return nil, errors.New("no challenge for this domain")
}
cert, err := tlsalpn01.ChallengeCert(sni, challenge.(string))
if err != nil {
return nil, err
}
return cert, nil
}
}
}
targetOwner := ""
if bytes.HasSuffix(sniBytes, MainDomainSuffix) {
// deliver default certificate for the main domain (*.codeberg.page)
sniBytes = MainDomainSuffix
sni = string(sniBytes)
} else {
var targetRepo, targetBranch string
targetOwner, targetRepo, targetBranch = getTargetFromDNS(sni)
if targetOwner == "" {
// DNS not set up, return a self-signed certificate to redirect to the docs
return FallbackCertificate(), nil
}
// TODO: use .domains file to list all domains, to keep users from getting rate-limited
_, _ = targetRepo, targetBranch
/*canonicalDomain := checkCanonicalDomain(targetOwner, targetRepo, targetBranch)
if sni != canonicalDomain {
return FallbackCertificate(), nil
}*/
}
// limit users to 1 certificate per week
var cert, key []byte
if tlsCertificate, ok := keyCache.Get(sni); ok {
// we can use an existing certificate object
return tlsCertificate.(*tls.Certificate), nil
} else if ok, err := keyDatabase.Has(sniBytes); err != nil {
// key database is not working
panic(err)
} else if ok {
// parse certificate from database
cert, err = keyDatabase.Get(sniBytes)
if err != nil {
// key database is not working
panic(err)
}
key, err = keyDatabase.Get(append(sniBytes, '/', 'k', 'e', 'y'))
if err != nil {
// key database is not working or key doesn't exist
panic(err)
}
} else {
// request a new certificate
// TODO: rate-limit certificates per owner
// LE Rate Limits:
// - 300 new orders per account per 3 hours
// - 20 requests per second
// - 10 Accounts per IP per 3 hours
if bytes.Equal(sniBytes, MainDomainSuffix) {
return nil, errors.New("won't request certificate for main domain, something really bad has happened")
}
log.Printf("Requesting new certificate for %s", sni)
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, err
}
key = x509.MarshalPKCS1PrivateKey(privateKey)
res, err := acmeClient.Certificate.Obtain(certificate.ObtainRequest{
Domains: []string{sni},
PrivateKey: key,
Bundle: true,
MustStaple: true,
})
if err != nil {
return nil, err
}
log.Printf("Obtained certificate for %s", sni)
err = keyDatabase.Put(append(sniBytes, '/', 'k', 'e', 'y'), key)
if err != nil {
return nil, err
}
err = keyDatabase.Put(sniBytes, res.Certificate)
if err != nil {
_ = keyDatabase.Delete(append(sniBytes, '/', 'k', 'e', 'y'))
return nil, err
}
cert = res.Certificate
}
tlsCertificate, err := tls.X509KeyPair(pem.EncodeToMemory(&pem.Block{
Bytes: cert,
Type: "CERTIFICATE",
}), pem.EncodeToMemory(&pem.Block{
Bytes: key,
Type: "RSA PRIVATE KEY",
}))
if err != nil {
panic(err)
}
err = keyCache.Set(sni, &tlsCertificate, 15 * time.Minute)
if err != nil {
panic(err)
}
return &tlsCertificate, nil
},
PreferServerCipherSuites: true,
NextProtos: []string{
tlsalpn01.ACMETLS1Protocol,
},
// generated 2021-07-13, Mozilla Guideline v5.6, Go 1.14.4, intermediate configuration
// https://ssl-config.mozilla.org/#server=go&version=1.14.4&config=intermediate&guideline=5.6
MinVersion: tls.VersionTLS12,
CipherSuites: []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
},
}
// GetHSTSHeader returns a HSTS header with includeSubdomains & preload for MainDomainSuffix and RawDomain, or an empty
// string for custom domains.
func GetHSTSHeader(host []byte) string {
if bytes.HasSuffix(host, MainDomainSuffix) || bytes.Equal(host, RawDomain) {
return "max-age=63072000; includeSubdomains; preload"
} else {
return ""
}
}
var challengeCache = mcache.New()
var keyCache = mcache.New()
var keyDatabase *pogreb.DB
var fallbackCertificate *tls.Certificate
// FallbackCertificate generates a new self-signed TLS certificate on demand.
func FallbackCertificate() *tls.Certificate {
if fallbackCertificate != nil {
return fallbackCertificate
}
fallbackSerial, err := rand.Int(rand.Reader, (&big.Int{}).Lsh(big.NewInt(1), 159))
if err != nil {
panic(err)
}
fallbackCertKey, err := rsa.GenerateKey(rand.Reader, 1024)
if err != nil {
panic(err)
}
fallbackCertSpecification := &x509.Certificate{
Subject: pkix.Name{
CommonName: strings.TrimPrefix(string(MainDomainSuffix), "."),
},
SerialNumber: fallbackSerial,
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(100, 0, 0),
}
fallbackCertBytes, err := x509.CreateCertificate(
rand.Reader,
fallbackCertSpecification,
fallbackCertSpecification,
fallbackCertKey.Public(),
fallbackCertKey,
)
if err != nil {
panic(err)
}
fallbackCert, err := tls.X509KeyPair(pem.EncodeToMemory(&pem.Block{
Bytes: fallbackCertBytes,
Type: "CERTIFICATE",
}), pem.EncodeToMemory(&pem.Block{
Bytes: x509.MarshalPKCS1PrivateKey(fallbackCertKey),
Type: "RSA PRIVATE KEY",
}))
if err != nil {
panic(err)
}
fallbackCertificate = &fallbackCert
return fallbackCertificate
}
type AcmeAccount struct {
Email string
Registration *registration.Resource
key crypto.PrivateKey
}
func (u *AcmeAccount) GetEmail() string {
return u.Email
}
func (u AcmeAccount) GetRegistration() *registration.Resource {
return u.Registration
}
func (u *AcmeAccount) GetPrivateKey() crypto.PrivateKey {
return u.key
}
var acmeClient *lego.Client
type AcmeTLSChallengeProvider struct{}
var _ challenge.Provider = AcmeTLSChallengeProvider{}
func (a AcmeTLSChallengeProvider) Present(domain, _, keyAuth string) error {
return challengeCache.Set(domain, keyAuth, 1*time.Hour)
}
func (a AcmeTLSChallengeProvider) CleanUp(domain, _, _ string) error {
challengeCache.Remove(domain)
return nil
}
func init() {
FallbackCertificate()
var err error
keyDatabase, err = pogreb.Open("key-database.pogreb", &pogreb.Options{
BackgroundSyncInterval: 30 * time.Second,
BackgroundCompactionInterval: 6 * time.Hour,
FileSystem: fs.OSMMap,
})
if err != nil {
panic(err)
}
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
panic(err)
}
myUser := AcmeAccount{
Email: "",
key: privateKey,
}
config := lego.NewConfig(&myUser)
config.CADirURL = envOr("ACME_API", "https://acme-v02.api.letsencrypt.org/directory")
config.Certificate.KeyType = certcrypto.RSA2048
acmeClient, err = lego.NewClient(config)
if err != nil {
panic(err)
}
err = acmeClient.Challenge.SetTLSALPN01Provider(AcmeTLSChallengeProvider{})
if err != nil {
panic(err)
}
// accept terms
if os.Getenv("ACME_ACCEPT_TERMS") == "true" {
reg, err := acmeClient.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: os.Getenv("ACME_ACCEPT_TERMS") == "true"})
if err != nil {
panic(err)
}
myUser.Registration = reg
} else {
log.Printf("Warning: not using ACME certificates as ACME_ACCEPT_TERMS is false!")
}
// generate certificate for main domain
if os.Getenv("ACME_ACCEPT_TERMS") != "true" || os.Getenv("DNS_PROVIDER") == "" {
err = keyCache.Set(string(MainDomainSuffix), FallbackCertificate(), mcache.TTL_FOREVER)
if err != nil {
panic(err)
}
} else {
log.Printf("Requesting new certificate for *%s", MainDomainSuffix)
dnsAcmeClient, err := lego.NewClient(config)
if err != nil {
panic(err)
}
provider, err := dns.NewDNSChallengeProviderByName(os.Getenv("DNS_PROVIDER"))
if err != nil {
panic(err)
}
err = dnsAcmeClient.Challenge.SetDNS01Provider(provider)
if err != nil {
panic(err)
}
mainPrivateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
panic(err)
}
mainKey := x509.MarshalPKCS1PrivateKey(mainPrivateKey)
res, err := acmeClient.Certificate.Obtain(certificate.ObtainRequest{
Domains: []string{"*" + string(MainDomainSuffix), string(MainDomainSuffix[1:])},
PrivateKey: mainKey,
Bundle: true,
MustStaple: true,
})
if err != nil {
panic(err)
}
err = keyDatabase.Put(append(MainDomainSuffix, '/', 'k', 'e', 'y'), mainKey)
if err != nil {
panic(err)
}
err = keyDatabase.Put(MainDomainSuffix, res.Certificate)
if err != nil {
_ = keyDatabase.Delete(append(MainDomainSuffix, '/', 'k', 'e', 'y'))
panic(err)
}
}
}
// TODO: renew & revoke