...

Source file src/github.com/Microsoft/hcsshim/internal/cosesign1/check.go

Documentation: github.com/Microsoft/hcsshim/internal/cosesign1

     1  package cosesign1
     2  
     3  import (
     4  	"crypto/x509"
     5  	"fmt"
     6  
     7  	didx509resolver "github.com/Microsoft/hcsshim/internal/did-x509-resolver"
     8  
     9  	"github.com/sirupsen/logrus"
    10  
    11  	"github.com/veraison/go-cose"
    12  )
    13  
    14  // helper functions to check that elements in the protected header are
    15  // of the expected types.
    16  
    17  // issuer and feed MUST be strings or not present
    18  func getStringValue(m cose.ProtectedHeader, k any) string {
    19  	val, ok := m[k]
    20  	if !ok {
    21  		return ""
    22  	}
    23  	str, ok := val.(string)
    24  	if !ok {
    25  		return ""
    26  	}
    27  	return str
    28  }
    29  
    30  // See https://datatracker.ietf.org/doc/draft-ietf-cose-x509/09/ x5chain section,
    31  // The "chain" can be an array of arrays of bytes or just a single array of bytes
    32  // in the single cert case. Each of these two functions handles of of these cases
    33  
    34  // a DER chain is an array of arrays of bytes
    35  func isDERChain(val interface{}) bool {
    36  	valArray, ok := val.([]interface{})
    37  	if !ok {
    38  		return false
    39  	}
    40  
    41  	for _, element := range valArray {
    42  		_, ok = element.([]byte)
    43  		if !ok {
    44  			return false
    45  		}
    46  	}
    47  
    48  	return len(valArray) > 0
    49  }
    50  
    51  // a DER is an array of bytes
    52  func isDEROnly(val interface{}) bool {
    53  	_, ok := val.([]byte)
    54  	return ok
    55  }
    56  
    57  type UnpackedCoseSign1 struct {
    58  	Issuer      string
    59  	Feed        string
    60  	ContentType string
    61  	Pubkey      string
    62  	Pubcert     string
    63  	ChainPem    string
    64  	Payload     []byte
    65  	CertChain   []*x509.Certificate
    66  }
    67  
    68  // This function is rather unpleasant in that it both decodes the COSE Sign1 document and its various
    69  // crypto parts AND checks that those parts are sound in this context. Higher layers may yet refuse the
    70  // payload for reasons beyond the scope of the checking of the document itself.
    71  // While this function could be decomposed into "unpack" and "verify" there would need to be extra state,
    72  // such as the cert pools, stored in some state object. Then the sensible pattern would be to have
    73  // accessors and member functions such as "verity()". However that was done there could exist state objects
    74  // for badly formed COSE Sign1 documents and that would complicate the jobs of callers.
    75  //
    76  // raw: an array of bytes comprising the COSE Sign1 document.
    77  func UnpackAndValidateCOSE1CertChain(raw []byte) (*UnpackedCoseSign1, error) {
    78  	var msg cose.Sign1Message
    79  	err := msg.UnmarshalCBOR(raw)
    80  	if err != nil {
    81  		return nil, err
    82  	}
    83  
    84  	protected := msg.Headers.Protected
    85  	val, ok := protected[cose.HeaderLabelAlgorithm]
    86  	if !ok {
    87  		return nil, fmt.Errorf("algorithm missing")
    88  	}
    89  
    90  	algo, ok := val.(cose.Algorithm)
    91  	if !ok {
    92  		return nil, fmt.Errorf("algorithm wrong type")
    93  	}
    94  
    95  	logrus.Debugf("COSE Sign1 unpack: algorithm %d", algo)
    96  
    97  	// The spec says this is ordered - leaf, intermediates, root. X5Bag is unordered and would need sorting
    98  	chainDER, ok := protected[cose.HeaderLabelX5Chain]
    99  
   100  	if !ok {
   101  		return nil, fmt.Errorf("x5Chain missing")
   102  	}
   103  
   104  	var chainIA []interface{}
   105  
   106  	// The HeaderLabelX5Chain entry in the cose header may be a blob (single cert) or an array of blobs (a chain) see https://datatracker.ietf.org/doc/draft-ietf-cose-x509/08/
   107  	if isDERChain(chainDER) {
   108  		chainIA = chainDER.([]interface{})
   109  	} else if isDEROnly(chainDER) {
   110  		chainIA = append(chainIA, chainDER)
   111  	} else {
   112  		return nil, fmt.Errorf("x5Chain wrong type")
   113  	}
   114  
   115  	var chain []*x509.Certificate
   116  
   117  	for index, element := range chainIA {
   118  		cert, err := x509.ParseCertificate(element.([]byte))
   119  		if err == nil {
   120  			chain = append(chain, cert)
   121  		} else {
   122  			logrus.Debugf("Parse certificate failed on %d: %s", index, err.Error())
   123  			return nil, err
   124  		}
   125  	}
   126  
   127  	// A reasonable chain will have a handful of certs, typically 3 or 4,
   128  	// so limit to an arbitary 100 which would be quite unreasonable
   129  	chainLen := len(chain)
   130  	if chainLen > 100 || chainLen < 1 {
   131  		return nil, fmt.Errorf("unreasonable number of certs (%d) in COSE_Sign1 document", chainLen)
   132  	}
   133  
   134  	var leafCert = chain[0]
   135  	var leafCertBase64 = x509ToBase64(leafCert)
   136  	var leafPubKey = leafCert.PublicKey
   137  	var leafPubKeyBase64 = keyToBase64(leafPubKey)
   138  
   139  	var chainPEM string
   140  	for i, c := range chain {
   141  		if i > 0 {
   142  			chainPEM += "\n"
   143  		}
   144  		chainPEM += convertx509ToPEM(c)
   145  	}
   146  
   147  	logrus.Debugln("Certificate chain:")
   148  	logrus.Debugln(chainPEM)
   149  
   150  	// First check that the chain is itself good.
   151  	// Note that a single cert would fail here as it
   152  	// falls back to the system root certs which will
   153  	// never have issued the leaf cert.
   154  
   155  	if len(chain) > 1 {
   156  		// Any valid cert chain is good for us as we will be matching
   157  		// part of the chain with a customer provided cert fingerprint.
   158  		_, err = didx509resolver.VerifyCertificateChain(chain, chain[len(chain)-1:], false)
   159  
   160  		if err != nil {
   161  			return nil, fmt.Errorf("certificate chain verification failed - %w", err)
   162  		}
   163  	}
   164  
   165  	// Next check that the signature over the document was made with the private key matching the
   166  	// public key we extracted from the leaf cert.
   167  
   168  	verifier, err := cose.NewVerifier(algo, leafPubKey)
   169  	if err != nil {
   170  		logrus.Debugf("cose.NewVerifier failed (algo %d): %s", algo, err.Error())
   171  		return nil, err
   172  	}
   173  
   174  	err = msg.Verify(nil, verifier)
   175  	if err != nil {
   176  		logrus.Debugf("msg.Verify failed: algo = %d err = %s", algo, err.Error())
   177  		return nil, err
   178  	}
   179  
   180  	issuer := getStringValue(protected, "iss")
   181  	feed := getStringValue(protected, "feed")
   182  	contenttype := getStringValue(protected, cose.HeaderLabelContentType)
   183  
   184  	return &UnpackedCoseSign1{
   185  		Pubcert:     leafCertBase64,
   186  		Feed:        feed,
   187  		Issuer:      issuer,
   188  		Pubkey:      leafPubKeyBase64,
   189  		ChainPem:    chainPEM,
   190  		ContentType: contenttype,
   191  		Payload:     msg.Payload,
   192  		CertChain:   chain,
   193  	}, nil
   194  }
   195  

View as plain text