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 }