ssh-cert/ssh-user-client/sign.go

573 lines
15 KiB
Go
Raw Normal View History

package main
/*
Signs all host keys.
*/
import (
"bufio"
"bytes"
"crypto"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/pkg/errors"
"github.com/smallstep/cli/crypto/keys"
"github.com/smallstep/cli/crypto/pemutil"
"github.com/urfave/cli"
"golang.org/x/crypto/ssh"
)
// Options available via a signing request.
type httpSignRequest struct {
Environment string
APIKey string
Type string
KeyID string
ValidPrincipals []string
Options map[string]string
Extensions map[string]string
PublicKeys []string
Duration time.Duration
}
// The standard API responses.
type httpSignResponse struct {
Successful bool
Message string
SignedKeys []string
}
// Flags for the sign command.
func signFlags() []cli.Flag {
return []cli.Flag{
cli.StringFlag{Name: "environment, e"},
cli.StringFlag{Name: "key, k"},
}
}
// Group and user data structure.
type Group struct {
Name string
ID uint64
Users []string
}
type User struct {
Name string
ID uint64
GID uint64
FullName string
HomeDir string
Shell string
Disabled bool
}
// UNIX Accounts tool set designed to easily get a group or user's information.
type UNIXAccounts struct {
Groups []*Group
Users []*User
}
// Read the /etc/group and /etc/passwd files to parse information.
func (u *UNIXAccounts) init() error {
// We do not want to parse twice.
if len(u.Groups) != 0 || len(u.Users) != 0 {
return fmt.Errorf("Already parsed accounts.")
}
// Open the group file.
groupFile, err := os.Open("/etc/group")
if err != nil {
return err
}
defer groupFile.Close()
groupReader := bufio.NewReader(groupFile)
for {
// Read a line and truncate it.
line, err := groupReader.ReadString('\n')
line = strings.TrimSuffix(line, "\n")
// If end of line, we are done reading.
if err == io.EOF {
break
}
// If error, something is wrong.
if err != nil {
return err
}
// Ignore comments.
if line[0] == '#' {
continue
}
// Fields are separated with a :.
fields := strings.Split(line, ":")
// Groups should have 4 fields. Nothing more, nothing less.
if len(fields) != 4 {
continue
}
// Parse information.
group := new(Group)
group.Name = fields[0]
group.ID, _ = strconv.ParseUint(fields[2], 10, 32)
group.Users = strings.Split(fields[3], ",")
// Add group to array.
u.Groups = append(u.Groups, group)
}
// Open the user file.
passwdFile, err := os.Open("/etc/passwd")
if err != nil {
return err
}
defer passwdFile.Close()
passwdReader := bufio.NewReader(passwdFile)
for {
// Read a line and truncate it.
line, err := passwdReader.ReadString('\n')
line = strings.TrimSuffix(line, "\n")
// If end of line, we are done.
if err == io.EOF {
break
}
// If error, something is wrong.
if err != nil {
return err
}
// Ignore comments.
if line[0] == '#' {
continue
}
// Fields are separated with a :.
fields := strings.Split(line, ":")
// Users have 7 fields. No more or less.
if len(fields) != 7 {
continue
}
// Prase information.
user := new(User)
user.Name = fields[0]
user.ID, _ = strconv.ParseUint(fields[2], 10, 32)
user.GID, _ = strconv.ParseUint(fields[3], 10, 32)
user.FullName = fields[4]
user.HomeDir = filepath.Clean(fields[5])
user.Shell = fields[6]
// A user is disabled if their shell is set to nologin or false. Users with no shell should also be disabled.
user.Disabled = false
if strings.Contains(user.Shell, "nologin") {
user.Disabled = true
}
if strings.Contains(user.Shell, "false") {
user.Disabled = true
}
if user.Shell == "" {
user.Disabled = true
}
// Add user to array.
u.Users = append(u.Users, user)
}
return nil
}
// Find user info for ID.
func (u *UNIXAccounts) userWithID(id uint64) *User {
for _, user := range u.Users {
if user.ID == id {
return user
}
}
return nil
}
// Find user info for name.
func (u *UNIXAccounts) userWithName(name string) *User {
for _, user := range u.Users {
if user.Name == name {
return user
}
}
return nil
}
// Find group info for ID.
func (u *UNIXAccounts) groupWithID(id uint64) *Group {
for _, group := range u.Groups {
if group.ID == id {
return group
}
}
return nil
}
// Find group info for name.
func (u *UNIXAccounts) groupWithName(name string) *Group {
for _, group := range u.Groups {
if group.Name == name {
return group
}
}
return nil
}
// Get all user accounts which are members of a group.
func (u *UNIXAccounts) usersInGroup(group *Group) []*User {
var users []*User
// Users with the Group ID set to the group's ID are a member.
for _, user := range u.Users {
if user.GID == group.ID {
users = append(users, user)
}
}
// Find user info for each member.
for _, name := range group.Users {
user := u.userWithName(name)
if user == nil {
continue
}
// If the member was added previously, we do not want duplicates.
alreadyExists := false
for _, usr := range users {
if usr == user {
alreadyExists = true
break
}
}
if !alreadyExists {
// The member is not a duplicate, so we add it to the array.
users = append(users, user)
}
}
return users
}
// The sign command calls this function.
func sign(c *cli.Context) error {
// Load the configuration.
config := initConfig(c)
// If we want to just sign a single key, pass it via the argument.
userKey := c.String("key")
// Filter the environments to a single environment.
environmentFilter := c.String("environment")
// If key path set in config or user key is passed in arguments,
// we will ignore environments in configuration and just sign the user key.
if config.SignOptions.KeyPath != "" || userKey != "" {
userPrivKey := config.SignOptions.KeyPath
if userKey != "" {
userPrivKey = userKey
}
userPubKey := userPrivKey + ".pub"
// If the user key was not generated, we need to make a new key.
if _, err := os.Stat(userPrivKey); err != nil {
// Make a key using our default type.
pub, priv, err := keys.GenerateKeyPair(config.KeyDefaults.Type, config.KeyDefaults.Curve, config.KeyDefaults.Size)
if err != nil {
return err
}
// If we cannot sign with the key, something is wrong.
if _, ok := priv.(crypto.Signer); !ok {
return errors.Errorf("key of type %T is not a crypto.Signer", priv)
}
// Get the public key in a form that we can generate an authorized key file.
sshKey, err := ssh.NewPublicKey(pub)
if err != nil {
return errors.Wrapf(err, "error converting public key")
}
// Get the private key in a format which can be saved to disk.
p, err := pemutil.Serialize(priv, pemutil.WithOpenSSH(true))
if err != nil {
return err
}
privKeyB := pem.EncodeToMemory(p)
// Prepare the authorized key.
pubKeyB := ssh.MarshalAuthorizedKey(sshKey)
// Save the private key.
f, err := os.Create(userPrivKey)
if err != nil {
return err
}
f.Write(privKeyB)
f.Close()
os.Chmod(userPrivKey, 0600)
os.Chown(userPrivKey, int(config.SignOptions.UID), int(config.SignOptions.GID))
// Save the public key.
f, err = os.Create(userPubKey)
if err != nil {
return err
}
f.Write(pubKeyB)
f.Close()
os.Chmod(userPubKey, 0600)
os.Chown(userPubKey, int(config.SignOptions.UID), int(config.SignOptions.GID))
}
// Setup a signing request.
sr := httpSignRequest{}
sr.Environment = config.SignOptions.Environment
sr.APIKey = config.SignOptions.APIKey
sr.Type = "user"
sr.KeyID = config.SignOptions.KeyID
sr.ValidPrincipals = config.SignOptions.ValidPrincipals
sr.Options = config.SignOptions.Options
sr.Extensions = config.SignOptions.Extensions
sr.Duration = config.SignOptions.Duration
// If a key ID is not set in the configuration, we will use the hostname.
if sr.KeyID == "" {
sr.KeyID, _ = os.Hostname()
}
// Read the user public key.
pubKeyFile, err := ioutil.ReadFile(userPubKey)
if err != nil {
return fmt.Errorf("Unable to read public key: %v", err)
}
sr.PublicKeys = append(sr.PublicKeys, string(pubKeyFile))
// Convert signing request into JSON for the request.
srData, err := json.Marshal(sr)
if err != nil {
return err
}
// Setup request.
req, _ := http.NewRequest("POST", config.CertServer+"/sign", bytes.NewBuffer(srData))
req.Header.Add("content-type", "application/json")
// Send the request.
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
// Parse the response.
var signResponse httpSignResponse
decoder := json.NewDecoder(res.Body)
err = decoder.Decode(&signResponse)
if err != nil {
return err
}
// If successful, we need to pull the signed keys and store them/update the sshd configurations.
if signResponse.Successful && len(signResponse.SignedKeys) == 1 {
// The name of the host certificate file is the same as the public key,
// but with `-cert` pre-pended to the extension.
certKeyFile := strings.Replace(userPubKey, ".pub", "-cert.pub", 1)
// Save the certificate file.
f, err := os.Create(certKeyFile)
if err != nil {
return err
}
f.WriteString(signResponse.SignedKeys[0])
f.Close()
os.Chmod(certKeyFile, 0600)
os.Chown(certKeyFile, int(config.SignOptions.UID), int(config.SignOptions.GID))
} else {
fmt.Println("Response from server:")
// The request was not successful, so there is likely a message with an error.
fmt.Println(signResponse.Message)
}
} else {
// Get all UNIX Accounts.
accounts := new(UNIXAccounts)
err := accounts.init()
if err != nil {
return err
}
// Go through the environments.
for _, environment := range config.Evironments {
// If we are filtering to a sepcific environment, ones which are not that environment should be skipped.
if environmentFilter != "" && environmentFilter != environment.Name {
continue
}
// If the system group is not set or if this is not setup to sign user keys, we ignore this environment.
if environment.SystemGroup == "" {
continue
}
fmt.Println("Signing keys for environment", environment.Name)
// Get the group for the environment from the UNIX Accounts.
group := accounts.groupWithName(environment.SystemGroup)
if group == nil {
continue
}
// Find all members of the group and loop to sign a key for each member.
users := accounts.usersInGroup(group)
for _, user := range users {
// If the user is disabled, we need to ignore.
if user.Disabled {
continue
}
// If the user does not already have an .ssh directory... We will ignore it.
if _, err := os.Stat(user.HomeDir + "/.ssh"); err != nil {
continue
}
// Get the path for the user key.
userPrivKey := user.HomeDir + "/.ssh/id_cert_" + environment.Name
userPubKey := userPrivKey + ".pub"
// If the user key was not generated, we need to make a new key.
if _, err := os.Stat(userPrivKey); err != nil {
// Make a key using our default type.
pub, priv, err := keys.GenerateKeyPair(config.KeyDefaults.Type, config.KeyDefaults.Curve, config.KeyDefaults.Size)
if err != nil {
return err
}
// If we cannot sign with the key, something is wrong.
if _, ok := priv.(crypto.Signer); !ok {
return errors.Errorf("key of type %T is not a crypto.Signer", priv)
}
// Get the public key in a form that we can generate an authorized key file.
sshKey, err := ssh.NewPublicKey(pub)
if err != nil {
return errors.Wrapf(err, "error converting public key")
}
// Get the private key in a format which can be saved to disk.
p, err := pemutil.Serialize(priv, pemutil.WithOpenSSH(true))
if err != nil {
return err
}
privKeyB := pem.EncodeToMemory(p)
// Prepare the authorized key.
pubKeyB := ssh.MarshalAuthorizedKey(sshKey)
// Save the private key.
f, err := os.Create(userPrivKey)
if err != nil {
return err
}
f.Write(privKeyB)
f.Close()
os.Chmod(userPrivKey, 0600)
os.Chown(userPrivKey, int(user.ID), int(user.GID))
// Save the public key.
f, err = os.Create(userPubKey)
if err != nil {
return err
}
f.Write(pubKeyB)
f.Close()
os.Chmod(userPubKey, 0600)
os.Chown(userPubKey, int(user.ID), int(user.GID))
}
// Setup a signing request.
sr := httpSignRequest{}
sr.Environment = environment.Name
sr.APIKey = environment.APIKey
sr.Type = "user"
// The key ID can have a place holder for the username.
keyID := environment.SignOptions.KeyID
keyID = strings.Replace(keyID, "USERNAME", user.Name, 10)
sr.KeyID = keyID
sr.ValidPrincipals = environment.SignOptions.ValidPrincipals
sr.Options = environment.SignOptions.Options
sr.Extensions = environment.SignOptions.Extensions
sr.Duration = environment.SignOptions.Duration
// If a key ID is not set in the configuration, we will use the hostname.
if sr.KeyID == "" {
sr.KeyID, _ = os.Hostname()
}
// Read the user public key.
pubKeyFile, err := ioutil.ReadFile(userPubKey)
if err != nil {
return fmt.Errorf("Unable to read public key: %v", err)
}
sr.PublicKeys = append(sr.PublicKeys, string(pubKeyFile))
// Convert signing request into JSON for the request.
srData, err := json.Marshal(sr)
if err != nil {
return err
}
// Setup request.
req, _ := http.NewRequest("POST", config.CertServer+"/sign", bytes.NewBuffer(srData))
req.Header.Add("content-type", "application/json")
// Send the request.
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
// Parse the response.
var signResponse httpSignResponse
decoder := json.NewDecoder(res.Body)
err = decoder.Decode(&signResponse)
if err != nil {
return err
}
// If successful, we need to pull the signed keys and store them/update the sshd configurations.
if signResponse.Successful && len(signResponse.SignedKeys) == 1 {
// The name of the host certificate file is the same as the public key,
// but with `-cert` pre-pended to the extension.
certKeyFile := strings.Replace(userPubKey, ".pub", "-cert.pub", 1)
// Save the certificate file.
f, err := os.Create(certKeyFile)
if err != nil {
return err
}
f.WriteString(signResponse.SignedKeys[0])
f.Close()
os.Chmod(certKeyFile, 0600)
os.Chown(certKeyFile, int(user.ID), int(user.GID))
fmt.Println("Signed key for", user.Name)
} else {
fmt.Println("Unable to sign key for", user.Name)
// The request was not successful, so there is likely a message with an error.
fmt.Println(signResponse.Message)
}
}
}
}
return nil
}