-
Notifications
You must be signed in to change notification settings - Fork 42
Expand file tree
/
Copy pathssl.go
More file actions
384 lines (341 loc) · 11.8 KB
/
Copy pathssl.go
File metadata and controls
384 lines (341 loc) · 11.8 KB
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
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
// SPDX-FileCopyrightText: 2026 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0
package ssl
import (
"bytes"
"errors"
"fmt"
"io"
"os"
"os/exec"
"strings"
"time"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
. "github.com/uyuni-project/uyuni-tools/shared/l10n"
"github.com/uyuni-project/uyuni-tools/shared/types"
"github.com/uyuni-project/uyuni-tools/shared/utils"
)
// ! Any changes below needs to be double checked against upgrade/migration scenario !
const (
// CAContainerPath is the path to the Root CA certificate in the server container.
CAContainerPath = "/etc/pki/trust/anchors/LOCAL-RHN-ORG-TRUSTED-SSL-CERT"
// DBCAContainerPath is the path to the DB Root CA certificate in the server container.
DBCAContainerPath = "/etc/pki/trust/anchors/DB-RHN-ORG-TRUSTED-SSL-CERT"
// ServerCertPath is the path to the server certificate in the server container.
ServerCertPath = "/etc/pki/tls/certs/spacewalk.crt"
// ServerCertKeyPath is the path to the server certificate key in the server container.
ServerCertKeyPath = "/etc/pki/tls/private/spacewalk.key"
// DBCertPath is the path to the database certificate in the database container.
DBCertPath = "/etc/pki/tls/certs/spacewalk.crt"
// DBCertKeyPaht is the path to the database certificate in the database container.
DBCertKeyPath = "/etc/pki/tls/private/pg-spacewalk.key"
)
// OrderCas generates the server certificate with the CA chain.
//
// Returns the certificate chain and the root CA.
func OrderCas(chain *types.CaChain, serverPair *types.SSLPair) (orderedCert []byte, rootCA []byte, err error) {
if err = CheckPaths(chain, serverPair); err != nil {
return
}
// Extract all certificates and their data
certs, err := readCertificates(chain.Root)
if err != nil {
return
}
for _, caPath := range chain.Intermediate {
var intermediateCerts []certificate
intermediateCerts, err = readCertificates(caPath)
if err != nil {
return
}
certs = append(certs, intermediateCerts...)
}
serverCerts, err := readCertificates(serverPair.Cert)
if err != nil {
return
}
certs = append(certs, serverCerts...)
serverCert, err := findServerCert(certs)
if err != nil {
err = errors.New(L("Failed to find a non-CA certificate"))
return
}
// Map all certificates using their hashes
mapBySubjectHash := map[string]certificate{}
if serverCert.subjectHash != "" {
mapBySubjectHash[serverCert.subjectHash] = *serverCert
}
for _, caCert := range certs {
if caCert.subjectHash != "" {
mapBySubjectHash[caCert.subjectHash] = caCert
}
}
// Sort from server certificate to RootCA
return sortCertificates(mapBySubjectHash, serverCert.subjectHash)
}
type certificate struct {
content []byte
subject string
subjectHash string
issuer string
issuerHash string
startDate time.Time
endDate time.Time
subjectKeyID string
authKeyID string
isCa bool
isCritical bool
isRoot bool
}
func findServerCert(certs []certificate) (*certificate, error) {
for _, cert := range certs {
if !cert.isCa {
return &cert, nil
}
}
return nil, errors.New(L("expected to find a certificate, got none"))
}
func readCertificates(path string) ([]certificate, error) {
fd, err := os.Open(path)
if err != nil {
return []certificate{}, utils.Errorf(err, L("Failed to read certificate file %s"), path)
}
certs := []certificate{}
for {
log.Debug().Msgf("Running openssl x509 on %s", path)
cmd := exec.Command("openssl", "x509")
cmd.Stdin = fd
out, err := cmd.Output()
if err != nil {
// openssl got an invalid certificate or the end of the file
break
}
// Extract data from the certificate
cert, err := extractCertificateData(out)
if err != nil {
return []certificate{}, err
}
certs = append(certs, cert)
}
return certs, nil
}
// Extract data from the certificate to help ordering and verifying it.
func extractCertificateData(content []byte) (certificate, error) {
args := []string{"x509", "-noout", "-subject", "-subject_hash", "-startdate", "-enddate",
"-issuer", "-issuer_hash", "-ext", "subjectKeyIdentifier,authorityKeyIdentifier,basicConstraints"}
log.Debug().Msg("Running command openssl " + strings.Join(args, " "))
cmd := exec.Command("openssl", args...)
log.Trace().Msgf("Extracting data from certificate:\n%s", string(content))
reader := bytes.NewReader(content)
cmd.Stdin = reader
out, err := cmd.Output()
if err != nil {
return certificate{}, utils.Errorf(err, L("Failed to extract data from certificate:\n%s"), string(content))
}
lines := strings.Split(string(out), "\n")
cert := certificate{content: content}
const timeLayout = "Jan 2 15:04:05 2006 MST"
nextVal := ""
for _, line := range lines {
if strings.TrimSpace(line) == "" {
continue
}
if strings.HasPrefix(line, "subject=") {
cert.subject = strings.SplitN(line, "=", 2)[1]
} else if strings.HasPrefix(line, "issuer=") {
cert.issuer = strings.SplitN(line, "=", 2)[1]
} else if strings.HasPrefix(line, "notBefore=") {
date := strings.SplitN(line, "=", 2)[1]
cert.startDate, err = time.Parse(timeLayout, date)
if err != nil {
return cert, utils.Errorf(err, L("Failed to parse start date: %s\n"), date)
}
} else if strings.HasPrefix(line, "notAfter=") {
date := strings.SplitN(line, "=", 2)[1]
cert.endDate, err = time.Parse(timeLayout, date)
if err != nil {
return cert, utils.Errorf(err, L("Failed to parse end date: %s\n"), date)
}
} else if strings.HasPrefix(line, "X509v3 Subject Key Identifier") {
nextVal = "subjectKeyId"
} else if strings.HasPrefix(line, "X509v3 Authority Key Identifier") {
nextVal = "authKeyId"
} else if strings.HasPrefix(line, "X509v3 Basic Constraints") {
nextVal = "basicConstraints"
} else if strings.HasPrefix(line, " ") {
if nextVal == "subjectKeyId" {
cert.subjectKeyID = strings.ToUpper(strings.TrimSpace(line))
} else if nextVal == "authKeyId" && strings.HasPrefix(line, " keyid:") {
cert.authKeyID = strings.ToUpper(strings.TrimSpace(strings.SplitN(line, ":", 2)[1]))
} else if nextVal == "basicConstraints" && strings.Contains(line, "CA:TRUE") {
cert.isCa = true
} else if nextVal == "basicConstraints" && strings.Contains(line, "critical") {
cert.isCritical = true
} else {
// Unhandled extension value
continue
}
} else if cert.subjectHash == "" {
// subject_hash comes first without key to identify it
cert.subjectHash = strings.TrimSpace(line)
} else {
// second issue_hash without key to identify this value
cert.issuerHash = strings.TrimSpace(line)
}
}
// This configuration might not work anymore in the future.
if cert.isCa && !cert.isCritical {
log.Warn().Msgf(L("Basic constraints for CA should be marked as `%s`, could cause issues otherwise!"), "critical")
}
if cert.subject == cert.issuer {
cert.isRoot = true
// Some Root CAs might not have their authorityKeyIdentifier set to themself
if cert.isCa && cert.authKeyID == "" {
cert.authKeyID = cert.subjectKeyID
}
} else {
cert.isRoot = false
}
return cert, nil
}
// Prepare the certificate chain starting by the server up to the root CA.
// Returns the certificate chain and the root CA.
func sortCertificates(
mapBySubjectHash map[string]certificate,
serverCertHash string,
) (orderedCert []byte, rootCA []byte, err error) {
if len(mapBySubjectHash) == 0 {
err = errors.New(L("no CA certificate found in the files"))
return
}
cert := mapBySubjectHash[serverCertHash]
issuerHash := cert.issuerHash
_, found := mapBySubjectHash[issuerHash]
if issuerHash == "" || !found {
err = fmt.Errorf(
L(`please check the CA chain, including intermediate CA certificates provided.
The chain is missing the CA with subject %[1]s (hash: %[2]s)`),
issuerHash, cert.issuer,
)
return
}
sortedChain := bytes.NewBuffer(mapBySubjectHash[serverCertHash].content)
for {
cert, found = mapBySubjectHash[issuerHash]
if !found {
err = fmt.Errorf(
L(`please check the CA chain, including intermediate CA certificates provided.
The chain is missing the CA with subject %[1]s (hash: %[2]s)`),
issuerHash, cert.issuer,
)
return
}
nextHash := cert.issuerHash
if nextHash == issuerHash {
// Found Root CA, we can exit
rootCA = cert.content
break
}
issuerHash = nextHash
sortedChain.Write(cert.content)
}
orderedCert = sortedChain.Bytes()
return orderedCert, rootCA, nil
}
// CheckPaths ensures that all the passed path exists and the required files are available.
func CheckPaths(chain *types.CaChain, serverPair *types.SSLPair) error {
if err := mandatoryFile(chain.Root, "root CA"); err != nil {
return err
}
for _, ca := range chain.Intermediate {
if err := optionalFile(ca); err != nil {
return err
}
}
if err := mandatoryFile(serverPair.Cert, L("server certificate is required")); err != nil {
return err
}
if err := mandatoryFile(serverPair.Key, L("server key is required")); err != nil {
return err
}
return nil
}
func mandatoryFile(file string, msg string) error {
if file == "" {
return errors.New(msg)
}
return optionalFile(file)
}
func optionalFile(file string) error {
if file != "" && !utils.FileExists(file) {
return fmt.Errorf(L("%s file is not accessible"), file)
}
return nil
}
// Converts an SSL key to RSA.
func GetRsaKey(keyContent string, password string) []byte {
// Kubernetes only handles RSA private TLS keys, convert and strip password
caPassword := password
utils.AskPasswordIfMissing(&caPassword, L("Source server SSL CA private key password"), 0, 0)
// Convert the key file to RSA format for kubectl to handle it
cmd := exec.Command("openssl", "rsa", "-passin", "env:pass")
stdin, err := cmd.StdinPipe()
if err != nil {
log.Fatal().Err(err).Msg(L("Failed to open openssl rsa process input stream"))
}
if _, err := io.WriteString(stdin, keyContent); err != nil {
log.Fatal().Err(err).Msg(L("Failed to write openssl key content to input stream"))
}
cmd.Env = append(cmd.Env, "pass="+caPassword)
out, err := cmd.Output()
if err != nil {
log.Fatal().Err(err).Msg(L("Failed to convert CA private key to RSA"))
}
return out
}
// StripTextFromCertificate removes the optional text part of an x509 certificate.
func StripTextFromCertificate(certContent string) []byte {
cmd := exec.Command("openssl", "x509")
stdin, err := cmd.StdinPipe()
if err != nil {
log.Fatal().Err(err).Msg(L("Failed to open openssl x509 process input stream"))
}
if _, err := io.WriteString(stdin, certContent); err != nil {
log.Fatal().Err(err).Msg(L("Failed to write SSL certificate to input stream"))
}
out, err := cmd.Output()
if err != nil {
log.Fatal().Err(err).Msg(L("failed to strip text part from CA certificate"))
}
return out
}
var newRunner = utils.NewRunner
// CheckKey verifies that the SSL key located at keyPath is valid and not encrypted.
func CheckKey(keyPath string) error {
if err := mandatoryFile(keyPath, L("server key is required")); err != nil {
return err
}
_, err := newRunner("openssl", "pkey", "-in", keyPath, "-passin", "pass:invalid", "-text", "-noout").Exec()
if err != nil {
return utils.Error(err, L("Invalid SSL key, it is probably encrypted"))
}
return nil
}
// nochecktime disables time verification and should only be for unit tests as the test cert isn't refreshed.
var nochecktime = false
// VerifyHostname checks that the certificate at certPath is matching the hostname.
func VerifyHostname(caPath string, certPath string, hostname string) error {
args := []string{"verify"}
if nochecktime {
args = append(args, "-no_check_time")
}
args = append(args, "-untrusted", certPath, "-CAfile", caPath, "-verify_hostname", hostname, certPath)
// The certPath needs to be added as trusted too since it could be a bundle with intermediate certs.
_, err := newRunner("openssl", args...).Log(zerolog.DebugLevel).Exec()
if err != nil {
return utils.Errorf(err, L("failed to validate hostname %s"), hostname)
}
return nil
}