2019-07-24 01:46:43 +00:00
|
|
|
package authority
|
|
|
|
|
|
|
|
import (
|
2019-10-28 18:50:43 +00:00
|
|
|
"context"
|
2019-07-24 01:46:43 +00:00
|
|
|
"crypto/rand"
|
2019-11-20 20:59:48 +00:00
|
|
|
"crypto/x509"
|
2019-07-24 01:46:43 +00:00
|
|
|
"encoding/binary"
|
|
|
|
"net/http"
|
|
|
|
"strings"
|
2019-10-28 18:50:43 +00:00
|
|
|
"time"
|
2019-07-24 01:46:43 +00:00
|
|
|
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/smallstep/certificates/authority/provisioner"
|
2019-10-10 20:08:57 +00:00
|
|
|
"github.com/smallstep/certificates/db"
|
2019-10-05 00:08:42 +00:00
|
|
|
"github.com/smallstep/certificates/templates"
|
2019-07-24 01:46:43 +00:00
|
|
|
"github.com/smallstep/cli/crypto/randutil"
|
2019-10-09 01:09:41 +00:00
|
|
|
"github.com/smallstep/cli/jose"
|
2019-07-24 01:46:43 +00:00
|
|
|
"golang.org/x/crypto/ssh"
|
|
|
|
)
|
|
|
|
|
2019-08-03 00:48:34 +00:00
|
|
|
const (
|
|
|
|
// SSHAddUserPrincipal is the principal that will run the add user command.
|
|
|
|
// Defaults to "provisioner" but it can be changed in the configuration.
|
|
|
|
SSHAddUserPrincipal = "provisioner"
|
|
|
|
|
|
|
|
// SSHAddUserCommand is the default command to run to add a new user.
|
|
|
|
// Defaults to "sudo useradd -m <principal>; nc -q0 localhost 22" but it can be changed in the
|
|
|
|
// configuration. The string "<principal>" will be replace by the new
|
|
|
|
// principal to add.
|
|
|
|
SSHAddUserCommand = "sudo useradd -m <principal>; nc -q0 localhost 22"
|
|
|
|
)
|
2019-07-24 01:46:43 +00:00
|
|
|
|
2019-10-09 01:09:41 +00:00
|
|
|
// SSHConfig contains the user and host keys.
|
|
|
|
type SSHConfig struct {
|
|
|
|
HostKey string `json:"hostKey"`
|
|
|
|
UserKey string `json:"userKey"`
|
|
|
|
Keys []*SSHPublicKey `json:"keys,omitempty"`
|
2019-11-15 02:24:58 +00:00
|
|
|
AddUserPrincipal string `json:"addUserPrincipal,omitempty"`
|
|
|
|
AddUserCommand string `json:"addUserCommand,omitempty"`
|
|
|
|
Bastion *Bastion `json:"bastion,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// Bastion contains the custom properties used on bastion.
|
|
|
|
type Bastion struct {
|
|
|
|
Hostname string `json:"hostname"`
|
|
|
|
User string `json:"user,omitempty"`
|
|
|
|
Port string `json:"port,omitempty"`
|
|
|
|
Command string `json:"cmd,omitempty"`
|
|
|
|
Flags string `json:"flags,omitempty"`
|
2019-10-09 01:09:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Validate checks the fields in SSHConfig.
|
|
|
|
func (c *SSHConfig) Validate() error {
|
|
|
|
if c == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
for _, k := range c.Keys {
|
|
|
|
if err := k.Validate(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// SSHPublicKey contains a public key used by federated CAs to keep old signing
|
|
|
|
// keys for this ca.
|
|
|
|
type SSHPublicKey struct {
|
|
|
|
Type string `json:"type"`
|
|
|
|
Federated bool `json:"federated"`
|
|
|
|
Key jose.JSONWebKey `json:"key"`
|
|
|
|
publicKey ssh.PublicKey
|
|
|
|
}
|
|
|
|
|
|
|
|
// Validate checks the fields in SSHPublicKey.
|
|
|
|
func (k *SSHPublicKey) Validate() error {
|
|
|
|
switch {
|
|
|
|
case k.Type == "":
|
|
|
|
return errors.New("type cannot be empty")
|
|
|
|
case k.Type != provisioner.SSHHostCert && k.Type != provisioner.SSHUserCert:
|
|
|
|
return errors.Errorf("invalid type %s, it must be user or host", k.Type)
|
|
|
|
case !k.Key.IsPublic():
|
|
|
|
return errors.New("invalid key type, it must be a public key")
|
|
|
|
}
|
|
|
|
|
|
|
|
key, err := ssh.NewPublicKey(k.Key.Key)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "error creating ssh key")
|
|
|
|
}
|
|
|
|
k.publicKey = key
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// PublicKey returns the ssh public key.
|
|
|
|
func (k *SSHPublicKey) PublicKey() ssh.PublicKey {
|
|
|
|
return k.publicKey
|
|
|
|
}
|
|
|
|
|
2019-09-26 20:22:07 +00:00
|
|
|
// SSHKeys represents the SSH User and Host public keys.
|
|
|
|
type SSHKeys struct {
|
2019-10-09 01:09:41 +00:00
|
|
|
UserKeys []ssh.PublicKey
|
|
|
|
HostKeys []ssh.PublicKey
|
2019-09-25 02:12:13 +00:00
|
|
|
}
|
|
|
|
|
2019-10-09 01:35:28 +00:00
|
|
|
// GetSSHRoots returns the SSH User and Host public keys.
|
|
|
|
func (a *Authority) GetSSHRoots() (*SSHKeys, error) {
|
2019-10-09 01:09:41 +00:00
|
|
|
return &SSHKeys{
|
|
|
|
HostKeys: a.sshCAHostCerts,
|
|
|
|
UserKeys: a.sshCAUserCerts,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2019-10-09 01:35:28 +00:00
|
|
|
// GetSSHFederation returns the public keys for federated SSH signers.
|
|
|
|
func (a *Authority) GetSSHFederation() (*SSHKeys, error) {
|
2019-10-09 01:09:41 +00:00
|
|
|
return &SSHKeys{
|
|
|
|
HostKeys: a.sshCAHostFederatedCerts,
|
|
|
|
UserKeys: a.sshCAUserFederatedCerts,
|
|
|
|
}, nil
|
2019-09-25 02:12:13 +00:00
|
|
|
}
|
|
|
|
|
2019-10-04 02:03:38 +00:00
|
|
|
// GetSSHConfig returns rendered templates for clients (user) or servers (host).
|
2019-10-05 00:08:42 +00:00
|
|
|
func (a *Authority) GetSSHConfig(typ string, data map[string]string) ([]templates.Output, error) {
|
2019-10-04 02:03:38 +00:00
|
|
|
if a.sshCAUserCertSignKey == nil && a.sshCAHostCertSignKey == nil {
|
|
|
|
return nil, &apiError{
|
|
|
|
err: errors.New("getSSHConfig: ssh is not configured"),
|
|
|
|
code: http.StatusNotFound,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var ts []templates.Template
|
|
|
|
switch typ {
|
|
|
|
case provisioner.SSHUserCert:
|
|
|
|
if a.config.Templates != nil && a.config.Templates.SSH != nil {
|
|
|
|
ts = a.config.Templates.SSH.User
|
|
|
|
}
|
|
|
|
case provisioner.SSHHostCert:
|
|
|
|
if a.config.Templates != nil && a.config.Templates.SSH != nil {
|
|
|
|
ts = a.config.Templates.SSH.Host
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
return nil, &apiError{
|
|
|
|
err: errors.Errorf("getSSHConfig: type %s is not valid", typ),
|
|
|
|
code: http.StatusBadRequest,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-05 00:08:42 +00:00
|
|
|
// Merge user and default data
|
|
|
|
var mergedData map[string]interface{}
|
|
|
|
|
|
|
|
if len(data) == 0 {
|
|
|
|
mergedData = a.config.Templates.Data
|
|
|
|
} else {
|
|
|
|
mergedData = make(map[string]interface{}, len(a.config.Templates.Data)+1)
|
|
|
|
mergedData["User"] = data
|
|
|
|
for k, v := range a.config.Templates.Data {
|
|
|
|
mergedData[k] = v
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Render templates
|
2019-10-04 02:03:38 +00:00
|
|
|
output := []templates.Output{}
|
|
|
|
for _, t := range ts {
|
2019-10-05 00:08:42 +00:00
|
|
|
o, err := t.Output(mergedData)
|
2019-10-04 02:03:38 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
output = append(output, o)
|
|
|
|
}
|
|
|
|
return output, nil
|
|
|
|
}
|
|
|
|
|
2019-11-15 02:24:58 +00:00
|
|
|
// GetSSHBastion returns the bastion configuration, for the given pair user,
|
|
|
|
// hostname.
|
|
|
|
func (a *Authority) GetSSHBastion(user string, hostname string) (*Bastion, error) {
|
|
|
|
if a.sshBastionFunc != nil {
|
|
|
|
return a.sshBastionFunc(user, hostname)
|
|
|
|
}
|
|
|
|
if a.config.SSH != nil {
|
|
|
|
if a.config.SSH.Bastion != nil && a.config.SSH.Bastion.Hostname != "" {
|
|
|
|
return a.config.SSH.Bastion, nil
|
|
|
|
}
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
return nil, &apiError{
|
|
|
|
err: errors.New("getSSHBastion: ssh is not configured"),
|
|
|
|
code: http.StatusNotFound,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-28 18:50:43 +00:00
|
|
|
// authorizeSSHSign loads the provisioner from the token, checks that it has not
|
|
|
|
// been used again and calls the provisioner AuthorizeSSHSign method. Returns a
|
|
|
|
// list of methods to apply to the signing flow.
|
|
|
|
func (a *Authority) authorizeSSHSign(ctx context.Context, ott string) ([]provisioner.SignOption, error) {
|
|
|
|
var errContext = apiCtx{"ott": ott}
|
2019-11-20 19:51:25 +00:00
|
|
|
p, err := a.authorizeToken(ctx, ott)
|
2019-10-28 18:50:43 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, &apiError{errors.Wrap(err, "authorizeSSHSign"), http.StatusUnauthorized, errContext}
|
|
|
|
}
|
|
|
|
opts, err := p.AuthorizeSSHSign(ctx, ott)
|
|
|
|
if err != nil {
|
|
|
|
return nil, &apiError{errors.Wrap(err, "authorizeSSHSign"), http.StatusUnauthorized, errContext}
|
|
|
|
}
|
|
|
|
return opts, nil
|
|
|
|
}
|
|
|
|
|
2019-07-24 01:46:43 +00:00
|
|
|
// SignSSH creates a signed SSH certificate with the given public key and options.
|
|
|
|
func (a *Authority) SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) {
|
|
|
|
var mods []provisioner.SSHCertificateModifier
|
|
|
|
var validators []provisioner.SSHCertificateValidator
|
|
|
|
|
|
|
|
for _, op := range signOpts {
|
|
|
|
switch o := op.(type) {
|
|
|
|
// modify the ssh.Certificate
|
|
|
|
case provisioner.SSHCertificateModifier:
|
|
|
|
mods = append(mods, o)
|
|
|
|
// modify the ssh.Certificate given the SSHOptions
|
|
|
|
case provisioner.SSHCertificateOptionModifier:
|
|
|
|
mods = append(mods, o.Option(opts))
|
|
|
|
// validate the ssh.Certificate
|
|
|
|
case provisioner.SSHCertificateValidator:
|
|
|
|
validators = append(validators, o)
|
|
|
|
// validate the given SSHOptions
|
|
|
|
case provisioner.SSHCertificateOptionsValidator:
|
|
|
|
if err := o.Valid(opts); err != nil {
|
2019-08-03 00:48:34 +00:00
|
|
|
return nil, &apiError{err: err, code: http.StatusForbidden}
|
2019-07-24 01:46:43 +00:00
|
|
|
}
|
|
|
|
default:
|
|
|
|
return nil, &apiError{
|
|
|
|
err: errors.Errorf("signSSH: invalid extra option type %T", o),
|
|
|
|
code: http.StatusInternalServerError,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
nonce, err := randutil.ASCII(32)
|
|
|
|
if err != nil {
|
2019-08-03 00:48:34 +00:00
|
|
|
return nil, &apiError{err: err, code: http.StatusInternalServerError}
|
2019-07-24 01:46:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var serial uint64
|
|
|
|
if err := binary.Read(rand.Reader, binary.BigEndian, &serial); err != nil {
|
2019-08-03 00:48:34 +00:00
|
|
|
return nil, &apiError{
|
|
|
|
err: errors.Wrap(err, "signSSH: error reading random number"),
|
|
|
|
code: http.StatusInternalServerError,
|
|
|
|
}
|
2019-07-24 01:46:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Build base certificate with the key and some random values
|
|
|
|
cert := &ssh.Certificate{
|
|
|
|
Nonce: []byte(nonce),
|
|
|
|
Key: key,
|
|
|
|
Serial: serial,
|
|
|
|
}
|
|
|
|
|
|
|
|
// Use opts to modify the certificate
|
|
|
|
if err := opts.Modify(cert); err != nil {
|
2019-08-03 00:48:34 +00:00
|
|
|
return nil, &apiError{err: err, code: http.StatusForbidden}
|
2019-07-24 01:46:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Use provisioner modifiers
|
|
|
|
for _, m := range mods {
|
|
|
|
if err := m.Modify(cert); err != nil {
|
2019-08-03 00:48:34 +00:00
|
|
|
return nil, &apiError{err: err, code: http.StatusForbidden}
|
2019-07-24 01:46:43 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get signer from authority keys
|
|
|
|
var signer ssh.Signer
|
|
|
|
switch cert.CertType {
|
|
|
|
case ssh.UserCert:
|
2019-08-01 22:04:56 +00:00
|
|
|
if a.sshCAUserCertSignKey == nil {
|
|
|
|
return nil, &apiError{
|
|
|
|
err: errors.New("signSSH: user certificate signing is not enabled"),
|
|
|
|
code: http.StatusNotImplemented,
|
|
|
|
}
|
|
|
|
}
|
2019-09-25 02:12:13 +00:00
|
|
|
signer = a.sshCAUserCertSignKey
|
2019-07-24 01:46:43 +00:00
|
|
|
case ssh.HostCert:
|
2019-08-01 22:04:56 +00:00
|
|
|
if a.sshCAHostCertSignKey == nil {
|
|
|
|
return nil, &apiError{
|
|
|
|
err: errors.New("signSSH: host certificate signing is not enabled"),
|
|
|
|
code: http.StatusNotImplemented,
|
|
|
|
}
|
|
|
|
}
|
2019-09-25 02:12:13 +00:00
|
|
|
signer = a.sshCAHostCertSignKey
|
2019-07-24 01:46:43 +00:00
|
|
|
default:
|
|
|
|
return nil, &apiError{
|
2019-08-03 00:48:34 +00:00
|
|
|
err: errors.Errorf("signSSH: unexpected ssh certificate type: %d", cert.CertType),
|
2019-07-24 01:46:43 +00:00
|
|
|
code: http.StatusInternalServerError,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
cert.SignatureKey = signer.PublicKey()
|
|
|
|
|
|
|
|
// Get bytes for signing trailing the signature length.
|
|
|
|
data := cert.Marshal()
|
|
|
|
data = data[:len(data)-4]
|
|
|
|
|
|
|
|
// Sign the certificate
|
|
|
|
sig, err := signer.Sign(rand.Reader, data)
|
|
|
|
if err != nil {
|
2019-08-03 00:48:34 +00:00
|
|
|
return nil, &apiError{
|
|
|
|
err: errors.Wrap(err, "signSSH: error signing certificate"),
|
|
|
|
code: http.StatusInternalServerError,
|
|
|
|
}
|
2019-07-24 01:46:43 +00:00
|
|
|
}
|
|
|
|
cert.Signature = sig
|
|
|
|
|
|
|
|
// User provisioners validators
|
|
|
|
for _, v := range validators {
|
|
|
|
if err := v.Valid(cert); err != nil {
|
2019-08-03 00:48:34 +00:00
|
|
|
return nil, &apiError{err: err, code: http.StatusForbidden}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-10 20:08:57 +00:00
|
|
|
if err = a.db.StoreSSHCertificate(cert); err != nil && err != db.ErrNotImplemented {
|
|
|
|
return nil, &apiError{
|
|
|
|
err: errors.Wrap(err, "signSSH: error storing certificate in db"),
|
|
|
|
code: http.StatusInternalServerError,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-03 00:48:34 +00:00
|
|
|
return cert, nil
|
|
|
|
}
|
|
|
|
|
2019-10-28 18:50:43 +00:00
|
|
|
// authorizeSSHRenew authorizes an SSH certificate renewal request, by
|
|
|
|
// validating the contents of an SSHPOP token.
|
|
|
|
func (a *Authority) authorizeSSHRenew(ctx context.Context, token string) (*ssh.Certificate, error) {
|
|
|
|
errContext := map[string]interface{}{"ott": token}
|
|
|
|
|
2019-11-20 19:51:25 +00:00
|
|
|
p, err := a.authorizeToken(ctx, token)
|
2019-10-28 18:50:43 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, &apiError{
|
|
|
|
err: errors.Wrap(err, "authorizeSSHRenew"),
|
|
|
|
code: http.StatusUnauthorized,
|
|
|
|
context: errContext,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
cert, err := p.AuthorizeSSHRenew(ctx, token)
|
|
|
|
if err != nil {
|
|
|
|
return nil, &apiError{
|
|
|
|
err: errors.Wrap(err, "authorizeSSHRenew"),
|
|
|
|
code: http.StatusUnauthorized,
|
|
|
|
context: errContext,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return cert, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// RenewSSH creates a signed SSH certificate using the old SSH certificate as a template.
|
|
|
|
func (a *Authority) RenewSSH(oldCert *ssh.Certificate) (*ssh.Certificate, error) {
|
|
|
|
nonce, err := randutil.ASCII(32)
|
|
|
|
if err != nil {
|
|
|
|
return nil, &apiError{err: err, code: http.StatusInternalServerError}
|
|
|
|
}
|
|
|
|
|
|
|
|
var serial uint64
|
|
|
|
if err := binary.Read(rand.Reader, binary.BigEndian, &serial); err != nil {
|
|
|
|
return nil, &apiError{
|
|
|
|
err: errors.Wrap(err, "renewSSH: error reading random number"),
|
|
|
|
code: http.StatusInternalServerError,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if oldCert.ValidAfter == 0 || oldCert.ValidBefore == 0 {
|
2019-11-08 05:39:36 +00:00
|
|
|
return nil, errors.New("rewnewSSH: cannot renew certificate without validity period")
|
2019-10-28 18:50:43 +00:00
|
|
|
}
|
|
|
|
dur := time.Duration(oldCert.ValidBefore-oldCert.ValidAfter) * time.Second
|
|
|
|
va := time.Now()
|
|
|
|
vb := va.Add(dur)
|
|
|
|
|
|
|
|
// Build base certificate with the key and some random values
|
|
|
|
cert := &ssh.Certificate{
|
|
|
|
Nonce: []byte(nonce),
|
|
|
|
Key: oldCert.Key,
|
|
|
|
Serial: serial,
|
|
|
|
CertType: oldCert.CertType,
|
|
|
|
KeyId: oldCert.KeyId,
|
|
|
|
ValidPrincipals: oldCert.ValidPrincipals,
|
|
|
|
Permissions: oldCert.Permissions,
|
|
|
|
ValidAfter: uint64(va.Unix()),
|
|
|
|
ValidBefore: uint64(vb.Unix()),
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get signer from authority keys
|
|
|
|
var signer ssh.Signer
|
|
|
|
switch cert.CertType {
|
|
|
|
case ssh.UserCert:
|
|
|
|
if a.sshCAUserCertSignKey == nil {
|
|
|
|
return nil, &apiError{
|
|
|
|
err: errors.New("renewSSH: user certificate signing is not enabled"),
|
|
|
|
code: http.StatusNotImplemented,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
signer = a.sshCAUserCertSignKey
|
|
|
|
case ssh.HostCert:
|
|
|
|
if a.sshCAHostCertSignKey == nil {
|
|
|
|
return nil, &apiError{
|
|
|
|
err: errors.New("renewSSH: host certificate signing is not enabled"),
|
|
|
|
code: http.StatusNotImplemented,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
signer = a.sshCAHostCertSignKey
|
|
|
|
default:
|
|
|
|
return nil, &apiError{
|
|
|
|
err: errors.Errorf("renewSSH: unexpected ssh certificate type: %d", cert.CertType),
|
|
|
|
code: http.StatusInternalServerError,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
cert.SignatureKey = signer.PublicKey()
|
|
|
|
|
|
|
|
// Get bytes for signing trailing the signature length.
|
|
|
|
data := cert.Marshal()
|
|
|
|
data = data[:len(data)-4]
|
|
|
|
|
|
|
|
// Sign the certificate
|
|
|
|
sig, err := signer.Sign(rand.Reader, data)
|
|
|
|
if err != nil {
|
|
|
|
return nil, &apiError{
|
|
|
|
err: errors.Wrap(err, "renewSSH: error signing certificate"),
|
|
|
|
code: http.StatusInternalServerError,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
cert.Signature = sig
|
|
|
|
|
|
|
|
if err = a.db.StoreSSHCertificate(cert); err != nil && err != db.ErrNotImplemented {
|
|
|
|
return nil, &apiError{
|
|
|
|
err: errors.Wrap(err, "renewSSH: error storing certificate in db"),
|
|
|
|
code: http.StatusInternalServerError,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return cert, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// authorizeSSHRekey authorizes an SSH certificate rekey request, by
|
|
|
|
// validating the contents of an SSHPOP token.
|
|
|
|
func (a *Authority) authorizeSSHRekey(ctx context.Context, token string) (*ssh.Certificate, []provisioner.SignOption, error) {
|
|
|
|
errContext := map[string]interface{}{"ott": token}
|
|
|
|
|
2019-11-20 19:51:25 +00:00
|
|
|
p, err := a.authorizeToken(ctx, token)
|
2019-10-28 18:50:43 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, nil, &apiError{
|
|
|
|
err: errors.Wrap(err, "authorizeSSHRenew"),
|
|
|
|
code: http.StatusUnauthorized,
|
|
|
|
context: errContext,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
cert, opts, err := p.AuthorizeSSHRekey(ctx, token)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, &apiError{
|
|
|
|
err: errors.Wrap(err, "authorizeSSHRekey"),
|
|
|
|
code: http.StatusUnauthorized,
|
|
|
|
context: errContext,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return cert, opts, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// RekeySSH creates a signed SSH certificate using the old SSH certificate as a template.
|
|
|
|
func (a *Authority) RekeySSH(oldCert *ssh.Certificate, pub ssh.PublicKey, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) {
|
|
|
|
var validators []provisioner.SSHCertificateValidator
|
|
|
|
|
|
|
|
for _, op := range signOpts {
|
|
|
|
switch o := op.(type) {
|
|
|
|
// validate the ssh.Certificate
|
|
|
|
case provisioner.SSHCertificateValidator:
|
|
|
|
validators = append(validators, o)
|
|
|
|
default:
|
|
|
|
return nil, &apiError{
|
|
|
|
err: errors.Errorf("rekeySSH: invalid extra option type %T", o),
|
|
|
|
code: http.StatusInternalServerError,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
nonce, err := randutil.ASCII(32)
|
|
|
|
if err != nil {
|
|
|
|
return nil, &apiError{err: err, code: http.StatusInternalServerError}
|
|
|
|
}
|
|
|
|
|
|
|
|
var serial uint64
|
|
|
|
if err := binary.Read(rand.Reader, binary.BigEndian, &serial); err != nil {
|
|
|
|
return nil, &apiError{
|
|
|
|
err: errors.Wrap(err, "rekeySSH: error reading random number"),
|
|
|
|
code: http.StatusInternalServerError,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if oldCert.ValidAfter == 0 || oldCert.ValidBefore == 0 {
|
2019-11-08 05:39:36 +00:00
|
|
|
return nil, errors.New("rekeySSH: cannot rekey certificate without validity period")
|
2019-10-28 18:50:43 +00:00
|
|
|
}
|
|
|
|
dur := time.Duration(oldCert.ValidBefore-oldCert.ValidAfter) * time.Second
|
|
|
|
va := time.Now()
|
|
|
|
vb := va.Add(dur)
|
|
|
|
|
|
|
|
// Build base certificate with the key and some random values
|
|
|
|
cert := &ssh.Certificate{
|
|
|
|
Nonce: []byte(nonce),
|
|
|
|
Key: pub,
|
|
|
|
Serial: serial,
|
|
|
|
CertType: oldCert.CertType,
|
|
|
|
KeyId: oldCert.KeyId,
|
|
|
|
ValidPrincipals: oldCert.ValidPrincipals,
|
|
|
|
Permissions: oldCert.Permissions,
|
|
|
|
ValidAfter: uint64(va.Unix()),
|
|
|
|
ValidBefore: uint64(vb.Unix()),
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get signer from authority keys
|
|
|
|
var signer ssh.Signer
|
|
|
|
switch cert.CertType {
|
|
|
|
case ssh.UserCert:
|
|
|
|
if a.sshCAUserCertSignKey == nil {
|
|
|
|
return nil, &apiError{
|
|
|
|
err: errors.New("rekeySSH: user certificate signing is not enabled"),
|
|
|
|
code: http.StatusNotImplemented,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
signer = a.sshCAUserCertSignKey
|
|
|
|
case ssh.HostCert:
|
|
|
|
if a.sshCAHostCertSignKey == nil {
|
|
|
|
return nil, &apiError{
|
|
|
|
err: errors.New("rekeySSH: host certificate signing is not enabled"),
|
|
|
|
code: http.StatusNotImplemented,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
signer = a.sshCAHostCertSignKey
|
|
|
|
default:
|
|
|
|
return nil, &apiError{
|
|
|
|
err: errors.Errorf("rekeySSH: unexpected ssh certificate type: %d", cert.CertType),
|
|
|
|
code: http.StatusInternalServerError,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
cert.SignatureKey = signer.PublicKey()
|
|
|
|
|
|
|
|
// Get bytes for signing trailing the signature length.
|
|
|
|
data := cert.Marshal()
|
|
|
|
data = data[:len(data)-4]
|
|
|
|
|
|
|
|
// Sign the certificate
|
|
|
|
sig, err := signer.Sign(rand.Reader, data)
|
|
|
|
if err != nil {
|
|
|
|
return nil, &apiError{
|
|
|
|
err: errors.Wrap(err, "rekeySSH: error signing certificate"),
|
|
|
|
code: http.StatusInternalServerError,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
cert.Signature = sig
|
|
|
|
|
|
|
|
// User provisioners validators
|
|
|
|
for _, v := range validators {
|
|
|
|
if err := v.Valid(cert); err != nil {
|
|
|
|
return nil, &apiError{err: err, code: http.StatusForbidden}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if err = a.db.StoreSSHCertificate(cert); err != nil && err != db.ErrNotImplemented {
|
|
|
|
return nil, &apiError{
|
|
|
|
err: errors.Wrap(err, "rekeySSH: error storing certificate in db"),
|
|
|
|
code: http.StatusInternalServerError,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return cert, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// authorizeSSHRevoke authorizes an SSH certificate revoke request, by
|
|
|
|
// validating the contents of an SSHPOP token.
|
|
|
|
func (a *Authority) authorizeSSHRevoke(ctx context.Context, token string) error {
|
|
|
|
errContext := map[string]interface{}{"ott": token}
|
|
|
|
|
2019-11-20 19:51:25 +00:00
|
|
|
p, err := a.authorizeToken(ctx, token)
|
2019-10-28 18:50:43 +00:00
|
|
|
if err != nil {
|
|
|
|
return &apiError{errors.Wrap(err, "authorizeSSHRevoke"), http.StatusUnauthorized, errContext}
|
|
|
|
}
|
|
|
|
if err = p.AuthorizeSSHRevoke(ctx, token); err != nil {
|
|
|
|
return &apiError{errors.Wrap(err, "authorizeSSHRevoke"), http.StatusUnauthorized, errContext}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-08-03 00:48:34 +00:00
|
|
|
// SignSSHAddUser signs a certificate that provisions a new user in a server.
|
|
|
|
func (a *Authority) SignSSHAddUser(key ssh.PublicKey, subject *ssh.Certificate) (*ssh.Certificate, error) {
|
|
|
|
if a.sshCAUserCertSignKey == nil {
|
|
|
|
return nil, &apiError{
|
2019-09-09 04:05:36 +00:00
|
|
|
err: errors.New("signSSHAddUser: user certificate signing is not enabled"),
|
2019-08-03 00:48:34 +00:00
|
|
|
code: http.StatusNotImplemented,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if subject.CertType != ssh.UserCert {
|
|
|
|
return nil, &apiError{
|
2019-10-10 20:08:57 +00:00
|
|
|
err: errors.New("signSSHAddUser: certificate is not a user certificate"),
|
2019-08-03 00:48:34 +00:00
|
|
|
code: http.StatusForbidden,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if len(subject.ValidPrincipals) != 1 {
|
|
|
|
return nil, &apiError{
|
2019-10-10 20:08:57 +00:00
|
|
|
err: errors.New("signSSHAddUser: certificate does not have only one principal"),
|
2019-08-03 00:48:34 +00:00
|
|
|
code: http.StatusForbidden,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
nonce, err := randutil.ASCII(32)
|
|
|
|
if err != nil {
|
|
|
|
return nil, &apiError{err: err, code: http.StatusInternalServerError}
|
|
|
|
}
|
|
|
|
|
|
|
|
var serial uint64
|
|
|
|
if err := binary.Read(rand.Reader, binary.BigEndian, &serial); err != nil {
|
|
|
|
return nil, &apiError{
|
2019-10-10 20:08:57 +00:00
|
|
|
err: errors.Wrap(err, "signSSHAddUser: error reading random number"),
|
2019-08-03 00:48:34 +00:00
|
|
|
code: http.StatusInternalServerError,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-09-25 02:12:13 +00:00
|
|
|
signer := a.sshCAUserCertSignKey
|
2019-08-03 00:48:34 +00:00
|
|
|
principal := subject.ValidPrincipals[0]
|
|
|
|
addUserPrincipal := a.getAddUserPrincipal()
|
|
|
|
|
|
|
|
cert := &ssh.Certificate{
|
|
|
|
Nonce: []byte(nonce),
|
|
|
|
Key: key,
|
|
|
|
Serial: serial,
|
|
|
|
CertType: ssh.UserCert,
|
|
|
|
KeyId: principal + "-" + addUserPrincipal,
|
|
|
|
ValidPrincipals: []string{addUserPrincipal},
|
|
|
|
ValidAfter: subject.ValidAfter,
|
|
|
|
ValidBefore: subject.ValidBefore,
|
|
|
|
Permissions: ssh.Permissions{
|
|
|
|
CriticalOptions: map[string]string{
|
|
|
|
"force-command": a.getAddUserCommand(principal),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
SignatureKey: signer.PublicKey(),
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get bytes for signing trailing the signature length.
|
|
|
|
data := cert.Marshal()
|
|
|
|
data = data[:len(data)-4]
|
|
|
|
|
|
|
|
// Sign the certificate
|
|
|
|
sig, err := signer.Sign(rand.Reader, data)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
cert.Signature = sig
|
2019-10-10 20:08:57 +00:00
|
|
|
|
|
|
|
if err = a.db.StoreSSHCertificate(cert); err != nil && err != db.ErrNotImplemented {
|
|
|
|
return nil, &apiError{
|
|
|
|
err: errors.Wrap(err, "signSSHAddUser: error storing certificate in db"),
|
|
|
|
code: http.StatusInternalServerError,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-07-24 01:46:43 +00:00
|
|
|
return cert, nil
|
|
|
|
}
|
2019-08-03 00:48:34 +00:00
|
|
|
|
2019-10-10 20:08:57 +00:00
|
|
|
// CheckSSHHost checks the given principal has been registered before.
|
|
|
|
func (a *Authority) CheckSSHHost(principal string) (bool, error) {
|
|
|
|
exists, err := a.db.IsSSHHost(principal)
|
|
|
|
if err != nil {
|
|
|
|
if err == db.ErrNotImplemented {
|
|
|
|
return false, &apiError{
|
|
|
|
err: errors.Wrap(err, "checkSSHHost: isSSHHost is not implemented"),
|
|
|
|
code: http.StatusNotImplemented,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false, &apiError{
|
|
|
|
err: errors.Wrap(err, "checkSSHHost: error checking if hosts exists"),
|
|
|
|
code: http.StatusInternalServerError,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return exists, nil
|
|
|
|
}
|
|
|
|
|
2019-10-25 20:47:49 +00:00
|
|
|
// GetSSHHosts returns a list of valid host principals.
|
2019-11-20 20:59:48 +00:00
|
|
|
func (a *Authority) GetSSHHosts(cert *x509.Certificate) ([]string, error) {
|
|
|
|
if a.sshGetHostsFunc != nil {
|
|
|
|
return a.sshGetHostsFunc(cert)
|
2019-11-20 19:32:27 +00:00
|
|
|
}
|
2019-11-20 20:59:48 +00:00
|
|
|
hosts, err := a.db.GetSSHHostPrincipals()
|
|
|
|
if err != nil {
|
|
|
|
return nil, &apiError{
|
|
|
|
err: errors.Wrap(err, "getSSHHosts"),
|
|
|
|
code: http.StatusInternalServerError,
|
|
|
|
}
|
2019-10-25 20:47:49 +00:00
|
|
|
}
|
2019-11-20 20:59:48 +00:00
|
|
|
return hosts, nil
|
2019-10-25 20:47:49 +00:00
|
|
|
}
|
|
|
|
|
2019-08-03 00:48:34 +00:00
|
|
|
func (a *Authority) getAddUserPrincipal() (cmd string) {
|
|
|
|
if a.config.SSH.AddUserPrincipal == "" {
|
|
|
|
return SSHAddUserPrincipal
|
|
|
|
}
|
|
|
|
return a.config.SSH.AddUserPrincipal
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a *Authority) getAddUserCommand(principal string) string {
|
|
|
|
var cmd string
|
|
|
|
if a.config.SSH.AddUserCommand == "" {
|
|
|
|
cmd = SSHAddUserCommand
|
|
|
|
} else {
|
|
|
|
cmd = a.config.SSH.AddUserCommand
|
|
|
|
}
|
|
|
|
return strings.Replace(cmd, "<principal>", principal, -1)
|
|
|
|
}
|