diff --git a/cmd/web_acme.go b/cmd/web_acme.go index 857114ff902..db23e3acdf5 100644 --- a/cmd/web_acme.go +++ b/cmd/web_acme.go @@ -21,8 +21,19 @@ import ( "gitea.dev/modules/util" "github.com/caddyserver/certmagic" + "github.com/mholt/acmez/v3/acme" ) +func acmeExternalAccountBinding() (*acme.EAB, error) { + if setting.AcmeEABKID == "" && setting.AcmeEABHMAC == "" { + return nil, nil + } + if setting.AcmeEABKID == "" || setting.AcmeEABHMAC == "" { + return nil, errors.New("both ACME_EAB_KID and ACME_EAB_HMAC must be set") + } + return &acme.EAB{KeyID: setting.AcmeEABKID, MACKey: setting.AcmeEABHMAC}, nil +} + func getCARoot(path string) (*x509.CertPool, error) { r, err := os.ReadFile(path) if err != nil { @@ -66,6 +77,10 @@ func runACME(listenAddr string, m http.Handler) error { log.Warn("Failed to parse CA Root certificate, using default CA trust: %v", err) } } + externalAccount, err := acmeExternalAccountBinding() + if err != nil { + return err + } // FIXME: this path is not right, it uses "AppWorkPath" incorrectly, and writes the data into "AppWorkPath/https" // Ideally it should migrate to AppDataPath write to "AppDataPath/https" // And one more thing, no idea why we should set the global default variables here @@ -84,6 +99,7 @@ func runACME(listenAddr string, m http.Handler) error { Email: setting.AcmeEmail, Agreed: setting.AcmeTOS, Profile: setting.AcmeProfile, + ExternalAccount: externalAccount, DisableHTTPChallenge: !enableHTTPChallenge, DisableTLSALPNChallenge: !enableTLSALPNChallenge, ListenHost: setting.HTTPAddr, @@ -100,7 +116,7 @@ func runACME(listenAddr string, m http.Handler) error { // takes HTTPS down on restart (https://github.com/go-gitea/gitea/issues/38519). // Prefer keeping the existing cert and retrying renewals asynchronously. ctx := graceful.GetManager().ShutdownContext() - err := magic.ManageSync(ctx, []string{setting.AppDomain}) + err = magic.ManageSync(ctx, []string{setting.AppDomain}) if err != nil { cert, cacheErr := magic.CacheManagedCertificate(ctx, setting.AppDomain) if cacheErr != nil || cert.Expired() { diff --git a/cmd/web_acme_test.go b/cmd/web_acme_test.go new file mode 100644 index 00000000000..bceb15b63ff --- /dev/null +++ b/cmd/web_acme_test.go @@ -0,0 +1,32 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "testing" + + "gitea.dev/modules/setting" + "gitea.dev/modules/test" + + "github.com/mholt/acmez/v3/acme" + "github.com/stretchr/testify/assert" +) + +func TestAcmeExternalAccountBinding(t *testing.T) { + t.Cleanup(test.MockVariableValue(&setting.AcmeEABKID, "")) + t.Cleanup(test.MockVariableValue(&setting.AcmeEABHMAC, "")) + + binding, err := acmeExternalAccountBinding() + assert.NoError(t, err) + assert.Nil(t, binding) + + setting.AcmeEABKID = "kid" + _, err = acmeExternalAccountBinding() + assert.ErrorContains(t, err, "both ACME_EAB_KID and ACME_EAB_HMAC must be set") + + setting.AcmeEABHMAC = "hmac" + binding, err = acmeExternalAccountBinding() + assert.NoError(t, err) + assert.Equal(t, &acme.EAB{KeyID: "kid", MACKey: "hmac"}, binding) +} diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 2dcbf66c8d7..ab45e7e9a4a 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -264,6 +264,11 @@ ;; ACME profile to request from the CA (e.g. "shortlived" for raw-IP certificates) ;ACME_PROFILE = ;; +;; External account binding credentials; set both to enable EAB +;; ACME_EAB_HMAC should be a base64url-encoded MAC key +;ACME_EAB_KID = +;ACME_EAB_HMAC = +;; ;; ACME live directory (not to be confused with ACME directory URL: ACME_URL) ;; (Refer to caddy's ACME manager https://github.com/caddyserver/certmagic) ;ACME_DIRECTORY = https diff --git a/go.mod b/go.mod index 1d5fdd73bd0..7dc4d7118e8 100644 --- a/go.mod +++ b/go.mod @@ -68,6 +68,7 @@ require ( github.com/mattn/go-isatty v0.0.24 github.com/mattn/go-sqlite3 v1.14.52 github.com/meilisearch/meilisearch-go v0.36.3 + github.com/mholt/acmez/v3 v3.1.6 github.com/mholt/archives v0.1.5 github.com/microcosm-cc/bluemonday v1.0.27 github.com/microsoft/go-mssqldb v1.11.2 @@ -199,7 +200,6 @@ require ( github.com/markbates/going v1.0.3 // indirect github.com/mattn/go-colorable v0.1.15 // indirect github.com/mattn/go-runewidth v0.0.24 // indirect - github.com/mholt/acmez/v3 v3.1.6 // indirect github.com/miekg/dns v1.1.72 // indirect github.com/mikelolasagasti/xz v1.0.1 // indirect github.com/minio/crc64nvme v1.1.1 // indirect diff --git a/modules/setting/server.go b/modules/setting/server.go index 289db6a044c..b975f653a61 100644 --- a/modules/setting/server.go +++ b/modules/setting/server.go @@ -102,6 +102,8 @@ var ( AcmeEmail string AcmeURL string AcmeProfile string + AcmeEABKID string + AcmeEABHMAC string AcmeCARoot string SSLMinimumVersion string SSLMaximumVersion string @@ -144,6 +146,11 @@ func loadServerDomainAndURL(sec ConfigSection, protocol string) { AppDomain = appURL.Hostname() } +func loadAcmeEABFrom(sec ConfigSection) { + AcmeEABKID = sec.Key("ACME_EAB_KID").MustString("") + AcmeEABHMAC = sec.Key("ACME_EAB_HMAC").MustString("") +} + func loadServerFrom(rootCfg ConfigProvider) { sec := rootCfg.Section("server") AppName = rootCfg.Section("").Key("APP_NAME").MustString("Gitea: Git with a cup of tea") @@ -173,6 +180,7 @@ func loadServerFrom(rootCfg ConfigProvider) { if EnableAcme { AcmeURL = sec.Key("ACME_URL").MustString("") AcmeProfile = sec.Key("ACME_PROFILE").MustString("") + loadAcmeEABFrom(sec) AcmeCARoot = sec.Key("ACME_CA_ROOT").MustString("") if sec.HasKey("ACME_ACCEPTTOS") { @@ -208,6 +216,7 @@ func loadServerFrom(rootCfg ConfigProvider) { KeyFile = filepath.Join(CustomPath, KeyFile) } } + SSLMinimumVersion = sec.Key("SSL_MIN_VERSION").MustString("") SSLMaximumVersion = sec.Key("SSL_MAX_VERSION").MustString("") SSLCurvePreferences = sec.Key("SSL_CURVE_PREFERENCES").Strings(",") diff --git a/modules/setting/server_test.go b/modules/setting/server_test.go new file mode 100644 index 00000000000..a855baaddb9 --- /dev/null +++ b/modules/setting/server_test.go @@ -0,0 +1,29 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "testing" + + "gitea.dev/modules/test" + + "github.com/stretchr/testify/assert" +) + +func TestLoadAcmeEABFrom(t *testing.T) { + t.Cleanup(test.MockVariableValue(&AcmeEABKID, "")) + t.Cleanup(test.MockVariableValue(&AcmeEABHMAC, "")) + + cfg, err := NewConfigProviderFromData(` +[server] +ACME_EAB_KID = kid +ACME_EAB_HMAC = hmac +`) + assert.NoError(t, err) + + loadAcmeEABFrom(cfg.Section("server")) + + assert.Equal(t, "kid", AcmeEABKID) + assert.Equal(t, "hmac", AcmeEABHMAC) +}