-
Notifications
You must be signed in to change notification settings - Fork 5.3k
/
certificate.go
497 lines (442 loc) · 15.7 KB
/
certificate.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
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
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
package db
import (
"fmt"
"regexp"
"strings"
"context"
"golang.org/x/crypto/ssh"
log "github.com/sirupsen/logrus"
"github.com/argoproj/argo-cd/v2/common"
appsv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
certutil "github.com/argoproj/argo-cd/v2/util/cert"
)
// A struct representing an entry in the list of SSH known hosts.
type SSHKnownHostsEntry struct {
// Hostname the key is for
Host string
// The type of the key
SubType string
// The data of the key, including the type
Data string
// The SHA256 fingerprint of the key
Fingerprint string
}
// A representation of a TLS certificate
type TLSCertificate struct {
// Subject of the certificate
Subject string
// Issuer of the certificate
Issuer string
// Certificate data
Data string
}
// Helper struct for certificate selection
type CertificateListSelector struct {
// Pattern to match the hostname with
HostNamePattern string
// Type of certificate to match
CertType string
// Subtype of certificate to match
CertSubType string
}
// Get a list of all configured repository certificates matching the given
// selector. The list of certificates explicitly excludes the CertData of
// the certificates, and only returns the metadata including CertInfo field.
//
// The CertInfo field in the returned entries will contain the following data:
// - For SSH keys, the SHA256 fingerprint of the key as string, prepended by
// the string "SHA256:"
// - For TLS certs, the Subject of the X509 cert as a string in DN notation
//
func (db *db) ListRepoCertificates(ctx context.Context, selector *CertificateListSelector) (*appsv1.RepositoryCertificateList, error) {
// selector may be given as nil, but we need at least an empty data structure
// so we create it if necessary.
if selector == nil {
selector = &CertificateListSelector{}
}
certificates := make([]appsv1.RepositoryCertificate, 0)
// Get all SSH known host entries
if selector.CertType == "" || selector.CertType == "*" || selector.CertType == "ssh" {
sshKnownHosts, err := db.getSSHKnownHostsData()
if err != nil {
return nil, err
}
for _, entry := range sshKnownHosts {
if certutil.MatchHostName(entry.Host, selector.HostNamePattern) && (selector.CertSubType == "" || selector.CertSubType == "*" || selector.CertSubType == entry.SubType) {
certificates = append(certificates, appsv1.RepositoryCertificate{
ServerName: entry.Host,
CertType: "ssh",
CertSubType: entry.SubType,
CertInfo: "SHA256:" + certutil.SSHFingerprintSHA256FromString(fmt.Sprintf("%s %s", entry.Host, entry.Data)),
})
}
}
}
// Get all TLS certificates
if selector.CertType == "" || selector.CertType == "*" || selector.CertType == "https" || selector.CertType == "tls" {
tlsCertificates, err := db.getTLSCertificateData()
if err != nil {
return nil, err
}
for _, entry := range tlsCertificates {
if certutil.MatchHostName(entry.Subject, selector.HostNamePattern) {
pemEntries, err := certutil.ParseTLSCertificatesFromData(entry.Data)
if err != nil {
continue
}
for _, pemEntry := range pemEntries {
var certInfo, certSubType string
x509Data, err := certutil.DecodePEMCertificateToX509(pemEntry)
if err != nil {
certInfo = err.Error()
certSubType = "invalid"
} else {
certInfo = x509Data.Subject.String()
certSubType = x509Data.PublicKeyAlgorithm.String()
}
certificates = append(certificates, appsv1.RepositoryCertificate{
ServerName: entry.Subject,
CertType: "https",
CertSubType: strings.ToLower(certSubType),
CertInfo: certInfo,
})
}
}
}
}
return &appsv1.RepositoryCertificateList{
Items: certificates,
}, nil
}
// Get a single certificate from the datastore
func (db *db) GetRepoCertificate(ctx context.Context, serverType string, serverName string) (*appsv1.RepositoryCertificate, error) {
if serverType == "ssh" {
sshKnownHostsList, err := db.getSSHKnownHostsData()
if err != nil {
return nil, err
}
for _, entry := range sshKnownHostsList {
if entry.Host == serverName {
repo := &appsv1.RepositoryCertificate{
ServerName: entry.Host,
CertType: "ssh",
CertSubType: entry.SubType,
CertData: []byte(entry.Data),
CertInfo: entry.Fingerprint,
}
return repo, nil
}
}
}
// Fail
return nil, nil
}
// Create one or more repository certificates and returns a list of certificates
// actually created.
func (db *db) CreateRepoCertificate(ctx context.Context, certificates *appsv1.RepositoryCertificateList, upsert bool) (*appsv1.RepositoryCertificateList, error) {
var (
saveSSHData bool = false
saveTLSData bool = false
)
sshKnownHostsList, err := db.getSSHKnownHostsData()
if err != nil {
return nil, err
}
tlsCertificates, err := db.getTLSCertificateData()
if err != nil {
return nil, err
}
// This will hold the final list of certificates that have been created
created := make([]appsv1.RepositoryCertificate, 0)
// Each request can contain multiple certificates of different types, so we
// make sure to handle each request accordingly.
for _, certificate := range certificates.Items {
// Ensure valid repo server name was given only for https certificates.
// For SSH known host entries, we let Go's ssh library do the validation
// later on.
if certificate.CertType == "https" && !certutil.IsValidHostname(certificate.ServerName, false) {
return nil, fmt.Errorf("Invalid hostname in request: %s", certificate.ServerName)
} else if certificate.CertType == "ssh" {
// Matches "[hostname]:port" format
reExtract := regexp.MustCompile(`^\[(.*)\]\:[0-9]+$`)
matches := reExtract.FindStringSubmatch(certificate.ServerName)
var hostnameToCheck string
if len(matches) == 0 {
hostnameToCheck = certificate.ServerName
} else {
hostnameToCheck = matches[1]
}
if !certutil.IsValidHostname(hostnameToCheck, false) {
return nil, fmt.Errorf("Invalid hostname in request: %s", hostnameToCheck)
}
}
if certificate.CertType == "ssh" {
// Whether we have a new certificate entry
newEntry := true
// Whether we have upserted an existing certificate entry
upserted := false
// Check whether known hosts entry already exists. Must match hostname
// and the key sub type (e.g. ssh-rsa). It is considered an error if we
// already have a corresponding key and upsert was not specified.
for _, entry := range sshKnownHostsList {
if entry.Host == certificate.ServerName && entry.SubType == certificate.CertSubType {
if !upsert && entry.Data != string(certificate.CertData) {
return nil, fmt.Errorf("Key for '%s' (subtype: '%s') already exist and upsert was not specified.", entry.Host, entry.SubType)
} else {
// Do not add an entry on upsert, but remember if we actual did an
// upsert.
newEntry = false
if entry.Data != string(certificate.CertData) {
entry.Data = string(certificate.CertData)
upserted = true
}
break
}
}
}
// Make sure that we received a valid public host key by parsing it
_, hostnames, rawKeyData, _, _, err := ssh.ParseKnownHosts([]byte(fmt.Sprintf("%s %s %s", certificate.ServerName, certificate.CertSubType, certificate.CertData)))
if err != nil {
return nil, err
}
if len(hostnames) == 0 {
log.Errorf("Could not parse hostname for key from token %s", certificate.ServerName)
}
if newEntry {
sshKnownHostsList = append(sshKnownHostsList, &SSHKnownHostsEntry{
Host: hostnames[0],
Data: string(certificate.CertData),
SubType: certificate.CertSubType,
})
}
// If we created a new entry, or if we upserted an existing one, we need
// to save the data and notify the consumer about the operation.
if newEntry || upserted {
certificate.CertInfo = certutil.SSHFingerprintSHA256(rawKeyData)
created = append(created, certificate)
saveSSHData = true
}
} else if certificate.CertType == "https" {
var tlsCertificate *TLSCertificate = nil
newEntry := true
upserted := false
pemCreated := make([]string, 0)
for _, entry := range tlsCertificates {
// We have an entry for this server already. Check for upsert.
if entry.Subject == certificate.ServerName {
newEntry = false
if entry.Data != string(certificate.CertData) {
if !upsert {
return nil, fmt.Errorf("TLS certificate for server '%s' already exist and upsert was not specified.", entry.Subject)
}
}
// Store pointer to this entry for later use.
tlsCertificate = entry
break
}
}
// Check for validity of data received
pemData, err := certutil.ParseTLSCertificatesFromData(string(certificate.CertData))
if err != nil {
return nil, err
}
// We should have at least one valid PEM entry
if len(pemData) == 0 {
return nil, fmt.Errorf("No valid PEM data received.")
}
// Make sure we have valid X509 certificates in the data
for _, entry := range pemData {
_, err := certutil.DecodePEMCertificateToX509(entry)
if err != nil {
return nil, err
}
pemCreated = append(pemCreated, entry)
}
// New certificate if pointer to existing cert is nil
if tlsCertificate == nil {
tlsCertificate = &TLSCertificate{
Subject: certificate.ServerName,
Data: string(certificate.CertData),
}
tlsCertificates = append(tlsCertificates, tlsCertificate)
} else {
// We have made sure the upsert flag was set above. Now just figure out
// again if we have to actually update the data in the existing cert.
if tlsCertificate.Data != string(certificate.CertData) {
tlsCertificate.Data = string(certificate.CertData)
upserted = true
}
}
if newEntry || upserted {
// We append the certificate for every PEM entry in the request, so the
// caller knows that we processed each single item.
for _, entry := range pemCreated {
created = append(created, appsv1.RepositoryCertificate{
ServerName: certificate.ServerName,
CertType: "https",
CertData: []byte(entry),
})
}
saveTLSData = true
}
} else {
// Invalid/unknown certificate type
return nil, fmt.Errorf("Unknown certificate type: %s", certificate.CertType)
}
}
if saveSSHData {
err = db.settingsMgr.SaveSSHKnownHostsData(ctx, knownHostsDataToStrings(sshKnownHostsList))
if err != nil {
return nil, err
}
}
if saveTLSData {
err = db.settingsMgr.SaveTLSCertificateData(ctx, tlsCertificatesToMap(tlsCertificates))
if err != nil {
return nil, err
}
}
return &appsv1.RepositoryCertificateList{Items: created}, nil
}
// Batch remove configured certificates according to the selector query
func (db *db) RemoveRepoCertificates(ctx context.Context, selector *CertificateListSelector) (*appsv1.RepositoryCertificateList, error) {
var (
knownHostsOld []*SSHKnownHostsEntry
knownHostsNew []*SSHKnownHostsEntry
tlsCertificatesOld []*TLSCertificate
tlsCertificatesNew []*TLSCertificate
err error
)
removed := &appsv1.RepositoryCertificateList{
Items: make([]appsv1.RepositoryCertificate, 0),
}
if selector.CertType == "" || selector.CertType == "ssh" || selector.CertType == "*" {
knownHostsOld, err = db.getSSHKnownHostsData()
if err != nil {
return nil, err
}
knownHostsNew = make([]*SSHKnownHostsEntry, 0)
for _, entry := range knownHostsOld {
if matchSSHKnownHostsEntry(entry, selector) {
removed.Items = append(removed.Items, appsv1.RepositoryCertificate{
ServerName: entry.Host,
CertType: "ssh",
CertSubType: entry.SubType,
CertData: []byte(entry.Data),
})
} else {
knownHostsNew = append(knownHostsNew, entry)
}
}
}
if selector.CertType == "" || selector.CertType == "*" || selector.CertType == "https" || selector.CertType == "tls" {
tlsCertificatesOld, err = db.getTLSCertificateData()
if err != nil {
return nil, err
}
tlsCertificatesNew = make([]*TLSCertificate, 0)
for _, entry := range tlsCertificatesOld {
if certutil.MatchHostName(entry.Subject, selector.HostNamePattern) {
// Wrap each PEM certificate into its own RepositoryCertificate object
// so the caller knows what has been removed actually.
//
// The downside of this is, only valid data can be removed from the CM,
// so if the data somehow got corrupted, it can only be removed by
// means of editing the CM directly using e.g. kubectl.
pemCertificates, err := certutil.ParseTLSCertificatesFromData(entry.Data)
if err != nil {
return nil, err
}
if len(pemCertificates) > 0 {
for _, pem := range pemCertificates {
removed.Items = append(removed.Items, appsv1.RepositoryCertificate{
ServerName: entry.Subject,
CertType: "https",
CertData: []byte(pem),
})
}
}
} else {
tlsCertificatesNew = append(tlsCertificatesNew, entry)
}
}
}
if len(knownHostsNew) < len(knownHostsOld) {
err = db.settingsMgr.SaveSSHKnownHostsData(ctx, knownHostsDataToStrings(knownHostsNew))
if err != nil {
return nil, err
}
}
if len(tlsCertificatesNew) < len(tlsCertificatesOld) {
err = db.settingsMgr.SaveTLSCertificateData(ctx, tlsCertificatesToMap(tlsCertificatesNew))
if err != nil {
return nil, err
}
}
return removed, nil
}
// Converts list of known hosts data to array of strings, suitable for storing
// in a known_hosts file for SSH.
func knownHostsDataToStrings(knownHostsList []*SSHKnownHostsEntry) []string {
knownHostsData := make([]string, 0)
for _, entry := range knownHostsList {
knownHostsData = append(knownHostsData, fmt.Sprintf("%s %s %s", entry.Host, entry.SubType, entry.Data))
}
return knownHostsData
}
// Converts list of TLS certificates to a map whose key will be the certificate
// subject and the data will be a string containing TLS certificate data as PEM
func tlsCertificatesToMap(tlsCertificates []*TLSCertificate) map[string]string {
certMap := make(map[string]string)
for _, entry := range tlsCertificates {
certMap[entry.Subject] = entry.Data
}
return certMap
}
// Get the TLS certificate data from the config map
func (db *db) getTLSCertificateData() ([]*TLSCertificate, error) {
certificates := make([]*TLSCertificate, 0)
certCM, err := db.settingsMgr.GetConfigMapByName(common.ArgoCDTLSCertsConfigMapName)
if err != nil {
return nil, err
}
for key, entry := range certCM.Data {
certificates = append(certificates, &TLSCertificate{Subject: key, Data: entry})
}
return certificates, nil
}
// Gets the SSH known host data from ConfigMap and parse it into an array of
// SSHKnownHostEntry structs.
func (db *db) getSSHKnownHostsData() ([]*SSHKnownHostsEntry, error) {
certCM, err := db.settingsMgr.GetConfigMapByName(common.ArgoCDKnownHostsConfigMapName)
if err != nil {
return nil, err
}
sshKnownHostsData := certCM.Data["ssh_known_hosts"]
entries := make([]*SSHKnownHostsEntry, 0)
// ssh_known_hosts data contains one key per line, so we must iterate over
// the whole data to get all keys.
//
// We validate the data found to a certain extent before we accept them as
// entry into our list to be returned.
//
sshKnownHostsEntries, err := certutil.ParseSSHKnownHostsFromData(sshKnownHostsData)
if err != nil {
return nil, err
}
for _, entry := range sshKnownHostsEntries {
hostname, subType, keyData, err := certutil.TokenizeSSHKnownHostsEntry(entry)
if err != nil {
return nil, err
}
entries = append(entries, &SSHKnownHostsEntry{
Host: hostname,
SubType: subType,
Data: string(keyData),
})
}
return entries, nil
}
func matchSSHKnownHostsEntry(entry *SSHKnownHostsEntry, selector *CertificateListSelector) bool {
return certutil.MatchHostName(entry.Host, selector.HostNamePattern) && (selector.CertSubType == "" || selector.CertSubType == "*" || selector.CertSubType == entry.SubType)
}