mirror of
https://codeberg.org/Codeberg/pages-server.git
synced 2025-04-25 06:16:58 +00:00
Implement simple version of automatic certificate issuance
This commit is contained in:
parent
7c70be21d7
commit
4494023086
7 changed files with 1061 additions and 23 deletions
271
certificates.go
271
certificates.go
|
@ -2,24 +2,169 @@ 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) {
|
||||
// TODO: check DNS name & get certificate from Let's Encrypt
|
||||
return FallbackCertificate(), nil
|
||||
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
|
||||
|
@ -44,6 +189,10 @@ func GetHSTSHeader(host []byte) string {
|
|||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -95,3 +244,121 @@ func FallbackCertificate() *tls.Certificate {
|
|||
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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue