github.com/argoproj/argo-cd/v3@v3.2.1/util/db/certificate.go (about)

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