...

Source file src/github.com/Microsoft/hcsshim/internal/did-x509-resolver/resolver.go

Documentation: github.com/Microsoft/hcsshim/internal/did-x509-resolver

     1  package didx509resolver
     2  
     3  import (
     4  	"crypto/sha256"
     5  	"crypto/sha512"
     6  	"crypto/x509"
     7  	"encoding/asn1"
     8  	"encoding/base64"
     9  	"encoding/json"
    10  	"encoding/pem"
    11  	"errors"
    12  	"fmt"
    13  	"net/url"
    14  	"strconv"
    15  	"strings"
    16  
    17  	"github.com/lestrrat-go/jwx/jwk"
    18  )
    19  
    20  func VerifyCertificateChain(chain []*x509.Certificate, trustedRoots []*x509.Certificate, ignoreTime bool) ([][]*x509.Certificate, error) {
    21  	roots := x509.NewCertPool()
    22  	for _, cert := range trustedRoots {
    23  		roots.AddCert(cert)
    24  	}
    25  
    26  	intermediates := x509.NewCertPool()
    27  
    28  	for _, c := range chain[1 : len(chain)-1] {
    29  		intermediates.AddCert(c)
    30  	}
    31  
    32  	opts := x509.VerifyOptions{
    33  		Roots:         roots,
    34  		Intermediates: intermediates,
    35  		KeyUsages:     []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
    36  		CurrentTime:   chain[0].NotAfter, // TODO: Figure out how to disable expiry checks?
    37  	}
    38  
    39  	return chain[0].Verify(opts)
    40  }
    41  
    42  func checkFingerprint(chain []*x509.Certificate, caFingerprintAlg, caFingerprint string) error {
    43  
    44  	expectedCaFingerprints := make(map[string]struct{})
    45  	for _, cert := range chain[1:] {
    46  		var n struct{}
    47  		var hash []byte
    48  		switch caFingerprintAlg {
    49  		case "sha256":
    50  			x := sha256.Sum256(cert.Raw)
    51  			hash = x[:]
    52  		case "sha384":
    53  			x := sha512.Sum384(cert.Raw)
    54  			hash = x[:]
    55  		case "sha512":
    56  			x := sha512.Sum512(cert.Raw)
    57  			hash = x[:]
    58  		default:
    59  			return errors.New("unsupported hash algorithm")
    60  		}
    61  		hashStr := base64.RawURLEncoding.EncodeToString(hash)
    62  		expectedCaFingerprints[hashStr] = n
    63  	}
    64  
    65  	if _, found := expectedCaFingerprints[caFingerprint]; !found {
    66  		return errors.New("unexpected certificate fingerprint")
    67  	}
    68  
    69  	return nil
    70  }
    71  
    72  func oidFromString(s string) (*asn1.ObjectIdentifier, error) {
    73  	tokens := strings.Split(s, ".")
    74  	var ints []int
    75  	for _, x := range tokens {
    76  		i, err := strconv.Atoi(x)
    77  		if err != nil {
    78  			return nil, errors.New("invalid OID")
    79  		}
    80  		ints = append(ints, i)
    81  	}
    82  	result := asn1.ObjectIdentifier(ints)
    83  	return &result, nil
    84  }
    85  
    86  func checkHasSan(sanType string, value string, cert *x509.Certificate) error {
    87  	switch sanType {
    88  	case "dns":
    89  		for _, name := range cert.DNSNames {
    90  			if name == value {
    91  				return nil
    92  			}
    93  		}
    94  	case "email":
    95  		for _, email := range cert.EmailAddresses {
    96  			if email == value {
    97  				return nil
    98  			}
    99  		}
   100  	case "ipaddress":
   101  		for _, ip := range cert.IPAddresses {
   102  			if ip.String() == value {
   103  				return nil
   104  			}
   105  		}
   106  	case "uri":
   107  		for _, uri := range cert.URIs {
   108  			if uri.String() == value {
   109  				return nil
   110  			}
   111  		}
   112  	default:
   113  		return fmt.Errorf("unknown SAN type: %s", sanType)
   114  	}
   115  	return fmt.Errorf("SAN not found: %s", value)
   116  }
   117  
   118  // The x509 package/module doesn't export these.
   119  // they are derived from https://www.rfc-editor.org/rfc/rfc5280 RFC 5280, 4.2.1.12  Extended Key Usage
   120  // and can never change
   121  
   122  var (
   123  	oidExtKeyUsageAny                            = asn1.ObjectIdentifier{2, 5, 29, 37, 0}
   124  	oidExtKeyUsageServerAuth                     = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 1}
   125  	oidExtKeyUsageClientAuth                     = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 2}
   126  	oidExtKeyUsageCodeSigning                    = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 3}
   127  	oidExtKeyUsageEmailProtection                = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 4}
   128  	oidExtKeyUsageIPSECEndSystem                 = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 5}
   129  	oidExtKeyUsageIPSECTunnel                    = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 6}
   130  	oidExtKeyUsageIPSECUser                      = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 7}
   131  	oidExtKeyUsageTimeStamping                   = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 8}
   132  	oidExtKeyUsageOCSPSigning                    = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 9}
   133  	oidExtKeyUsageMicrosoftServerGatedCrypto     = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 10, 3, 3}
   134  	oidExtKeyUsageNetscapeServerGatedCrypto      = asn1.ObjectIdentifier{2, 16, 840, 1, 113730, 4, 1}
   135  	oidExtKeyUsageMicrosoftCommercialCodeSigning = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 2, 1, 22}
   136  	oidExtKeyUsageMicrosoftKernelCodeSigning     = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 61, 1, 1}
   137  )
   138  
   139  var extKeyUsageOIDs = []struct {
   140  	extKeyUsage x509.ExtKeyUsage
   141  	oid         asn1.ObjectIdentifier
   142  }{
   143  	{x509.ExtKeyUsageAny, oidExtKeyUsageAny},
   144  	{x509.ExtKeyUsageServerAuth, oidExtKeyUsageServerAuth},
   145  	{x509.ExtKeyUsageClientAuth, oidExtKeyUsageClientAuth},
   146  	{x509.ExtKeyUsageCodeSigning, oidExtKeyUsageCodeSigning},
   147  	{x509.ExtKeyUsageEmailProtection, oidExtKeyUsageEmailProtection},
   148  	{x509.ExtKeyUsageIPSECEndSystem, oidExtKeyUsageIPSECEndSystem},
   149  	{x509.ExtKeyUsageIPSECTunnel, oidExtKeyUsageIPSECTunnel},
   150  	{x509.ExtKeyUsageIPSECUser, oidExtKeyUsageIPSECUser},
   151  	{x509.ExtKeyUsageTimeStamping, oidExtKeyUsageTimeStamping},
   152  	{x509.ExtKeyUsageOCSPSigning, oidExtKeyUsageOCSPSigning},
   153  	{x509.ExtKeyUsageMicrosoftServerGatedCrypto, oidExtKeyUsageMicrosoftServerGatedCrypto},
   154  	{x509.ExtKeyUsageNetscapeServerGatedCrypto, oidExtKeyUsageNetscapeServerGatedCrypto},
   155  	{x509.ExtKeyUsageMicrosoftCommercialCodeSigning, oidExtKeyUsageMicrosoftCommercialCodeSigning},
   156  	{x509.ExtKeyUsageMicrosoftKernelCodeSigning, oidExtKeyUsageMicrosoftKernelCodeSigning},
   157  }
   158  
   159  func OidFromExtKeyUsage(eku x509.ExtKeyUsage) (oid asn1.ObjectIdentifier, ok bool) {
   160  	for _, pair := range extKeyUsageOIDs {
   161  		if eku == pair.extKeyUsage {
   162  			return pair.oid, true
   163  		}
   164  	}
   165  	return
   166  }
   167  
   168  // did:x509 examples
   169  //
   170  //      did:x509:0:sha256:WE4P5dd8DnLHSkyHaIjhp4udlkF9LqoKwCvu9gl38jk::subject:C:US:ST:California:L:San%20Francisco:O:GitHub%2C%20Inc.
   171  //      did:x509:0:sha256:I5ni_nuWegx4NiLaeGabiz36bDUhDDiHEFl8HXMA_4o::subject:CN:Test%20Leaf%20%28DO%20NOT%20TRUST%20
   172  //
   173  // see https://github.com/microsoft/did-x509/blob/main/specification.md and https://www.w3.org/TR/2022/REC-did-core-20220719/
   174  
   175  // check that the did "did" matches the public cert chain "chain"
   176  func verifyDid(chain []*x509.Certificate, did string) error {
   177  	var topTokens = strings.Split(did, "::")
   178  
   179  	if len(topTokens) <= 1 {
   180  		return errors.New("invalid DID string")
   181  	}
   182  
   183  	var pretokens = strings.Split(topTokens[0], ":")
   184  
   185  	if len(pretokens) < 5 || pretokens[0] != "did" || pretokens[1] != "x509" {
   186  		return errors.New("unsupported method/prefix")
   187  	}
   188  
   189  	if pretokens[2] != "0" {
   190  		return errors.New("unsupported did:x509 version")
   191  	}
   192  
   193  	caFingerprintAlg := pretokens[3]
   194  	caFingerprint := pretokens[4]
   195  
   196  	policies := topTokens[1:]
   197  
   198  	if len(chain) < 2 {
   199  		return errors.New("certificate chain too short")
   200  	}
   201  
   202  	err := checkFingerprint(chain, caFingerprintAlg, caFingerprint)
   203  	if err != nil {
   204  		return err
   205  	}
   206  
   207  	for _, policy := range policies {
   208  		parts := strings.Split(policy, ":")
   209  
   210  		if len(parts) < 2 {
   211  			return errors.New("invalid policy")
   212  		}
   213  
   214  		policyName, args := parts[0], parts[1:]
   215  		switch policyName {
   216  		case "subject":
   217  			if len(args) == 0 || len(args)%2 != 0 {
   218  				return errors.New("key-value pairs required")
   219  			}
   220  
   221  			if len(args) < 2 {
   222  				return errors.New("at least one key-value pair is required")
   223  			}
   224  
   225  			// Walk the x509 subject description (a list of key:value pairs like "CN:ContainerPlat" saying the subject common
   226  			// name is ContainerPlat) extracting the various fields and checking they do not occur more than once.
   227  			var seenFields []string
   228  			for i := 0; i < len(args); i += 2 {
   229  				k := strings.ToUpper(args[i])
   230  				v, err := url.QueryUnescape(args[i+1])
   231  
   232  				if err != nil {
   233  					return fmt.Errorf("urlUnescape failed: %w", err)
   234  				}
   235  
   236  				for _, sk := range seenFields {
   237  					if sk == k {
   238  						return fmt.Errorf("duplicate field '%s'", k)
   239  					}
   240  				}
   241  				seenFields = append(seenFields, k)
   242  
   243  				leaf := chain[0]
   244  				var fieldValues []string
   245  				switch k {
   246  				case "C":
   247  					fieldValues = leaf.Subject.Country
   248  				case "O":
   249  					fieldValues = leaf.Subject.Organization
   250  				case "OU":
   251  					fieldValues = leaf.Subject.OrganizationalUnit
   252  				case "L":
   253  					fieldValues = leaf.Subject.Locality
   254  				case "S":
   255  					fieldValues = leaf.Subject.Province
   256  				case "STREET":
   257  					fieldValues = leaf.Subject.StreetAddress
   258  				case "POSTALCODE":
   259  					fieldValues = leaf.Subject.PostalCode
   260  				case "SERIALNUMBER":
   261  					fieldValues = []string{leaf.Subject.SerialNumber}
   262  				case "CN":
   263  					fieldValues = []string{leaf.Subject.CommonName}
   264  				default:
   265  					for _, aav := range leaf.Subject.Names {
   266  						if aav.Type.String() == k {
   267  							fieldValues = []string{aav.Value.(string)}
   268  							break
   269  						}
   270  					}
   271  					if len(fieldValues) == 0 {
   272  						return fmt.Errorf("unsupported subject key: %s", k)
   273  					}
   274  				}
   275  				found := false
   276  				for _, fv := range fieldValues {
   277  					if fv == v {
   278  						found = true
   279  						break
   280  					}
   281  				}
   282  				if !found {
   283  					return fmt.Errorf("invalid subject value: %s=%s", k, v)
   284  				}
   285  			}
   286  		case "san":
   287  			if len(args) != 2 {
   288  				return fmt.Errorf("exactly one SAN type and value required")
   289  			}
   290  			sanType := args[0]
   291  			sanValue, err := url.QueryUnescape(args[1])
   292  			if err != nil {
   293  				return fmt.Errorf("url.QueryUnescape failed: %w", err)
   294  			}
   295  			err = checkHasSan(sanType, sanValue, chain[0])
   296  			if err != nil {
   297  				return err
   298  			}
   299  		case "eku":
   300  			if len(args) != 1 {
   301  				return errors.New("exactly one EKU required")
   302  			}
   303  
   304  			ekuOid, err := oidFromString(args[0])
   305  			if err != nil {
   306  				return fmt.Errorf("oidFromString failed: %w", err)
   307  			}
   308  
   309  			if len(chain[0].UnknownExtKeyUsage) == 0 {
   310  				return errors.New("no EKU extension in certificate")
   311  			}
   312  
   313  			foundEku := false
   314  			for _, certEku := range chain[0].ExtKeyUsage {
   315  				certEkuOid, ok := OidFromExtKeyUsage(certEku)
   316  				if ok && certEkuOid.Equal(*ekuOid) {
   317  					foundEku = true
   318  					break
   319  				}
   320  			}
   321  			for _, certEkuOid := range chain[0].UnknownExtKeyUsage {
   322  				if certEkuOid.Equal(*ekuOid) {
   323  					foundEku = true
   324  					break
   325  				}
   326  			}
   327  
   328  			if !foundEku {
   329  				return fmt.Errorf("EKU not found: %s", ekuOid)
   330  			}
   331  		case "fulcio-issuer":
   332  			if len(args) != 1 {
   333  				return errors.New("excessive arguments to fulcio-issuer")
   334  			}
   335  			decodedArg, err := url.QueryUnescape(args[0])
   336  			if err != nil {
   337  				return fmt.Errorf("urlUnescape failed: %w", err)
   338  			}
   339  			fulcioIssuer := "https://" + decodedArg
   340  			fulcioIssuerOid, err := oidFromString("1.3.6.1.4.1.57264.1.1")
   341  			if err != nil {
   342  				return fmt.Errorf("oidFromString failed: %w", err)
   343  			}
   344  			found := false
   345  			for _, ext := range chain[0].Extensions {
   346  				if ext.Id.Equal(*fulcioIssuerOid) {
   347  					if string(ext.Value) == fulcioIssuer {
   348  						found = true
   349  						break
   350  					}
   351  				}
   352  			}
   353  			if !found {
   354  				return fmt.Errorf("invalid fulcio-issuer: %s", fulcioIssuer)
   355  			}
   356  		default:
   357  			return fmt.Errorf("unsupported did:x509 policy name '%s'", policyName)
   358  		}
   359  	}
   360  
   361  	return nil
   362  }
   363  
   364  func createDidDocument(did string, chain []*x509.Certificate) (string, error) {
   365  	format := `
   366  {
   367  	"@context": "https://www.w3.org/ns/did/v1",
   368  	"id": "%s",
   369  	"verificationMethod": [{
   370  		"id": "%[1]s#key-1",
   371  		"type": "JsonWebKey2020",
   372  		"controller": "%[1]s",
   373  		"publicKeyJwk": %s,
   374  	}]
   375  	%s
   376  	%s
   377  }`
   378  
   379  	includeAssertionMethod := chain[0].KeyUsage == 0 || (chain[0].KeyUsage&x509.KeyUsageDigitalSignature) != 0
   380  	includeKeyAgreement := chain[0].KeyUsage == 0 || (chain[0].KeyUsage&x509.KeyUsageKeyAgreement) != 0
   381  
   382  	if !includeAssertionMethod && !includeKeyAgreement {
   383  		return "", errors.New("leaf certificate key usage must include digital signature or key agreement")
   384  	}
   385  
   386  	am := ""
   387  	ka := ""
   388  	if includeAssertionMethod {
   389  		am = fmt.Sprintf(",\"assertionMethod\": \"%s#key-1\"", did)
   390  	}
   391  	if includeKeyAgreement {
   392  		ka = fmt.Sprintf(",\"keyAgreement\": \"%s#key-1\"", did)
   393  	}
   394  
   395  	leaf, err := jwk.New(chain[0].PublicKey)
   396  	if err != nil {
   397  		return "", err
   398  	}
   399  	jleaf, err := json.Marshal(leaf)
   400  	if err != nil {
   401  		return "", err
   402  	}
   403  	doc := fmt.Sprintf(format, did, jleaf, am, ka)
   404  	return doc, nil
   405  }
   406  
   407  func parsePemChain(chainPem string) ([]*x509.Certificate, error) {
   408  	var chain = []*x509.Certificate{}
   409  
   410  	bs := []byte(chainPem)
   411  	for block, rest := pem.Decode(bs); block != nil; block, rest = pem.Decode(rest) {
   412  		if block.Type == "CERTIFICATE" {
   413  			cert, err := x509.ParseCertificate(block.Bytes)
   414  			if err != nil {
   415  				return []*x509.Certificate{}, fmt.Errorf("certificate parser failed: %w", err)
   416  			}
   417  			chain = append(chain, cert)
   418  		}
   419  	}
   420  
   421  	return chain, nil
   422  }
   423  
   424  func Resolve(chainPem string, did string, ignoreTime bool) (string, error) {
   425  	chain, err := parsePemChain(chainPem)
   426  
   427  	if err != nil {
   428  		return "", err
   429  	}
   430  
   431  	if len(chain) == 0 {
   432  		return "", errors.New("no certificate chain")
   433  	}
   434  
   435  	// The last certificate in the chain is assumed to be the trusted root.
   436  	roots := []*x509.Certificate{chain[len(chain)-1]}
   437  
   438  	chains, err := VerifyCertificateChain(chain, roots, ignoreTime)
   439  
   440  	if err != nil {
   441  		return "", fmt.Errorf("certificate chain verification failed: %w", err)
   442  	}
   443  
   444  	for _, chain := range chains {
   445  		err = verifyDid(chain, did)
   446  		if err != nil {
   447  			return "", fmt.Errorf("DID verification failed: %w", err)
   448  		}
   449  	}
   450  
   451  	doc, err := createDidDocument(did, chain)
   452  
   453  	if err != nil {
   454  		return "", fmt.Errorf("DID document creation failed: %w", err)
   455  	}
   456  
   457  	return doc, nil
   458  }
   459  

View as plain text