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  }