diff --git a/pkg/certmanager/acme.go b/pkg/certmanager/acme.go index 9475077cc..7708d2473 100644 --- a/pkg/certmanager/acme.go +++ b/pkg/certmanager/acme.go @@ -62,10 +62,22 @@ type ( // completed before the certificate can be issued or renewed. var ErrHTTPChallengeRequired = errors.New("HTTP challenge required") -func NewACMEService(email string, keyType keys.Type, directoryURL string, insecureTLS bool, logger *log.Logger) (*ACMEService, error) { - accountKey, err := keys.Generate(keyType) - if err != nil { - return nil, fmt.Errorf("cannot generate account key: %w", err) +func NewACMEService( + email string, + keyType keys.Type, + directoryURL string, + insecureTLS bool, + accountKey crypto.Signer, + logger *log.Logger, +) (*ACMEService, error) { + if accountKey == nil { + var err error + accountKey, err = keys.Generate(keyType) + if err != nil { + return nil, fmt.Errorf("cannot generate account key: %w", err) + } + + logger.Warn("no account key provided, generating new ACME account - this will create a new account on each restart") } var httpClient *http.Client @@ -78,6 +90,7 @@ func NewACMEService(email string, keyType keys.Type, directoryURL string, insecu Transport: transport, Timeout: 30 * time.Second, } + logger.Warn("ACME service configured with insecure TLS - use only for local testing") } else { httpClient = httpclient.DefaultPooledClient( @@ -225,7 +238,6 @@ func (s *ACMEService) ObtainCertificate( ctx context.Context, domain string, ) (*Certificate, error) { - // For HTTP-01, we always need to serve the challenge challenge, err := s.GetHTTPChallenge(ctx, domain) if err != nil { return nil, fmt.Errorf("cannot get HTTP challenge: %w", err) diff --git a/pkg/crypto/pem/pem.go b/pkg/crypto/pem/pem.go index 55dfc0231..9123b375a 100644 --- a/pkg/crypto/pem/pem.go +++ b/pkg/crypto/pem/pem.go @@ -12,7 +12,6 @@ // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR // PERFORMANCE OF THIS SOFTWARE. -// Package pem provides utilities for encoding certificates and keys in PEM format package pem import ( @@ -25,15 +24,21 @@ import ( "fmt" ) +const ( + BlockTypeCertificate = "CERTIFICATE" + BlockTypeECPrivateKey = "EC PRIVATE KEY" + BlockTypeRSAPrivateKey = "RSA PRIVATE KEY" + BlockTypePKCS8PrivateKey = "PRIVATE KEY" +) + func EncodeCertificate(der []byte) []byte { block := &pem.Block{ - Type: "CERTIFICATE", + Type: BlockTypeCertificate, Bytes: der, } return pem.EncodeToMemory(block) } -// EncodeCertificateChain encodes multiple DER-encoded certificates into a single PEM chain func EncodeCertificateChain(derCerts [][]byte) []byte { var chain []byte for _, der := range derCerts { @@ -53,17 +58,17 @@ func EncodePrivateKey(key crypto.Signer) ([]byte, error) { return nil, fmt.Errorf("cannot marshal EC private key: %w", err) } keyDER = der - keyType = "EC PRIVATE KEY" + keyType = BlockTypeECPrivateKey case *rsa.PrivateKey: keyDER = x509.MarshalPKCS1PrivateKey(k) - keyType = "RSA PRIVATE KEY" + keyType = BlockTypeRSAPrivateKey case ed25519.PrivateKey: der, err := x509.MarshalPKCS8PrivateKey(k) if err != nil { return nil, fmt.Errorf("cannot marshal ED25519 private key: %w", err) } keyDER = der - keyType = "PRIVATE KEY" + keyType = BlockTypePKCS8PrivateKey default: return nil, fmt.Errorf("unsupported key type: %T", key) } @@ -75,3 +80,29 @@ func EncodePrivateKey(key crypto.Signer) ([]byte, error) { return pem.EncodeToMemory(block), nil } + +func DecodePrivateKey(pemData []byte) (crypto.Signer, error) { + block, _ := pem.Decode(pemData) + if block == nil { + return nil, fmt.Errorf("cannot to decode PEM block") + } + + switch block.Type { + case BlockTypeECPrivateKey: + return x509.ParseECPrivateKey(block.Bytes) + case BlockTypeRSAPrivateKey: + return x509.ParsePKCS1PrivateKey(block.Bytes) + case BlockTypePKCS8PrivateKey: + key, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("cannot parse PKCS8 private key: %w", err) + } + signer, ok := key.(crypto.Signer) + if !ok { + return nil, fmt.Errorf("key is not a crypto.Signer") + } + return signer, nil + default: + return nil, fmt.Errorf("unsupported PEM block type: %s", block.Type) + } +} diff --git a/pkg/probod/custom_domains_config.go b/pkg/probod/custom_domains_config.go index 1f493fc88..09cc591bd 100644 --- a/pkg/probod/custom_domains_config.go +++ b/pkg/probod/custom_domains_config.go @@ -26,4 +26,5 @@ type acmeConfig struct { Email string `json:"email"` KeyType string `json:"key-type"` InsecureTLS bool `json:"insecure-tls"` + AccountKey string `json:"account-key"` } diff --git a/pkg/probod/probod.go b/pkg/probod/probod.go index d1df5b427..cf40f4d0e 100644 --- a/pkg/probod/probod.go +++ b/pkg/probod/probod.go @@ -16,6 +16,7 @@ package probod import ( "context" + "crypto" "crypto/tls" "errors" "fmt" @@ -33,6 +34,7 @@ import ( "github.com/getprobo/probo/pkg/crypto/cipher" "github.com/getprobo/probo/pkg/crypto/keys" "github.com/getprobo/probo/pkg/crypto/passwdhash" + "github.com/getprobo/probo/pkg/crypto/pem" "github.com/getprobo/probo/pkg/html2pdf" "github.com/getprobo/probo/pkg/mailer" "github.com/getprobo/probo/pkg/probo" @@ -256,18 +258,25 @@ func (impl *Implm) Run( return fmt.Errorf("cannot create usrmgr service: %w", err) } - var acmeService *certmanager.ACMEService - if impl.cfg.CustomDomains.ACME.Directory != "" { - acmeService, err = certmanager.NewACMEService( - impl.cfg.CustomDomains.ACME.Email, - keys.Type(impl.cfg.CustomDomains.ACME.KeyType), - impl.cfg.CustomDomains.ACME.Directory, - impl.cfg.CustomDomains.ACME.InsecureTLS, - l, - ) + var accountKey crypto.Signer + if impl.cfg.CustomDomains.ACME.AccountKey != "" { + accountKey, err = pem.DecodePrivateKey([]byte(impl.cfg.CustomDomains.ACME.AccountKey)) if err != nil { - return fmt.Errorf("failed to initialize ACME service: %w", err) + return fmt.Errorf("failed to decode ACME account key: %w", err) } + l.Info("using configured ACME account key") + } + + acmeService, err := certmanager.NewACMEService( + impl.cfg.CustomDomains.ACME.Email, + keys.Type(impl.cfg.CustomDomains.ACME.KeyType), + impl.cfg.CustomDomains.ACME.Directory, + impl.cfg.CustomDomains.ACME.InsecureTLS, + accountKey, + l, + ) + if err != nil { + return fmt.Errorf("failed to initialize ACME service: %w", err) } proboService, err := probo.NewService(