github.com/grailbio/base@v0.0.11/security/ssh/certificateauthority/ssh.go (about)

     1  // Copyright 2018 GRAIL, Inc. All rights reserved.
     2  // Use of this source code is governed by the Apache-2.0
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package certificateauthority implements an x509 certificate authority.
     6  package certificateauthority
     7  
     8  import (
     9  	"crypto/rand"
    10  
    11  	"golang.org/x/crypto/ssh"
    12  
    13  	"github.com/grailbio/base/security/keycrypt"
    14  
    15  	"errors"
    16  	"time"
    17  )
    18  
    19  // CertificateAuthority is a ssh certificate authority.
    20  type CertificateAuthority struct {
    21  	// The amount of allowable clock drift between the systems between
    22  	// which certificates are exchanged.
    23  	DriftMargin time.Duration
    24  
    25  	// The keycrypt secret that contains the PEM-encoded private key.
    26  	PrivateKey keycrypt.Secret
    27  
    28  	// Contains the PEM-encoded Certificate.
    29  	Certificate string
    30  
    31  	// The ssh certificate signer. Populated by Init().
    32  	Signer ssh.Signer
    33  }
    34  
    35  type CertificateRequest struct {
    36  	// SSH Public Key that is being signed
    37  	SshPublicKey []byte
    38  
    39  	// List of host names, or usernames that will be added to the cert
    40  	Principals []string
    41  
    42  	// How long this certificate should be valid for
    43  	Ttl time.Duration
    44  
    45  	// What identifier should be included in the request
    46  	// This value will be used in logging
    47  	KeyID string
    48  
    49  	CertType string // either "user" or "host"
    50  
    51  	CriticalOptions []string
    52  
    53  	// Extensions to assign to the ssh Certificate
    54  	// The default allow basic function - permit-pty is usually required
    55  	// map[string]string{
    56  	//     "permit-X11-forwarding":   "",
    57  	//     "permit-agent-forwarding": "",
    58  	//     "permit-port-forwarding":  "",
    59  	//     "permit-pty":              "",
    60  	//     "permit-user-rc":          "",
    61  	// }
    62  	Extensions []string
    63  }
    64  
    65  const sshSignAlg = ssh.SigAlgoRSASHA2256
    66  
    67  func validateCertType(certType string) (uint32, error) {
    68  	switch certType {
    69  	case "user":
    70  		return ssh.UserCert, nil
    71  	case "host":
    72  		return ssh.HostCert, nil
    73  	}
    74  	return 0, errors.New("CertType must be either 'user' or 'host'")
    75  }
    76  
    77  // Init initializes the certificate authority. Init extracts the
    78  // authority certificate and private key from ca.Signer.
    79  func (ca *CertificateAuthority) Init() error {
    80  	pkPemBlock, err := ca.PrivateKey.Get()
    81  	if err != nil {
    82  		return err
    83  	}
    84  
    85  	// Load the private key
    86  	privateSigner, err := ssh.ParsePrivateKey(pkPemBlock)
    87  	if err != nil {
    88  		return err
    89  	}
    90  
    91  	// Load the Certificate
    92  	certificate, _, _, _, err := ssh.ParseAuthorizedKey([]byte(ca.Certificate))
    93  	if err != nil {
    94  		return err
    95  	}
    96  	// Link the private key with its matching Authority Certificate
    97  	ca.Signer, err = ssh.NewCertSigner(certificate.(*ssh.Certificate), privateSigner)
    98  	if err != nil {
    99  		return err
   100  	}
   101  
   102  	ca.Signer = privateSigner
   103  
   104  	return nil
   105  }
   106  
   107  func (ca CertificateAuthority) IssueWithKeyUsage(cr CertificateRequest) (string, error) {
   108  	return ca.issueWithKeyUsage(time.Now(), cr)
   109  }
   110  
   111  func (ca CertificateAuthority) issueWithKeyUsage(now time.Time, cr CertificateRequest) (string, error) {
   112  
   113  	// Load the Certificate
   114  	pubKey, _, _, _, err := ssh.ParseAuthorizedKey(cr.SshPublicKey)
   115  	if err != nil {
   116  		return "", err
   117  	}
   118  
   119  	now = now.Add(-ca.DriftMargin)
   120  
   121  	certType, err := validateCertType(cr.CertType)
   122  	if err != nil {
   123  		return "", err
   124  	}
   125  
   126  	certificate := &ssh.Certificate{
   127  		Serial:          0,
   128  		Key:             pubKey,
   129  		KeyId:           cr.KeyID,      // Descriptive name of the key (shown in logs)
   130  		ValidPrincipals: cr.Principals, // hostnames (for host cert), or usernames (for client cert)
   131  		ValidAfter:      uint64(now.In(time.UTC).Unix()),
   132  		ValidBefore:     uint64(now.Add(ca.DriftMargin + cr.Ttl).In(time.UTC).Unix()),
   133  		CertType:        certType, // int representing a "user" or "host" type
   134  		Permissions: ssh.Permissions{
   135  			CriticalOptions: convertArrayToMap(cr.CriticalOptions),
   136  			Extensions:      convertArrayToMap(cr.Extensions),
   137  		},
   138  	}
   139  
   140  	// Replicate the certificate.SignCert functions but with a custom algorithm
   141  	certificate.Nonce = make([]byte, 32)
   142  	if _, err = rand.Read(certificate.Nonce); err != nil {
   143  		return "", err
   144  	}
   145  	certificate.SignatureKey = ca.Signer.PublicKey()
   146  
   147  	// based on Certificate.bytesForSigning()
   148  	certificateBytes := certificate.Marshal()
   149  	// Drop trailing signature length
   150  	certificateBytes = certificateBytes[:len(certificateBytes)-4]
   151  
   152  	certificate.Signature, err = ca.Signer.(ssh.AlgorithmSigner).SignWithAlgorithm(rand.Reader, certificateBytes, sshSignAlg)
   153  	if err != nil {
   154  		return "", err
   155  	}
   156  
   157  	return string(ssh.MarshalAuthorizedKey(certificate)), err
   158  }
   159  
   160  // Convert an array of strings into a map of string value pairs
   161  // Value of the key is set to "" which is what the SSH Library wants for extensions and CriticalOptions as flags
   162  func convertArrayToMap(initial []string) map[string]string {
   163  	if initial == nil {
   164  		return nil
   165  	}
   166  
   167  	results := map[string]string{}
   168  	for _, key := range initial {
   169  		results[key] = ""
   170  	}
   171  	return results
   172  }