github.com/argoproj/argo-cd@v1.8.7/util/db/certificate.go (about) 1 package db 2 3 import ( 4 "fmt" 5 "regexp" 6 "strings" 7 8 "golang.org/x/crypto/ssh" 9 "golang.org/x/net/context" 10 11 log "github.com/sirupsen/logrus" 12 13 "github.com/argoproj/argo-cd/common" 14 appsv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1" 15 certutil "github.com/argoproj/argo-cd/util/cert" 16 ) 17 18 // A struct representing an entry in the list of SSH known hosts. 19 type SSHKnownHostsEntry struct { 20 // Hostname the key is for 21 Host string 22 // The type of the key 23 SubType string 24 // The data of the key, including the type 25 Data string 26 // The SHA256 fingerprint of the key 27 Fingerprint string 28 } 29 30 // A representation of a TLS certificate 31 type TLSCertificate struct { 32 // Subject of the certificate 33 Subject string 34 // Issuer of the certificate 35 Issuer string 36 // Certificate data 37 Data string 38 } 39 40 // Helper struct for certificate selection 41 type CertificateListSelector struct { 42 // Pattern to match the hostname with 43 HostNamePattern string 44 // Type of certificate to match 45 CertType string 46 // Subtype of certificate to match 47 CertSubType string 48 } 49 50 // Get a list of all configured repository certificates matching the given 51 // selector. The list of certificates explicitly excludes the CertData of 52 // the certificates, and only returns the metadata including CertInfo field. 53 // 54 // The CertInfo field in the returned entries will contain the following data: 55 // - For SSH keys, the SHA256 fingerprint of the key as string, prepended by 56 // the string "SHA256:" 57 // - For TLS certs, the Subject of the X509 cert as a string in DN notation 58 // 59 func (db *db) ListRepoCertificates(ctx context.Context, selector *CertificateListSelector) (*appsv1.RepositoryCertificateList, error) { 60 61 // selector may be given as nil, but we need at least an empty data structure 62 // so we create it if necessary. 63 if selector == nil { 64 selector = &CertificateListSelector{} 65 } 66 67 certificates := make([]appsv1.RepositoryCertificate, 0) 68 69 // Get all SSH known host entries 70 if selector.CertType == "" || selector.CertType == "*" || selector.CertType == "ssh" { 71 sshKnownHosts, err := db.getSSHKnownHostsData() 72 if err != nil { 73 return nil, err 74 } 75 76 for _, entry := range sshKnownHosts { 77 if certutil.MatchHostName(entry.Host, selector.HostNamePattern) && (selector.CertSubType == "" || selector.CertSubType == "*" || selector.CertSubType == entry.SubType) { 78 certificates = append(certificates, appsv1.RepositoryCertificate{ 79 ServerName: entry.Host, 80 CertType: "ssh", 81 CertSubType: entry.SubType, 82 CertInfo: "SHA256:" + certutil.SSHFingerprintSHA256FromString(fmt.Sprintf("%s %s", entry.Host, entry.Data)), 83 }) 84 } 85 } 86 } 87 88 // Get all TLS certificates 89 if selector.CertType == "" || selector.CertType == "*" || selector.CertType == "https" || selector.CertType == "tls" { 90 tlsCertificates, err := db.getTLSCertificateData() 91 if err != nil { 92 return nil, err 93 } 94 for _, entry := range tlsCertificates { 95 if certutil.MatchHostName(entry.Subject, selector.HostNamePattern) { 96 pemEntries, err := certutil.ParseTLSCertificatesFromData(entry.Data) 97 if err != nil { 98 continue 99 } 100 for _, pemEntry := range pemEntries { 101 var certInfo, certSubType string 102 x509Data, err := certutil.DecodePEMCertificateToX509(pemEntry) 103 if err != nil { 104 certInfo = err.Error() 105 certSubType = "invalid" 106 } else { 107 certInfo = x509Data.Subject.String() 108 certSubType = x509Data.PublicKeyAlgorithm.String() 109 } 110 certificates = append(certificates, appsv1.RepositoryCertificate{ 111 ServerName: entry.Subject, 112 CertType: "https", 113 CertSubType: strings.ToLower(certSubType), 114 CertInfo: certInfo, 115 }) 116 } 117 } 118 } 119 } 120 121 return &appsv1.RepositoryCertificateList{ 122 Items: certificates, 123 }, nil 124 } 125 126 // Get a single certificate from the datastore 127 func (db *db) GetRepoCertificate(ctx context.Context, serverType string, serverName string) (*appsv1.RepositoryCertificate, error) { 128 if serverType == "ssh" { 129 sshKnownHostsList, err := db.getSSHKnownHostsData() 130 if err != nil { 131 return nil, err 132 } 133 for _, entry := range sshKnownHostsList { 134 if entry.Host == serverName { 135 repo := &appsv1.RepositoryCertificate{ 136 ServerName: entry.Host, 137 CertType: "ssh", 138 CertSubType: entry.SubType, 139 CertData: []byte(entry.Data), 140 CertInfo: entry.Fingerprint, 141 } 142 return repo, nil 143 } 144 } 145 } 146 147 // Fail 148 return nil, nil 149 } 150 151 // Create one or more repository certificates and returns a list of certificates 152 // actually created. 153 func (db *db) CreateRepoCertificate(ctx context.Context, certificates *appsv1.RepositoryCertificateList, upsert bool) (*appsv1.RepositoryCertificateList, error) { 154 var ( 155 saveSSHData bool = false 156 saveTLSData bool = false 157 ) 158 159 sshKnownHostsList, err := db.getSSHKnownHostsData() 160 if err != nil { 161 return nil, err 162 } 163 164 tlsCertificates, err := db.getTLSCertificateData() 165 if err != nil { 166 return nil, err 167 } 168 169 // This will hold the final list of certificates that have been created 170 created := make([]appsv1.RepositoryCertificate, 0) 171 172 // Each request can contain multiple certificates of different types, so we 173 // make sure to handle each request accordingly. 174 for _, certificate := range certificates.Items { 175 // Ensure valid repo server name was given only for https certificates. 176 // For SSH known host entries, we let Go's ssh library do the validation 177 // later on. 178 if certificate.CertType == "https" && !certutil.IsValidHostname(certificate.ServerName, false) { 179 return nil, fmt.Errorf("Invalid hostname in request: %s", certificate.ServerName) 180 } else if certificate.CertType == "ssh" { 181 // Matches "[hostname]:port" format 182 reExtract := regexp.MustCompile(`^\[(.*)\]\:[0-9]+$`) 183 matches := reExtract.FindStringSubmatch(certificate.ServerName) 184 var hostnameToCheck string 185 if len(matches) == 0 { 186 hostnameToCheck = certificate.ServerName 187 } else { 188 hostnameToCheck = matches[1] 189 } 190 if !certutil.IsValidHostname(hostnameToCheck, false) { 191 return nil, fmt.Errorf("Invalid hostname in request: %s", hostnameToCheck) 192 } 193 } 194 195 if certificate.CertType == "ssh" { 196 // Whether we have a new certificate entry 197 newEntry := true 198 // Whether we have upserted an existing certificate entry 199 upserted := false 200 201 // Check whether known hosts entry already exists. Must match hostname 202 // and the key sub type (e.g. ssh-rsa). It is considered an error if we 203 // already have a corresponding key and upsert was not specified. 204 for _, entry := range sshKnownHostsList { 205 if entry.Host == certificate.ServerName && entry.SubType == certificate.CertSubType { 206 if !upsert && entry.Data != string(certificate.CertData) { 207 return nil, fmt.Errorf("Key for '%s' (subtype: '%s') already exist and upsert was not specified.", entry.Host, entry.SubType) 208 } else { 209 // Do not add an entry on upsert, but remember if we actual did an 210 // upsert. 211 newEntry = false 212 if entry.Data != string(certificate.CertData) { 213 entry.Data = string(certificate.CertData) 214 upserted = true 215 } 216 break 217 } 218 } 219 } 220 221 // Make sure that we received a valid public host key by parsing it 222 _, hostnames, rawKeyData, _, _, err := ssh.ParseKnownHosts([]byte(fmt.Sprintf("%s %s %s", certificate.ServerName, certificate.CertSubType, certificate.CertData))) 223 if err != nil { 224 return nil, err 225 } 226 227 if len(hostnames) == 0 { 228 log.Errorf("Could not parse hostname for key from token %s", certificate.ServerName) 229 } 230 231 if newEntry { 232 sshKnownHostsList = append(sshKnownHostsList, &SSHKnownHostsEntry{ 233 Host: hostnames[0], 234 Data: string(certificate.CertData), 235 SubType: certificate.CertSubType, 236 }) 237 } 238 239 // If we created a new entry, or if we upserted an existing one, we need 240 // to save the data and notify the consumer about the operation. 241 if newEntry || upserted { 242 certificate.CertInfo = certutil.SSHFingerprintSHA256(rawKeyData) 243 created = append(created, certificate) 244 saveSSHData = true 245 } 246 247 } else if certificate.CertType == "https" { 248 var tlsCertificate *TLSCertificate = nil 249 newEntry := true 250 upserted := false 251 pemCreated := make([]string, 0) 252 253 for _, entry := range tlsCertificates { 254 // We have an entry for this server already. Check for upsert. 255 if entry.Subject == certificate.ServerName { 256 newEntry = false 257 if entry.Data != string(certificate.CertData) { 258 if !upsert { 259 return nil, fmt.Errorf("TLS certificate for server '%s' already exist and upsert was not specified.", entry.Subject) 260 } 261 } 262 // Store pointer to this entry for later use. 263 tlsCertificate = entry 264 break 265 } 266 } 267 268 // Check for validity of data received 269 pemData, err := certutil.ParseTLSCertificatesFromData(string(certificate.CertData)) 270 if err != nil { 271 return nil, err 272 } 273 274 // We should have at least one valid PEM entry 275 if len(pemData) == 0 { 276 return nil, fmt.Errorf("No valid PEM data received.") 277 } 278 279 // Make sure we have valid X509 certificates in the data 280 for _, entry := range pemData { 281 _, err := certutil.DecodePEMCertificateToX509(entry) 282 if err != nil { 283 return nil, err 284 } 285 pemCreated = append(pemCreated, entry) 286 } 287 288 // New certificate if pointer to existing cert is nil 289 if tlsCertificate == nil { 290 tlsCertificate = &TLSCertificate{ 291 Subject: certificate.ServerName, 292 Data: string(certificate.CertData), 293 } 294 tlsCertificates = append(tlsCertificates, tlsCertificate) 295 } else { 296 // We have made sure the upsert flag was set above. Now just figure out 297 // again if we have to actually update the data in the existing cert. 298 if tlsCertificate.Data != string(certificate.CertData) { 299 tlsCertificate.Data = string(certificate.CertData) 300 upserted = true 301 } 302 } 303 304 if newEntry || upserted { 305 // We append the certificate for every PEM entry in the request, so the 306 // caller knows that we processed each single item. 307 for _, entry := range pemCreated { 308 created = append(created, appsv1.RepositoryCertificate{ 309 ServerName: certificate.ServerName, 310 CertType: "https", 311 CertData: []byte(entry), 312 }) 313 } 314 saveTLSData = true 315 } 316 } else { 317 // Invalid/unknown certificate type 318 return nil, fmt.Errorf("Unknown certificate type: %s", certificate.CertType) 319 } 320 } 321 322 if saveSSHData { 323 err = db.settingsMgr.SaveSSHKnownHostsData(ctx, knownHostsDataToStrings(sshKnownHostsList)) 324 if err != nil { 325 return nil, err 326 } 327 } 328 329 if saveTLSData { 330 err = db.settingsMgr.SaveTLSCertificateData(ctx, tlsCertificatesToMap(tlsCertificates)) 331 if err != nil { 332 return nil, err 333 } 334 } 335 336 return &appsv1.RepositoryCertificateList{Items: created}, nil 337 } 338 339 // Batch remove configured certificates according to the selector query 340 func (db *db) RemoveRepoCertificates(ctx context.Context, selector *CertificateListSelector) (*appsv1.RepositoryCertificateList, error) { 341 var ( 342 knownHostsOld []*SSHKnownHostsEntry 343 knownHostsNew []*SSHKnownHostsEntry 344 tlsCertificatesOld []*TLSCertificate 345 tlsCertificatesNew []*TLSCertificate 346 err error 347 ) 348 349 removed := &appsv1.RepositoryCertificateList{ 350 Items: make([]appsv1.RepositoryCertificate, 0), 351 } 352 353 if selector.CertType == "" || selector.CertType == "ssh" || selector.CertType == "*" { 354 knownHostsOld, err = db.getSSHKnownHostsData() 355 if err != nil { 356 return nil, err 357 } 358 knownHostsNew = make([]*SSHKnownHostsEntry, 0) 359 360 for _, entry := range knownHostsOld { 361 if matchSSHKnownHostsEntry(entry, selector) { 362 removed.Items = append(removed.Items, appsv1.RepositoryCertificate{ 363 ServerName: entry.Host, 364 CertType: "ssh", 365 CertSubType: entry.SubType, 366 CertData: []byte(entry.Data), 367 }) 368 } else { 369 knownHostsNew = append(knownHostsNew, entry) 370 } 371 } 372 } 373 374 if selector.CertType == "" || selector.CertType == "*" || selector.CertType == "https" || selector.CertType == "tls" { 375 tlsCertificatesOld, err = db.getTLSCertificateData() 376 if err != nil { 377 return nil, err 378 } 379 tlsCertificatesNew = make([]*TLSCertificate, 0) 380 for _, entry := range tlsCertificatesOld { 381 if certutil.MatchHostName(entry.Subject, selector.HostNamePattern) { 382 // Wrap each PEM certificate into its own RepositoryCertificate object 383 // so the caller knows what has been removed actually. 384 // 385 // The downside of this is, only valid data can be removed from the CM, 386 // so if the data somehow got corrupted, it can only be removed by 387 // means of editing the CM directly using e.g. kubectl. 388 pemCertificates, err := certutil.ParseTLSCertificatesFromData(entry.Data) 389 if err != nil { 390 return nil, err 391 } 392 if len(pemCertificates) > 0 { 393 for _, pem := range pemCertificates { 394 removed.Items = append(removed.Items, appsv1.RepositoryCertificate{ 395 ServerName: entry.Subject, 396 CertType: "https", 397 CertData: []byte(pem), 398 }) 399 } 400 } 401 } else { 402 tlsCertificatesNew = append(tlsCertificatesNew, entry) 403 } 404 } 405 } 406 407 if len(knownHostsNew) < len(knownHostsOld) { 408 err = db.settingsMgr.SaveSSHKnownHostsData(ctx, knownHostsDataToStrings(knownHostsNew)) 409 if err != nil { 410 return nil, err 411 } 412 } 413 414 if len(tlsCertificatesNew) < len(tlsCertificatesOld) { 415 err = db.settingsMgr.SaveTLSCertificateData(ctx, tlsCertificatesToMap(tlsCertificatesNew)) 416 if err != nil { 417 return nil, err 418 } 419 } 420 421 return removed, nil 422 } 423 424 // Converts list of known hosts data to array of strings, suitable for storing 425 // in a known_hosts file for SSH. 426 func knownHostsDataToStrings(knownHostsList []*SSHKnownHostsEntry) []string { 427 knownHostsData := make([]string, 0) 428 for _, entry := range knownHostsList { 429 knownHostsData = append(knownHostsData, fmt.Sprintf("%s %s %s", entry.Host, entry.SubType, entry.Data)) 430 } 431 return knownHostsData 432 } 433 434 // Converts list of TLS certificates to a map whose key will be the certificate 435 // subject and the data will be a string containing TLS certificate data as PEM 436 func tlsCertificatesToMap(tlsCertificates []*TLSCertificate) map[string]string { 437 certMap := make(map[string]string) 438 for _, entry := range tlsCertificates { 439 certMap[entry.Subject] = entry.Data 440 } 441 return certMap 442 } 443 444 // Get the TLS certificate data from the config map 445 func (db *db) getTLSCertificateData() ([]*TLSCertificate, error) { 446 certificates := make([]*TLSCertificate, 0) 447 certCM, err := db.settingsMgr.GetConfigMapByName(common.ArgoCDTLSCertsConfigMapName) 448 if err != nil { 449 return nil, err 450 } 451 for key, entry := range certCM.Data { 452 certificates = append(certificates, &TLSCertificate{Subject: key, Data: entry}) 453 } 454 455 return certificates, nil 456 } 457 458 // Gets the SSH known host data from ConfigMap and parse it into an array of 459 // SSHKnownHostEntry structs. 460 func (db *db) getSSHKnownHostsData() ([]*SSHKnownHostsEntry, error) { 461 certCM, err := db.settingsMgr.GetConfigMapByName(common.ArgoCDKnownHostsConfigMapName) 462 if err != nil { 463 return nil, err 464 } 465 466 sshKnownHostsData := certCM.Data["ssh_known_hosts"] 467 entries := make([]*SSHKnownHostsEntry, 0) 468 469 // ssh_known_hosts data contains one key per line, so we must iterate over 470 // the whole data to get all keys. 471 // 472 // We validate the data found to a certain extent before we accept them as 473 // entry into our list to be returned. 474 // 475 sshKnownHostsEntries, err := certutil.ParseSSHKnownHostsFromData(sshKnownHostsData) 476 if err != nil { 477 return nil, err 478 } 479 480 for _, entry := range sshKnownHostsEntries { 481 hostname, subType, keyData, err := certutil.TokenizeSSHKnownHostsEntry(entry) 482 if err != nil { 483 return nil, err 484 } 485 entries = append(entries, &SSHKnownHostsEntry{ 486 Host: hostname, 487 SubType: subType, 488 Data: string(keyData), 489 }) 490 } 491 492 return entries, nil 493 } 494 495 func matchSSHKnownHostsEntry(entry *SSHKnownHostsEntry, selector *CertificateListSelector) bool { 496 return certutil.MatchHostName(entry.Host, selector.HostNamePattern) && (selector.CertSubType == "" || selector.CertSubType == "*" || selector.CertSubType == entry.SubType) 497 }