SSH Certificate Authority Toolkit
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

291 lines
7.7 KiB

5 years ago
5 years ago
5 years ago
5 years ago
  1. package main
  2. /*
  3. Signs all host keys.
  4. */
  5. import (
  6. "bufio"
  7. "bytes"
  8. "encoding/json"
  9. "fmt"
  10. "io"
  11. "io/ioutil"
  12. "net/http"
  13. "os"
  14. "strconv"
  15. "strings"
  16. "syscall"
  17. "time"
  18. "github.com/urfave/cli"
  19. )
  20. // Options available via a signing request.
  21. type httpSignRequest struct {
  22. Environment string
  23. APIKey string
  24. Type string
  25. KeyID string
  26. ValidPrincipals []string
  27. Options map[string]string
  28. Extensions map[string]string
  29. PublicKeys []string
  30. Duration time.Duration
  31. }
  32. // The standard API responses.
  33. type httpSignResponse struct {
  34. Successful bool
  35. Message string
  36. SignedKeys []string
  37. }
  38. // Flags for the sign command.
  39. func signFlags() []cli.Flag {
  40. return []cli.Flag{
  41. cli.StringFlag{Name: "key, k"},
  42. }
  43. }
  44. // The sign command calls this function.
  45. func sign(c *cli.Context) error {
  46. // Load the configuration.
  47. config := initConfig(c)
  48. // If we want to just sign a single key, pass it via the argument.
  49. hostKey := c.String("key")
  50. // All host key files.
  51. var hostKeys []string
  52. // If a host key file was provided, we just use it. Otherwise we find system host keys.
  53. if hostKey != "" {
  54. hostKeys = append(hostKeys, hostKey)
  55. } else {
  56. // Host keys are stored in /etc/ssh/.
  57. files, err := ioutil.ReadDir("/etc/ssh/")
  58. if err != nil {
  59. return err
  60. }
  61. // Host keys end in .pub, but are not certificates.
  62. for _, f := range files {
  63. if strings.Contains(f.Name(), ".pub") && !strings.Contains(f.Name(), "-cert") && f.Name() != "ssh_host_key.pub" {
  64. hostKeys = append(hostKeys, "/etc/ssh/"+f.Name())
  65. }
  66. }
  67. }
  68. // Setup a signing request.
  69. sr := httpSignRequest{}
  70. sr.Environment = config.SignOptions.Environment
  71. sr.APIKey = config.SignOptions.APIKey
  72. sr.Type = "host"
  73. sr.KeyID = config.SignOptions.KeyID
  74. sr.Duration = config.SignOptions.Duration
  75. // If a key ID is not set in the configuration, we will use the hostname.
  76. if sr.KeyID == "" {
  77. sr.KeyID, _ = os.Hostname()
  78. }
  79. // Read the host keys into the signing request.
  80. for _, hostKey := range hostKeys {
  81. // Read the host public key.
  82. pubKeyFile, err := ioutil.ReadFile(hostKey)
  83. if err != nil {
  84. return fmt.Errorf("Unable to read public key: %v", err)
  85. }
  86. sr.PublicKeys = append(sr.PublicKeys, string(pubKeyFile))
  87. }
  88. // Convert signing request into JSON for the request.
  89. srData, err := json.Marshal(sr)
  90. if err != nil {
  91. return err
  92. }
  93. // Setup request.
  94. req, _ := http.NewRequest("POST", config.CertServer+"/sign", bytes.NewBuffer(srData))
  95. req.Header.Add("content-type", "application/json")
  96. // Send the request.
  97. res, err := http.DefaultClient.Do(req)
  98. if err != nil {
  99. return err
  100. }
  101. defer res.Body.Close()
  102. // Parse the response.
  103. var signResponse httpSignResponse
  104. decoder := json.NewDecoder(res.Body)
  105. err = decoder.Decode(&signResponse)
  106. if err != nil {
  107. return err
  108. }
  109. // If successful, we need to pull the signed keys and store them/update the sshd configurations.
  110. if signResponse.Successful {
  111. // The configuration file path.
  112. sshdConfig := "/etc/ssh/sshd_config"
  113. // Open the sshd_config file.
  114. sshdConfigFile, err := os.Open(sshdConfig)
  115. if err != nil {
  116. return err
  117. }
  118. defer sshdConfigFile.Close()
  119. configReader := bufio.NewReader(sshdConfigFile)
  120. // We save our changes to the _new configuration file temporarily during edits.
  121. newConfig, err := os.Create(sshdConfig + "_new")
  122. if err != nil {
  123. return err
  124. }
  125. // We go through the file until all host key configurations are found.
  126. // Once found, we then insert our certificate configurations.
  127. foundHostKeys := false
  128. // This is the last line read that is not a host key configuration.
  129. // We need to write this line after adding the certificate configurations.
  130. var lastReadLine string
  131. for {
  132. // Read a line.
  133. line, err := configReader.ReadString('\n')
  134. // If end of line, we are done reading.
  135. if err == io.EOF {
  136. break
  137. }
  138. // If error, something is wrong.
  139. if err != nil {
  140. return err
  141. }
  142. // Configurations in sshd_config is white space separated. If a whitepsace is not found, it is not a configuration line.
  143. i := strings.IndexByte(line, ' ')
  144. if i != -1 {
  145. // We pull the configuration name.
  146. conf := line[:i]
  147. // If we find a Match configuration, we want to stop here as anything below this line is specific to the match.
  148. if conf == "Match" {
  149. lastReadLine = line
  150. break
  151. }
  152. // If we found the host keys already, we check to see if this line is another host key or host certificate.
  153. // If it is not, we are done reading at this point and we need to store the line for writing after we isnert our config.
  154. if foundHostKeys && conf != "HostKey" && conf != "HostCertificate" {
  155. lastReadLine = line
  156. break
  157. }
  158. // If this is a host certificate configuration, we need to ignore it.
  159. if conf == "HostCertificate" {
  160. continue
  161. }
  162. // If this is a host key configuration, we need to set the fact we found the host key configurations.
  163. if conf == "HostKey" {
  164. foundHostKeys = true
  165. }
  166. }
  167. // Write this line to the new configuration.
  168. newConfig.WriteString(line)
  169. }
  170. // Go through each of the signed keys, and save them/add to the sshd configuration.
  171. for i, signedKey := range signResponse.SignedKeys {
  172. // The signed key result should be in the same order we sent them,
  173. // that means that the host key files will be in the same order.
  174. // The name of the host certificate file is the same as the public key,
  175. // but with `-cert` pre-pended to the extension.
  176. certKeyFile := strings.Replace(hostKeys[i], ".pub", "-cert.pub", 1)
  177. // Save the certificate file.
  178. f, err := os.Create(certKeyFile)
  179. if err != nil {
  180. return err
  181. }
  182. f.WriteString(signedKey)
  183. f.Close()
  184. os.Chmod(certKeyFile, 0644)
  185. // Append to the new sshd configuration file the certificate configuration.
  186. newConfig.WriteString("HostCertificate " + certKeyFile + "\n")
  187. }
  188. // Append the line we last read before we inserted our host certificates.
  189. newConfig.WriteString(lastReadLine)
  190. for {
  191. // Read the next line available.
  192. line, err := configReader.ReadString('\n')
  193. // If end of line, we are done reading.
  194. if err == io.EOF {
  195. break
  196. }
  197. // If error, something is wrong.
  198. if err != nil {
  199. return err
  200. }
  201. // Configurations in sshd_config is white space separated. If a whitepsace is not found, it is not a configuration line.
  202. i := strings.IndexByte(line, ' ')
  203. if i != -1 {
  204. // We pull the configuration name.
  205. conf := line[:i]
  206. // If this is a host certificate configuration, we need to ignore it.
  207. if conf == "HostCertificate" {
  208. continue
  209. }
  210. }
  211. // Write line to the new sshd configuration file.
  212. newConfig.WriteString(line)
  213. }
  214. // Check new configuration.
  215. newConfig.Close()
  216. fileinfo, err := os.Stat(sshdConfig + "_new")
  217. if err != nil {
  218. return err
  219. }
  220. // If new configuration is smaller than 256 bytes, something happened...
  221. if fileinfo.Size() <= 256 {
  222. os.Remove(sshdConfig + "_new")
  223. return fmt.Errorf("File size of new ssd_config is too small.")
  224. }
  225. // We can now replace the old configuration with new modified configuration.
  226. err = os.Rename(sshdConfig+"_new", sshdConfig)
  227. if err != nil {
  228. return err
  229. }
  230. // We need the PID of sshd so that we can signal it to re-load its configuration file.
  231. pidB, err := ioutil.ReadFile("/var/run/sshd.pid")
  232. if err != nil {
  233. return err
  234. }
  235. pid, err := strconv.Atoi(string(pidB[:len(pidB)-1]))
  236. if err != nil {
  237. return err
  238. }
  239. // Find the active process for SSHD based on its pid.
  240. sshd, err := os.FindProcess(pid)
  241. if err != nil {
  242. return err
  243. }
  244. // Signal SSHD to reload its configuration file.
  245. sshd.Signal(syscall.SIGHUP)
  246. } else {
  247. // The request was not successful, so there is likely a message with an error.
  248. fmt.Println(signResponse.Message)
  249. }
  250. return nil
  251. }