github.com/microsoft/moc@v0.17.1/pkg/auth/pkv.go (about)

     1  // Copyright (c) Microsoft Corporation.
     2  // Licensed under the Apache v2.0 license.
     3  
     4  package auth
     5  
     6  import (
     7  	"crypto/sha256"
     8  	"crypto/x509"
     9  	"encoding/hex"
    10  	"strings"
    11  
    12  	"github.com/pkg/errors"
    13  )
    14  
    15  const (
    16  	// formatSHA256 is the prefix for pins that are full-length SHA-256 hashes encoded in base 16 (hex)
    17  	formatSHA256 = "sha256"
    18  )
    19  
    20  type publicKeyVerifier struct {
    21  	pubkeypinSet *Set
    22  }
    23  
    24  func NewPublicKeyVerifier() *publicKeyVerifier {
    25  	pkv := &publicKeyVerifier{}
    26  	pkv.pubkeypinSet = NewSet()
    27  	return pkv
    28  }
    29  
    30  func (pkv *publicKeyVerifier) Allow(caCertHash string) error {
    31  	return pkv.pubkeypinSet.Allow(caCertHash)
    32  }
    33  
    34  // VerifyPeerCertificate is a callback to be used for client verification during the TLS handshake
    35  func (c *publicKeyVerifier) VerifyPeerCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
    36  
    37  	x509certs := []*x509.Certificate{}
    38  
    39  	for _, crt := range rawCerts {
    40  		x509cert, err := x509.ParseCertificate(crt)
    41  		if err != nil {
    42  			return errors.Wrapf(err, "bad server certificate")
    43  		}
    44  		x509certs = append(x509certs, x509cert)
    45  	}
    46  
    47  	err := c.pubkeypinSet.CheckAny(x509certs)
    48  	if err != nil {
    49  		return err
    50  	}
    51  	return nil
    52  }
    53  
    54  // NB. This is taken from kubeadm https://github.com/kubernetes/kubernetes/blob/master/cmd/kubeadm/app/util/pubkeypin/pubkeypin.go
    55  // Bringing the moc pkgs are light on dependencies and didn't want to muck that up for a self concerned pkg.
    56  // Also this gives us freedom to tweak to our usecase
    57  
    58  // Set is a set of pinned x509 public keys.
    59  type Set struct {
    60  	sha256Hashes map[string]bool
    61  }
    62  
    63  // NewSet returns a new, empty PubKeyPinSet
    64  func NewSet() *Set {
    65  	return &Set{make(map[string]bool)}
    66  }
    67  
    68  // Allow adds an allowed public key hash to the Set
    69  func (s *Set) Allow(pubKeyHashes ...string) error {
    70  	for _, pubKeyHash := range pubKeyHashes {
    71  		parts := strings.Split(pubKeyHash, ":")
    72  		if len(parts) != 2 {
    73  			return errors.New("invalid public key hash, expected \"format:value\"")
    74  		}
    75  		format, value := parts[0], parts[1]
    76  
    77  		switch strings.ToLower(format) {
    78  		case "sha256":
    79  			return s.allowSHA256(value)
    80  		default:
    81  			return errors.Errorf("unknown hash format %q", format)
    82  		}
    83  	}
    84  	return nil
    85  }
    86  
    87  // CheckAny checks if at least one certificate matches one of the public keys in the set
    88  func (s *Set) CheckAny(certificates []*x509.Certificate) error {
    89  	var hashes []string
    90  
    91  	for _, certificate := range certificates {
    92  		if s.checkSHA256(certificate) {
    93  			return nil
    94  		}
    95  
    96  		hashes = append(hashes, Hash(certificate))
    97  	}
    98  	return errors.Errorf("none of the public keys %q are pinned", strings.Join(hashes, ":"))
    99  }
   100  
   101  // Hash calculates the SHA-256 hash of the Subject Public Key Information (SPKI)
   102  // object in an x509 certificate (in DER encoding). It returns the full hash as a
   103  // hex encoded string (suitable for passing to Set.Allow).
   104  func Hash(certificate *x509.Certificate) string {
   105  	spkiHash := sha256.Sum256(certificate.RawSubjectPublicKeyInfo)
   106  	return formatSHA256 + ":" + strings.ToLower(hex.EncodeToString(spkiHash[:]))
   107  }
   108  
   109  // allowSHA256 validates a "sha256" format hash and adds a canonical version of it into the Set
   110  func (s *Set) allowSHA256(hash string) error {
   111  	// validate that the hash is the right length to be a full SHA-256 hash
   112  	hashLength := hex.DecodedLen(len(hash))
   113  	if hashLength != sha256.Size {
   114  		return errors.Errorf("expected a %d byte SHA-256 hash, found %d bytes", sha256.Size, hashLength)
   115  	}
   116  
   117  	// validate that the hash is valid hex
   118  	_, err := hex.DecodeString(hash)
   119  	if err != nil {
   120  		return err
   121  	}
   122  
   123  	// in the end, just store the original hex string in memory (in lowercase)
   124  	s.sha256Hashes[strings.ToLower(hash)] = true
   125  	return nil
   126  }
   127  
   128  // checkSHA256 returns true if the certificate's "sha256" hash is pinned in the Set
   129  func (s *Set) checkSHA256(certificate *x509.Certificate) bool {
   130  	actualHash := sha256.Sum256(certificate.RawSubjectPublicKeyInfo)
   131  	actualHashHex := strings.ToLower(hex.EncodeToString(actualHash[:]))
   132  	return s.sha256Hashes[actualHashHex]
   133  }