-
Notifications
You must be signed in to change notification settings - Fork 1
/
cert.go
197 lines (166 loc) · 6.37 KB
/
cert.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
/**
* Cthulhu System
*
* Copyright (C) 2024 Linus Ilian Moser <linus.moser@megakuul.ch>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package cert
import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"encoding/json"
"fmt"
"os"
"strings"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/providers/dns"
"github.com/go-acme/lego/v4/registration"
"github.com/joho/godotenv"
"github.com/megakuul/cthulhu/shared/dbadapter/go"
"github.com/megakuul/cthulhu/shared/metaconfig/go"
cterror "github.com/megakuul/cthulhu/shared/util/error/go"
)
// CertificateProvider is a component that is used to request certificates
// from a acme provider with dns01 verification.
type CertificateProvider struct {
client *lego.Client
}
// Holds default values for MetaConfig options used.
type CertificateProviderDefaultOptions struct {
// Directory url of the ACME server (https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.1)
ACME_DIRECTORY_URL string
// Key type for certificates generated by the ACME server.
ACME_CERTIFICATE_KEY_TYPE string
// Email used for account registration on the ACME server.
ACME_REGISTRATION_EMAIL string
// DNS provider name used for dns01 verification (https://go-acme.github.io/lego/dns).
ACME_DNS_PROVIDER string
// .env file injected. Holds the dns credentials (https://go-acme.github.io/lego/dns).
ACME_DNS_ENV_FILE string
}
// Create new certificate provider.
// This will setup the connection to the acme & dns provider.
// The acme client is configured to use dns01 as verification.
//
// adapter: adapter that should be used for communication with the database.
// metaconfig: metaconfig used to acquire configuration options.
// defOpts: default options for used values, if they are not present in metaconfig.
// returns certificate provider and an error if setup failed.
func NewCertificateProvider(
adapter *dbadapter.DatabaseAdapter,
metaconfig *metaconfig.MetaConfig,
defOpts CertificateProviderDefaultOptions) (*CertificateProvider, error) {
hostname, err := os.Hostname()
if err!=nil {
return nil, cterror.Errorf("CERT", "Failed to acquire hostname: "+err.Error())
}
userPrefix := fmt.Sprintf("RUNE_CERT_ACME_USER_%s", strings.ToUpper(hostname))
userEncoded, err := adapter.Basic.GetField(userPrefix)
if err!=nil {
return nil, cterror.Errorf("CERT", "Failed to fetch ACME user data: "+err.Error())
}
isRegistered := false
var user acmeUser
if userEncoded!=nil && json.Unmarshal([]byte(*userEncoded), &user)==nil {
isRegistered = true
}
keyType, err := getKeyType(
metaconfig.GetString("ACME_CERTIFICATE_KEY_TYPE", defOpts.ACME_CERTIFICATE_KEY_TYPE))
if err!=nil {
return nil, cterror.Errorf("CERT", "Failed to process ACME_CERTIFICATE_KEY_TYPE: ", err.Error())
}
config := lego.NewConfig(&user)
config.CADirURL = metaconfig.GetString("ACME_DIRECTORY_URL", defOpts.ACME_DIRECTORY_URL)
config.Certificate.KeyType = keyType
client, err := lego.NewClient(config)
if err!=nil {
return nil, cterror.Errorf("CERT", "Failed to create lego client: "+err.Error())
}
if dnsEnvPath := metaconfig.GetString("ACME_DNS_ENV_FILE", defOpts.ACME_DNS_ENV_FILE); dnsEnvPath!="" {
if godotenv.Load(dnsEnvPath); err!=nil {
return nil, cterror.Errorf("CERT", "Failed to load ACME_DNS_ENV_FILE: "+err.Error())
}
}
dnsProvider, err := dns.NewDNSChallengeProviderByName(
metaconfig.GetString("ACME_DNS_PROVIDER", defOpts.ACME_DNS_PROVIDER))
if err!=nil {
return nil, cterror.Errorf("CERT", "Failed to find ACME_DNS_PROVIDER: "+err.Error())
}
client.Challenge.SetDNS01Provider(dnsProvider)
if !isRegistered {
reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
if err!=nil {
return nil, cterror.Errorf("CERT", "Failed to register ACME user: "+err.Error())
}
privateKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
if err!=nil {
return nil, cterror.Errorf("CERT", "Failed to generate ACME user private key: "+err.Error())
}
user.email = metaconfig.GetString("ACME_REGISTRATION_EMAIL", defOpts.ACME_REGISTRATION_EMAIL)
user.registration = reg
user.key = privateKey
userEncoded, err := json.Marshal(user)
if err!=nil {
return nil, cterror.Errorf("CERT", "Failed to encode ACME user data: "+err.Error())
}
err = adapter.Basic.SetField(userPrefix, string(userEncoded), 0)
if err!=nil {
return nil, cterror.Errorf("CERT", "Failed to send ACME user data to database: "+err.Error())
}
}
return &CertificateProvider{
client: client,
}, nil
}
// Requests certificate pair from the acme server.
//
// domains: List of domains that will be in the SAN block of the certificate.
// returns bundled certificate chain, private key and an error if the request failed.
func (c* CertificateProvider) RequestCertificate(domains []string) ([]byte, []byte, error) {
request := certificate.ObtainRequest{
Domains: domains,
Bundle: true,
}
certificates, err := c.client.Certificate.Obtain(request)
if err!=nil {
return nil, nil, cterror.Errorf("CERT", "Failed to obtain certificate: "+err.Error())
}
return certificates.Certificate, certificates.PrivateKey, nil
}
func getKeyType(name string) (certcrypto.KeyType, error) {
switch strings.ToUpper(name) {
case "RSA2048":
return certcrypto.RSA2048, nil
case "RSA3072":
return certcrypto.RSA3072, nil
case "RSA4096":
return certcrypto.RSA4096, nil
case "RSA8192":
return certcrypto.RSA8192, nil
case "EC256":
return certcrypto.EC256, nil
case "EC384":
return certcrypto.EC384, nil
}
return "", cterror.Errorf("CERT",
fmt.Sprintf("Invalid or unsupported key type '%s'. Key types supported are [%s].", name,
"RSA2048, RSA3072, RSA4096, RSA8192, EC256, EC384"))
}