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"
|
2022-03-31 14:12:29 +00:00
|
|
|
"errors"
|
2019-07-24 01:46:43 +00:00
|
|
|
"net/http"
|
|
|
|
"strings"
|
2019-10-28 18:50:43 +00:00
|
|
|
"time"
|
2019-07-24 01:46:43 +00:00
|
|
|
|
2022-04-24 11:11:32 +00:00
|
|
|
"golang.org/x/crypto/ssh"
|
|
|
|
|
|
|
|
"go.step.sm/crypto/randutil"
|
|
|
|
"go.step.sm/crypto/sshutil"
|
|
|
|
|
2021-05-03 19:48:20 +00:00
|
|
|
"github.com/smallstep/certificates/authority/config"
|
2019-07-24 01:46:43 +00:00
|
|
|
"github.com/smallstep/certificates/authority/provisioner"
|
2019-10-10 20:08:57 +00:00
|
|
|
"github.com/smallstep/certificates/db"
|
2019-12-16 07:54:25 +00:00
|
|
|
"github.com/smallstep/certificates/errs"
|
2019-10-05 00:08:42 +00:00
|
|
|
"github.com/smallstep/certificates/templates"
|
2022-09-30 00:16:26 +00:00
|
|
|
"github.com/smallstep/certificates/webhook"
|
2019-07-24 01:46:43 +00:00
|
|
|
)
|
|
|
|
|
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:35:28 +00:00
|
|
|
// GetSSHRoots returns the SSH User and Host public keys.
|
2021-05-03 19:48:20 +00:00
|
|
|
func (a *Authority) GetSSHRoots(context.Context) (*config.SSHKeys, error) {
|
|
|
|
return &config.SSHKeys{
|
2019-10-09 01:09:41 +00:00
|
|
|
HostKeys: a.sshCAHostCerts,
|
|
|
|
UserKeys: a.sshCAUserCerts,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2019-10-09 01:35:28 +00:00
|
|
|
// GetSSHFederation returns the public keys for federated SSH signers.
|
2021-05-03 19:48:20 +00:00
|
|
|
func (a *Authority) GetSSHFederation(context.Context) (*config.SSHKeys, error) {
|
|
|
|
return &config.SSHKeys{
|
2019-10-09 01:09:41 +00:00
|
|
|
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).
|
2020-03-11 02:01:45 +00:00
|
|
|
func (a *Authority) GetSSHConfig(ctx context.Context, typ string, data map[string]string) ([]templates.Output, error) {
|
2019-10-04 02:03:38 +00:00
|
|
|
if a.sshCAUserCertSignKey == nil && a.sshCAHostCertSignKey == nil {
|
2020-01-24 06:04:34 +00:00
|
|
|
return nil, errs.NotFound("getSSHConfig: ssh is not configured")
|
2019-10-04 02:03:38 +00:00
|
|
|
}
|
|
|
|
|
2020-06-17 00:57:35 +00:00
|
|
|
if a.templates == nil {
|
2020-06-16 00:25:47 +00:00
|
|
|
return nil, errs.NotFound("getSSHConfig: ssh templates are not configured")
|
|
|
|
}
|
|
|
|
|
2019-10-04 02:03:38 +00:00
|
|
|
var ts []templates.Template
|
|
|
|
switch typ {
|
|
|
|
case provisioner.SSHUserCert:
|
2020-06-17 00:57:35 +00:00
|
|
|
if a.templates != nil && a.templates.SSH != nil {
|
|
|
|
ts = a.templates.SSH.User
|
2019-10-04 02:03:38 +00:00
|
|
|
}
|
|
|
|
case provisioner.SSHHostCert:
|
2020-06-17 00:57:35 +00:00
|
|
|
if a.templates != nil && a.templates.SSH != nil {
|
|
|
|
ts = a.templates.SSH.Host
|
2019-10-04 02:03:38 +00:00
|
|
|
}
|
|
|
|
default:
|
2021-11-18 23:12:44 +00:00
|
|
|
return nil, errs.BadRequest("invalid certificate type '%s'", typ)
|
2019-10-04 02:03:38 +00:00
|
|
|
}
|
|
|
|
|
2019-10-05 00:08:42 +00:00
|
|
|
// Merge user and default data
|
|
|
|
var mergedData map[string]interface{}
|
|
|
|
|
|
|
|
if len(data) == 0 {
|
2020-06-17 00:57:35 +00:00
|
|
|
mergedData = a.templates.Data
|
2019-10-05 00:08:42 +00:00
|
|
|
} else {
|
2020-06-17 00:57:35 +00:00
|
|
|
mergedData = make(map[string]interface{}, len(a.templates.Data)+1)
|
2019-10-05 00:08:42 +00:00
|
|
|
mergedData["User"] = data
|
2020-06-17 00:57:35 +00:00
|
|
|
for k, v := range a.templates.Data {
|
2019-10-05 00:08:42 +00:00
|
|
|
mergedData[k] = v
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Render templates
|
2019-10-04 02:03:38 +00:00
|
|
|
output := []templates.Output{}
|
|
|
|
for _, t := range ts {
|
2020-06-17 00:26:54 +00:00
|
|
|
if err := t.Load(); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check for required variables.
|
|
|
|
if err := t.ValidateRequiredData(data); err != nil {
|
2021-11-19 02:17:36 +00:00
|
|
|
return nil, errs.BadRequestErr(err, "%v, please use `--set <key=value>` flag", err)
|
2020-06-17 00:26:54 +00:00
|
|
|
}
|
|
|
|
|
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
|
|
|
|
}
|
2021-11-12 21:12:11 +00:00
|
|
|
|
2021-11-15 23:32:07 +00:00
|
|
|
// Backwards compatibility for version of the cli older than v0.18.0.
|
|
|
|
// Before v0.18.0 we were not passing any value for SSHTemplateVersionKey
|
|
|
|
// from the cli.
|
2021-11-16 18:02:04 +00:00
|
|
|
if o.Name == "step_includes.tpl" && data[templates.SSHTemplateVersionKey] == "" {
|
|
|
|
o.Type = templates.File
|
|
|
|
o.Path = strings.TrimPrefix(o.Path, "${STEPPATH}/")
|
2021-11-12 21:12:11 +00:00
|
|
|
}
|
|
|
|
|
2019-10-04 02:03:38 +00:00
|
|
|
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.
|
2021-10-08 18:59:57 +00:00
|
|
|
func (a *Authority) GetSSHBastion(ctx context.Context, user, hostname string) (*config.Bastion, error) {
|
2019-11-15 02:24:58 +00:00
|
|
|
if a.sshBastionFunc != nil {
|
2020-03-11 02:01:45 +00:00
|
|
|
bs, err := a.sshBastionFunc(ctx, user, hostname)
|
2019-12-20 21:30:05 +00:00
|
|
|
return bs, errs.Wrap(http.StatusInternalServerError, err, "authority.GetSSHBastion")
|
2019-11-15 02:24:58 +00:00
|
|
|
}
|
|
|
|
if a.config.SSH != nil {
|
|
|
|
if a.config.SSH.Bastion != nil && a.config.SSH.Bastion.Hostname != "" {
|
2020-06-19 19:37:08 +00:00
|
|
|
// Do not return a bastion for a bastion host.
|
|
|
|
//
|
|
|
|
// This condition might fail if a different name or IP is used.
|
|
|
|
// Trying to resolve hostnames to IPs and compare them won't be a
|
|
|
|
// complete solution because it depends on the network
|
|
|
|
// configuration, of the CA and clients and can also return false
|
|
|
|
// positives. Although not perfect, this simple solution will work
|
|
|
|
// in most cases.
|
|
|
|
if !strings.EqualFold(hostname, a.config.SSH.Bastion.Hostname) {
|
|
|
|
return a.config.SSH.Bastion, nil
|
|
|
|
}
|
2019-11-15 02:24:58 +00:00
|
|
|
}
|
2022-08-23 19:43:48 +00:00
|
|
|
//nolint:nilnil // legacy
|
2019-11-15 02:24:58 +00:00
|
|
|
return nil, nil
|
|
|
|
}
|
2020-01-24 06:04:34 +00:00
|
|
|
return nil, errs.NotFound("authority.GetSSHBastion; ssh is not configured")
|
2019-10-28 18:50:43 +00:00
|
|
|
}
|
|
|
|
|
2019-07-24 01:46:43 +00:00
|
|
|
// SignSSH creates a signed SSH certificate with the given public key and options.
|
2020-07-23 01:24:45 +00:00
|
|
|
func (a *Authority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) {
|
2020-07-27 22:43:41 +00:00
|
|
|
var (
|
|
|
|
certOptions []sshutil.Option
|
|
|
|
mods []provisioner.SSHCertModifier
|
|
|
|
validators []provisioner.SSHCertValidator
|
|
|
|
)
|
2019-07-24 01:46:43 +00:00
|
|
|
|
2020-08-04 01:36:05 +00:00
|
|
|
// Validate given options.
|
|
|
|
if err := opts.Validate(); err != nil {
|
2021-11-19 02:44:58 +00:00
|
|
|
return nil, err
|
2020-08-04 01:36:05 +00:00
|
|
|
}
|
|
|
|
|
2020-01-04 02:22:02 +00:00
|
|
|
// Set backdate with the configured value
|
|
|
|
opts.Backdate = a.config.AuthorityConfig.Backdate.Duration
|
|
|
|
|
2022-05-19 01:33:53 +00:00
|
|
|
var prov provisioner.Interface
|
2022-09-30 00:16:26 +00:00
|
|
|
var webhookCtl webhookController
|
2019-07-24 01:46:43 +00:00
|
|
|
for _, op := range signOpts {
|
|
|
|
switch o := op.(type) {
|
2022-05-19 01:33:53 +00:00
|
|
|
// Capture current provisioner
|
|
|
|
case provisioner.Interface:
|
|
|
|
prov = o
|
|
|
|
|
2020-07-27 22:43:41 +00:00
|
|
|
// add options to NewCertificate
|
|
|
|
case provisioner.SSHCertificateOptions:
|
|
|
|
certOptions = append(certOptions, o.Options(opts)...)
|
|
|
|
|
2019-07-24 01:46:43 +00:00
|
|
|
// modify the ssh.Certificate
|
2020-01-24 06:04:34 +00:00
|
|
|
case provisioner.SSHCertModifier:
|
2019-07-24 01:46:43 +00:00
|
|
|
mods = append(mods, o)
|
2020-07-27 22:43:41 +00:00
|
|
|
|
2019-07-24 01:46:43 +00:00
|
|
|
// validate the ssh.Certificate
|
2020-01-24 06:04:34 +00:00
|
|
|
case provisioner.SSHCertValidator:
|
2019-07-24 01:46:43 +00:00
|
|
|
validators = append(validators, o)
|
2020-07-27 22:43:41 +00:00
|
|
|
|
2019-07-24 01:46:43 +00:00
|
|
|
// validate the given SSHOptions
|
2020-01-24 06:04:34 +00:00
|
|
|
case provisioner.SSHCertOptionsValidator:
|
2019-07-24 01:46:43 +00:00
|
|
|
if err := o.Valid(opts); err != nil {
|
2021-11-24 01:52:17 +00:00
|
|
|
return nil, errs.BadRequestErr(err, "error validating ssh certificate options")
|
2019-07-24 01:46:43 +00:00
|
|
|
}
|
2020-07-27 22:43:41 +00:00
|
|
|
|
2022-09-30 00:16:26 +00:00
|
|
|
// call webhooks
|
|
|
|
case webhookController:
|
|
|
|
webhookCtl = o
|
|
|
|
|
2019-07-24 01:46:43 +00:00
|
|
|
default:
|
2020-07-29 23:06:39 +00:00
|
|
|
return nil, errs.InternalServer("authority.SignSSH: invalid extra option type %T", o)
|
2019-07-24 01:46:43 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-29 23:06:39 +00:00
|
|
|
// Simulated certificate request with request options.
|
|
|
|
cr := sshutil.CertificateRequest{
|
2020-07-30 02:26:46 +00:00
|
|
|
Type: opts.CertType,
|
2020-07-29 23:06:39 +00:00
|
|
|
KeyID: opts.KeyID,
|
|
|
|
Principals: opts.Principals,
|
|
|
|
Key: key,
|
|
|
|
}
|
|
|
|
|
2022-09-30 00:16:26 +00:00
|
|
|
// Call enriching webhooks
|
|
|
|
if err := callEnrichingWebhooksSSH(webhookCtl, cr); err != nil {
|
|
|
|
return nil, errs.ApplyOptions(
|
|
|
|
errs.ForbiddenErr(err, err.Error()),
|
|
|
|
errs.WithKeyVal("signOptions", signOpts),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2020-07-27 22:43:41 +00:00
|
|
|
// Create certificate from template.
|
2020-07-29 23:06:39 +00:00
|
|
|
certificate, err := sshutil.NewCertificate(cr, certOptions...)
|
2019-07-24 01:46:43 +00:00
|
|
|
if err != nil {
|
2022-09-21 06:07:16 +00:00
|
|
|
var te *sshutil.TemplateError
|
|
|
|
if errors.As(err, &te) {
|
2021-11-19 02:44:58 +00:00
|
|
|
return nil, errs.ApplyOptions(
|
2022-09-21 06:07:16 +00:00
|
|
|
errs.BadRequestErr(err, err.Error()),
|
2020-07-27 22:43:41 +00:00
|
|
|
errs.WithKeyVal("signOptions", signOpts),
|
|
|
|
)
|
|
|
|
}
|
2022-01-10 14:49:37 +00:00
|
|
|
// explicitly check for unmarshaling errors, which are most probably caused by JSON template syntax errors
|
|
|
|
if strings.HasPrefix(err.Error(), "error unmarshaling certificate") {
|
2022-01-12 09:41:36 +00:00
|
|
|
return nil, errs.InternalServerErr(templatingError(err),
|
2022-01-10 14:49:37 +00:00
|
|
|
errs.WithKeyVal("signOptions", signOpts),
|
2022-01-12 09:41:36 +00:00
|
|
|
errs.WithMessage("error applying certificate template"),
|
2022-01-10 14:49:37 +00:00
|
|
|
)
|
|
|
|
}
|
2020-07-27 22:43:41 +00:00
|
|
|
return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.SignSSH")
|
2019-07-24 01:46:43 +00:00
|
|
|
}
|
|
|
|
|
2020-07-29 23:06:39 +00:00
|
|
|
// Get actual *ssh.Certificate and continue with provisioner modifiers.
|
2020-08-28 21:29:18 +00:00
|
|
|
certTpl := certificate.GetCertificate()
|
2019-07-24 01:46:43 +00:00
|
|
|
|
2020-07-29 23:06:39 +00:00
|
|
|
// Use SignSSHOptions to modify the certificate validity. It will be later
|
|
|
|
// checked or set if not defined.
|
2020-08-28 21:29:18 +00:00
|
|
|
if err := opts.ModifyValidity(certTpl); err != nil {
|
2021-11-19 02:44:58 +00:00
|
|
|
return nil, errs.BadRequestErr(err, err.Error())
|
2019-07-24 01:46:43 +00:00
|
|
|
}
|
|
|
|
|
2020-07-27 22:43:41 +00:00
|
|
|
// Use provisioner modifiers.
|
2019-07-24 01:46:43 +00:00
|
|
|
for _, m := range mods {
|
2020-08-28 21:29:18 +00:00
|
|
|
if err := m.Modify(certTpl, opts); err != nil {
|
2021-11-23 20:04:51 +00:00
|
|
|
return nil, errs.ForbiddenErr(err, "error creating ssh certificate")
|
2019-07-24 01:46:43 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get signer from authority keys
|
|
|
|
var signer ssh.Signer
|
2020-08-28 21:29:18 +00:00
|
|
|
switch certTpl.CertType {
|
2019-07-24 01:46:43 +00:00
|
|
|
case ssh.UserCert:
|
2019-08-01 22:04:56 +00:00
|
|
|
if a.sshCAUserCertSignKey == nil {
|
2020-07-29 23:06:39 +00:00
|
|
|
return nil, errs.NotImplemented("authority.SignSSH: user certificate signing is not enabled")
|
2019-08-01 22:04:56 +00:00
|
|
|
}
|
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 {
|
2020-07-29 23:06:39 +00:00
|
|
|
return nil, errs.NotImplemented("authority.SignSSH: host certificate signing is not enabled")
|
2019-08-01 22:04:56 +00:00
|
|
|
}
|
2019-09-25 02:12:13 +00:00
|
|
|
signer = a.sshCAHostCertSignKey
|
2019-07-24 01:46:43 +00:00
|
|
|
default:
|
2020-08-28 21:29:18 +00:00
|
|
|
return nil, errs.InternalServer("authority.SignSSH: unexpected ssh certificate type: %d", certTpl.CertType)
|
2019-07-24 01:46:43 +00:00
|
|
|
}
|
|
|
|
|
2022-04-26 11:12:16 +00:00
|
|
|
// Check if authority is allowed to sign the certificate
|
|
|
|
if err := a.isAllowedToSignSSHCertificate(certTpl); err != nil {
|
2022-09-22 01:35:18 +00:00
|
|
|
var ee *errs.Error
|
|
|
|
if errors.As(err, &ee) {
|
|
|
|
return nil, ee
|
2022-03-08 12:26:07 +00:00
|
|
|
}
|
2022-04-26 11:12:16 +00:00
|
|
|
return nil, errs.InternalServerErr(err,
|
|
|
|
errs.WithMessage("authority.SignSSH: error creating ssh certificate"),
|
|
|
|
)
|
2022-03-08 12:26:07 +00:00
|
|
|
}
|
|
|
|
|
2022-09-30 00:16:26 +00:00
|
|
|
// Send certificate to webhooks for authorization
|
|
|
|
if err := callAuthorizingWebhooksSSH(webhookCtl, certificate, certTpl); err != nil {
|
|
|
|
return nil, errs.ApplyOptions(
|
|
|
|
errs.ForbiddenErr(err, "authority.SignSSH: error signing certificate"),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2020-07-27 22:43:41 +00:00
|
|
|
// Sign certificate.
|
2020-08-28 21:29:18 +00:00
|
|
|
cert, err := sshutil.CreateCertificate(certTpl, signer)
|
2019-07-24 01:46:43 +00:00
|
|
|
if err != nil {
|
2020-07-29 23:06:39 +00:00
|
|
|
return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.SignSSH: error signing certificate")
|
2019-07-24 01:46:43 +00:00
|
|
|
}
|
|
|
|
|
2020-07-27 22:43:41 +00:00
|
|
|
// User provisioners validators.
|
2019-07-24 01:46:43 +00:00
|
|
|
for _, v := range validators {
|
2020-01-24 21:42:00 +00:00
|
|
|
if err := v.Valid(cert, opts); err != nil {
|
2021-11-23 20:04:51 +00:00
|
|
|
return nil, errs.ForbiddenErr(err, "error validating ssh certificate")
|
2019-08-03 00:48:34 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-23 19:43:48 +00:00
|
|
|
if err = a.storeSSHCertificate(prov, cert); err != nil && !errors.Is(err, db.ErrNotImplemented) {
|
2020-07-29 23:06:39 +00:00
|
|
|
return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.SignSSH: error storing certificate in db")
|
2019-10-10 20:08:57 +00:00
|
|
|
}
|
|
|
|
|
2019-08-03 00:48:34 +00:00
|
|
|
return cert, nil
|
|
|
|
}
|
|
|
|
|
2022-04-26 11:12:16 +00:00
|
|
|
// isAllowedToSignSSHCertificate checks if the Authority is allowed to sign the SSH certificate.
|
|
|
|
func (a *Authority) isAllowedToSignSSHCertificate(cert *ssh.Certificate) error {
|
|
|
|
return a.policyEngine.IsSSHCertificateAllowed(cert)
|
|
|
|
}
|
|
|
|
|
2019-10-28 18:50:43 +00:00
|
|
|
// RenewSSH creates a signed SSH certificate using the old SSH certificate as a template.
|
2020-03-11 02:01:45 +00:00
|
|
|
func (a *Authority) RenewSSH(ctx context.Context, oldCert *ssh.Certificate) (*ssh.Certificate, error) {
|
2019-10-28 18:50:43 +00:00
|
|
|
if oldCert.ValidAfter == 0 || oldCert.ValidBefore == 0 {
|
2021-11-18 23:12:44 +00:00
|
|
|
return nil, errs.BadRequest("cannot renew a certificate without validity period")
|
2021-07-21 22:22:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if err := a.authorizeSSHCertificate(ctx, oldCert); err != nil {
|
|
|
|
return nil, err
|
2019-10-28 18:50:43 +00:00
|
|
|
}
|
2020-01-04 02:22:02 +00:00
|
|
|
|
2022-05-20 21:41:44 +00:00
|
|
|
// Attempt to extract the provisioner from the token.
|
|
|
|
var prov provisioner.Interface
|
|
|
|
if token, ok := provisioner.TokenFromContext(ctx); ok {
|
|
|
|
prov, _, _ = a.getProvisionerFromToken(token)
|
|
|
|
}
|
|
|
|
|
2020-01-04 02:22:02 +00:00
|
|
|
backdate := a.config.AuthorityConfig.Backdate.Duration
|
|
|
|
duration := time.Duration(oldCert.ValidBefore-oldCert.ValidAfter) * time.Second
|
|
|
|
now := time.Now()
|
|
|
|
va := now.Add(-1 * backdate)
|
|
|
|
vb := now.Add(duration - backdate)
|
2019-10-28 18:50:43 +00:00
|
|
|
|
2020-07-27 22:54:51 +00:00
|
|
|
// Build base certificate with the old key.
|
|
|
|
// Nonce and serial will be automatically generated on signing.
|
2020-08-28 21:29:18 +00:00
|
|
|
certTpl := &ssh.Certificate{
|
2019-10-28 18:50:43 +00:00
|
|
|
Key: oldCert.Key,
|
|
|
|
CertType: oldCert.CertType,
|
|
|
|
KeyId: oldCert.KeyId,
|
|
|
|
ValidPrincipals: oldCert.ValidPrincipals,
|
|
|
|
Permissions: oldCert.Permissions,
|
2020-07-27 22:54:51 +00:00
|
|
|
Reserved: oldCert.Reserved,
|
2019-10-28 18:50:43 +00:00
|
|
|
ValidAfter: uint64(va.Unix()),
|
|
|
|
ValidBefore: uint64(vb.Unix()),
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get signer from authority keys
|
|
|
|
var signer ssh.Signer
|
2020-08-28 21:29:18 +00:00
|
|
|
switch certTpl.CertType {
|
2019-10-28 18:50:43 +00:00
|
|
|
case ssh.UserCert:
|
|
|
|
if a.sshCAUserCertSignKey == nil {
|
2020-01-24 06:04:34 +00:00
|
|
|
return nil, errs.NotImplemented("renewSSH: user certificate signing is not enabled")
|
2019-10-28 18:50:43 +00:00
|
|
|
}
|
|
|
|
signer = a.sshCAUserCertSignKey
|
|
|
|
case ssh.HostCert:
|
|
|
|
if a.sshCAHostCertSignKey == nil {
|
2020-01-24 06:04:34 +00:00
|
|
|
return nil, errs.NotImplemented("renewSSH: host certificate signing is not enabled")
|
2019-10-28 18:50:43 +00:00
|
|
|
}
|
|
|
|
signer = a.sshCAHostCertSignKey
|
|
|
|
default:
|
2020-08-28 21:29:18 +00:00
|
|
|
return nil, errs.InternalServer("renewSSH: unexpected ssh certificate type: %d", certTpl.CertType)
|
2019-10-28 18:50:43 +00:00
|
|
|
}
|
|
|
|
|
2020-07-27 22:54:51 +00:00
|
|
|
// Sign certificate.
|
2020-08-28 21:29:18 +00:00
|
|
|
cert, err := sshutil.CreateCertificate(certTpl, signer)
|
2019-10-28 18:50:43 +00:00
|
|
|
if err != nil {
|
2020-07-27 22:54:51 +00:00
|
|
|
return nil, errs.Wrap(http.StatusInternalServerError, err, "signSSH: error signing certificate")
|
2019-10-28 18:50:43 +00:00
|
|
|
}
|
|
|
|
|
2022-08-23 19:43:48 +00:00
|
|
|
if err = a.storeRenewedSSHCertificate(prov, oldCert, cert); err != nil && !errors.Is(err, db.ErrNotImplemented) {
|
2019-12-20 21:30:05 +00:00
|
|
|
return nil, errs.Wrap(http.StatusInternalServerError, err, "renewSSH: error storing certificate in db")
|
2019-10-28 18:50:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return cert, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// RekeySSH creates a signed SSH certificate using the old SSH certificate as a template.
|
2020-03-11 02:01:45 +00:00
|
|
|
func (a *Authority) RekeySSH(ctx context.Context, oldCert *ssh.Certificate, pub ssh.PublicKey, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) {
|
2020-01-24 06:04:34 +00:00
|
|
|
var validators []provisioner.SSHCertValidator
|
2019-10-28 18:50:43 +00:00
|
|
|
|
2022-05-20 21:41:44 +00:00
|
|
|
var prov provisioner.Interface
|
2019-10-28 18:50:43 +00:00
|
|
|
for _, op := range signOpts {
|
|
|
|
switch o := op.(type) {
|
2022-05-20 21:41:44 +00:00
|
|
|
// Capture current provisioner
|
|
|
|
case provisioner.Interface:
|
|
|
|
prov = o
|
2019-10-28 18:50:43 +00:00
|
|
|
// validate the ssh.Certificate
|
2020-01-24 06:04:34 +00:00
|
|
|
case provisioner.SSHCertValidator:
|
2019-10-28 18:50:43 +00:00
|
|
|
validators = append(validators, o)
|
|
|
|
default:
|
2020-01-24 06:04:34 +00:00
|
|
|
return nil, errs.InternalServer("rekeySSH; invalid extra option type %T", o)
|
2019-10-28 18:50:43 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if oldCert.ValidAfter == 0 || oldCert.ValidBefore == 0 {
|
2021-11-18 23:12:44 +00:00
|
|
|
return nil, errs.BadRequest("cannot rekey a certificate without validity period")
|
2019-10-28 18:50:43 +00:00
|
|
|
}
|
2020-01-04 02:30:17 +00:00
|
|
|
|
2021-07-21 22:22:57 +00:00
|
|
|
if err := a.authorizeSSHCertificate(ctx, oldCert); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2020-01-04 02:30:17 +00:00
|
|
|
backdate := a.config.AuthorityConfig.Backdate.Duration
|
|
|
|
duration := time.Duration(oldCert.ValidBefore-oldCert.ValidAfter) * time.Second
|
|
|
|
now := time.Now()
|
|
|
|
va := now.Add(-1 * backdate)
|
|
|
|
vb := now.Add(duration - backdate)
|
2019-10-28 18:50:43 +00:00
|
|
|
|
2020-07-27 22:54:51 +00:00
|
|
|
// Build base certificate with the new key.
|
|
|
|
// Nonce and serial will be automatically generated on signing.
|
2019-10-28 18:50:43 +00:00
|
|
|
cert := &ssh.Certificate{
|
|
|
|
Key: pub,
|
|
|
|
CertType: oldCert.CertType,
|
|
|
|
KeyId: oldCert.KeyId,
|
|
|
|
ValidPrincipals: oldCert.ValidPrincipals,
|
|
|
|
Permissions: oldCert.Permissions,
|
2020-07-27 22:54:51 +00:00
|
|
|
Reserved: oldCert.Reserved,
|
2019-10-28 18:50:43 +00:00
|
|
|
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 {
|
2020-01-24 06:04:34 +00:00
|
|
|
return nil, errs.NotImplemented("rekeySSH; user certificate signing is not enabled")
|
2019-10-28 18:50:43 +00:00
|
|
|
}
|
|
|
|
signer = a.sshCAUserCertSignKey
|
|
|
|
case ssh.HostCert:
|
|
|
|
if a.sshCAHostCertSignKey == nil {
|
2020-01-24 06:04:34 +00:00
|
|
|
return nil, errs.NotImplemented("rekeySSH; host certificate signing is not enabled")
|
2019-10-28 18:50:43 +00:00
|
|
|
}
|
|
|
|
signer = a.sshCAHostCertSignKey
|
|
|
|
default:
|
2021-11-18 23:12:44 +00:00
|
|
|
return nil, errs.BadRequest("unexpected certificate type '%d'", cert.CertType)
|
2019-10-28 18:50:43 +00:00
|
|
|
}
|
|
|
|
|
2020-07-27 22:54:51 +00:00
|
|
|
var err error
|
|
|
|
// Sign certificate.
|
|
|
|
cert, err = sshutil.CreateCertificate(cert, signer)
|
2019-10-28 18:50:43 +00:00
|
|
|
if err != nil {
|
2020-07-27 22:54:51 +00:00
|
|
|
return nil, errs.Wrap(http.StatusInternalServerError, err, "signSSH: error signing certificate")
|
2019-10-28 18:50:43 +00:00
|
|
|
}
|
|
|
|
|
2020-01-24 21:42:00 +00:00
|
|
|
// Apply validators from provisioner.
|
2019-10-28 18:50:43 +00:00
|
|
|
for _, v := range validators {
|
2020-07-23 01:24:45 +00:00
|
|
|
if err := v.Valid(cert, provisioner.SignSSHOptions{Backdate: backdate}); err != nil {
|
2021-11-23 20:04:51 +00:00
|
|
|
return nil, errs.ForbiddenErr(err, "error validating ssh certificate")
|
2019-10-28 18:50:43 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-23 19:43:48 +00:00
|
|
|
if err = a.storeRenewedSSHCertificate(prov, oldCert, cert); err != nil && !errors.Is(err, db.ErrNotImplemented) {
|
2019-12-20 21:30:05 +00:00
|
|
|
return nil, errs.Wrap(http.StatusInternalServerError, err, "rekeySSH; error storing certificate in db")
|
2019-10-28 18:50:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return cert, nil
|
|
|
|
}
|
|
|
|
|
2022-05-19 01:33:53 +00:00
|
|
|
func (a *Authority) storeSSHCertificate(prov provisioner.Interface, cert *ssh.Certificate) error {
|
2021-07-21 01:16:24 +00:00
|
|
|
type sshCertificateStorer interface {
|
2022-05-19 01:33:53 +00:00
|
|
|
StoreSSHCertificate(provisioner.Interface, *ssh.Certificate) error
|
|
|
|
}
|
|
|
|
|
|
|
|
// Store certificate in admindb or linkedca
|
|
|
|
switch s := a.adminDB.(type) {
|
|
|
|
case sshCertificateStorer:
|
|
|
|
return s.StoreSSHCertificate(prov, cert)
|
|
|
|
case db.CertificateStorer:
|
|
|
|
return s.StoreSSHCertificate(cert)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Store certificate in localdb
|
|
|
|
switch s := a.db.(type) {
|
|
|
|
case sshCertificateStorer:
|
|
|
|
return s.StoreSSHCertificate(prov, cert)
|
|
|
|
case db.CertificateStorer:
|
|
|
|
return s.StoreSSHCertificate(cert)
|
|
|
|
default:
|
|
|
|
return nil
|
2021-07-21 01:16:24 +00:00
|
|
|
}
|
2022-05-19 01:33:53 +00:00
|
|
|
}
|
|
|
|
|
2022-05-20 21:41:44 +00:00
|
|
|
func (a *Authority) storeRenewedSSHCertificate(prov provisioner.Interface, parent, cert *ssh.Certificate) error {
|
2022-05-19 01:33:53 +00:00
|
|
|
type sshRenewerCertificateStorer interface {
|
2022-05-20 21:41:44 +00:00
|
|
|
StoreRenewedSSHCertificate(p provisioner.Interface, parent, cert *ssh.Certificate) error
|
2022-05-19 01:33:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Store certificate in admindb or linkedca
|
|
|
|
switch s := a.adminDB.(type) {
|
|
|
|
case sshRenewerCertificateStorer:
|
2022-05-20 21:41:44 +00:00
|
|
|
return s.StoreRenewedSSHCertificate(prov, parent, cert)
|
2022-05-19 01:33:53 +00:00
|
|
|
case db.CertificateStorer:
|
2021-07-21 01:16:24 +00:00
|
|
|
return s.StoreSSHCertificate(cert)
|
|
|
|
}
|
2022-05-19 01:33:53 +00:00
|
|
|
|
|
|
|
// Store certificate in localdb
|
|
|
|
switch s := a.db.(type) {
|
|
|
|
case sshRenewerCertificateStorer:
|
2022-05-20 21:41:44 +00:00
|
|
|
return s.StoreRenewedSSHCertificate(prov, parent, cert)
|
2022-05-19 01:33:53 +00:00
|
|
|
case db.CertificateStorer:
|
|
|
|
return s.StoreSSHCertificate(cert)
|
|
|
|
default:
|
|
|
|
return nil
|
|
|
|
}
|
2021-07-21 01:16:24 +00:00
|
|
|
}
|
|
|
|
|
2020-04-24 02:42:55 +00:00
|
|
|
// IsValidForAddUser checks if a user provisioner certificate can be issued to
|
|
|
|
// the given certificate.
|
|
|
|
func IsValidForAddUser(cert *ssh.Certificate) error {
|
|
|
|
if cert.CertType != ssh.UserCert {
|
2021-11-23 20:04:51 +00:00
|
|
|
return errs.Forbidden("certificate is not a user certificate")
|
2020-04-24 02:42:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
switch len(cert.ValidPrincipals) {
|
|
|
|
case 0:
|
2021-11-23 20:04:51 +00:00
|
|
|
return errs.Forbidden("certificate does not have any principals")
|
2020-04-24 02:42:55 +00:00
|
|
|
case 1:
|
|
|
|
return nil
|
|
|
|
case 2:
|
|
|
|
// OIDC provisioners adds a second principal with the email address.
|
|
|
|
// @ cannot be the first character.
|
|
|
|
if strings.Index(cert.ValidPrincipals[1], "@") > 0 {
|
|
|
|
return nil
|
|
|
|
}
|
2021-11-23 20:04:51 +00:00
|
|
|
return errs.Forbidden("certificate does not have only one principal")
|
2020-04-24 02:42:55 +00:00
|
|
|
default:
|
2021-11-23 20:04:51 +00:00
|
|
|
return errs.Forbidden("certificate does not have only one principal")
|
2020-04-24 02:42:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-03 00:48:34 +00:00
|
|
|
// SignSSHAddUser signs a certificate that provisions a new user in a server.
|
2020-03-11 02:01:45 +00:00
|
|
|
func (a *Authority) SignSSHAddUser(ctx context.Context, key ssh.PublicKey, subject *ssh.Certificate) (*ssh.Certificate, error) {
|
2019-08-03 00:48:34 +00:00
|
|
|
if a.sshCAUserCertSignKey == nil {
|
2020-01-24 06:04:34 +00:00
|
|
|
return nil, errs.NotImplemented("signSSHAddUser: user certificate signing is not enabled")
|
2019-08-03 00:48:34 +00:00
|
|
|
}
|
2020-04-24 02:42:55 +00:00
|
|
|
if err := IsValidForAddUser(subject); err != nil {
|
2021-11-23 20:04:51 +00:00
|
|
|
return nil, err
|
2019-08-03 00:48:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
nonce, err := randutil.ASCII(32)
|
|
|
|
if err != nil {
|
2020-01-24 06:04:34 +00:00
|
|
|
return nil, errs.Wrap(http.StatusInternalServerError, err, "signSSHAddUser")
|
2019-08-03 00:48:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var serial uint64
|
|
|
|
if err := binary.Read(rand.Reader, binary.BigEndian, &serial); err != nil {
|
2019-12-20 21:30:05 +00:00
|
|
|
return nil, errs.Wrap(http.StatusInternalServerError, err, "signSSHAddUser: error reading random number")
|
2019-08-03 00:48:34 +00:00
|
|
|
}
|
|
|
|
|
2022-05-20 21:41:44 +00:00
|
|
|
// Attempt to extract the provisioner from the token.
|
|
|
|
var prov provisioner.Interface
|
|
|
|
if token, ok := provisioner.TokenFromContext(ctx); ok {
|
|
|
|
prov, _, _ = a.getProvisionerFromToken(token)
|
|
|
|
}
|
|
|
|
|
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
|
|
|
|
2022-08-23 19:43:48 +00:00
|
|
|
if err = a.storeRenewedSSHCertificate(prov, subject, cert); err != nil && !errors.Is(err, db.ErrNotImplemented) {
|
2019-12-20 21:30:05 +00:00
|
|
|
return nil, errs.Wrap(http.StatusInternalServerError, err, "signSSHAddUser: error storing certificate in db")
|
2019-10-10 20:08:57 +00:00
|
|
|
}
|
|
|
|
|
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.
|
2021-10-08 18:59:57 +00:00
|
|
|
func (a *Authority) CheckSSHHost(ctx context.Context, principal, token string) (bool, error) {
|
2019-12-10 07:14:56 +00:00
|
|
|
if a.sshCheckHostFunc != nil {
|
|
|
|
exists, err := a.sshCheckHostFunc(ctx, principal, token, a.GetRootCertificates())
|
|
|
|
if err != nil {
|
2019-12-16 07:54:25 +00:00
|
|
|
return false, errs.Wrap(http.StatusInternalServerError, err,
|
|
|
|
"checkSSHHost: error from injected checkSSHHost func")
|
2019-12-10 07:14:56 +00:00
|
|
|
}
|
|
|
|
return exists, nil
|
|
|
|
}
|
2019-10-10 20:08:57 +00:00
|
|
|
exists, err := a.db.IsSSHHost(principal)
|
|
|
|
if err != nil {
|
2022-08-23 19:43:48 +00:00
|
|
|
if errors.Is(err, db.ErrNotImplemented) {
|
2019-12-16 07:54:25 +00:00
|
|
|
return false, errs.Wrap(http.StatusNotImplemented, err,
|
|
|
|
"checkSSHHost: isSSHHost is not implemented")
|
2019-10-10 20:08:57 +00:00
|
|
|
}
|
2019-12-16 07:54:25 +00:00
|
|
|
return false, errs.Wrap(http.StatusInternalServerError, err,
|
|
|
|
"checkSSHHost: error checking if hosts exists")
|
2019-10-10 20:08:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return exists, nil
|
|
|
|
}
|
|
|
|
|
2019-10-25 20:47:49 +00:00
|
|
|
// GetSSHHosts returns a list of valid host principals.
|
2021-05-03 19:48:20 +00:00
|
|
|
func (a *Authority) GetSSHHosts(ctx context.Context, cert *x509.Certificate) ([]config.Host, error) {
|
2022-08-05 01:44:44 +00:00
|
|
|
if a.GetConfig().AuthorityConfig.DisableGetSSHHosts {
|
2022-07-28 06:30:00 +00:00
|
|
|
return nil, errs.New(http.StatusNotFound, "ssh hosts list api disabled")
|
|
|
|
}
|
2019-11-20 20:59:48 +00:00
|
|
|
if a.sshGetHostsFunc != nil {
|
2020-03-11 02:01:45 +00:00
|
|
|
hosts, err := a.sshGetHostsFunc(ctx, cert)
|
2019-12-20 21:30:05 +00:00
|
|
|
return hosts, errs.Wrap(http.StatusInternalServerError, err, "getSSHHosts")
|
2019-11-20 19:32:27 +00:00
|
|
|
}
|
2019-11-21 01:23:51 +00:00
|
|
|
hostnames, err := a.db.GetSSHHostPrincipals()
|
2019-11-20 20:59:48 +00:00
|
|
|
if err != nil {
|
2019-12-20 21:30:05 +00:00
|
|
|
return nil, errs.Wrap(http.StatusInternalServerError, err, "getSSHHosts")
|
2019-10-25 20:47:49 +00:00
|
|
|
}
|
2019-11-21 01:23:51 +00:00
|
|
|
|
2021-05-03 19:48:20 +00:00
|
|
|
hosts := make([]config.Host, len(hostnames))
|
2019-11-21 01:23:51 +00:00
|
|
|
for i, hn := range hostnames {
|
2021-05-03 19:48:20 +00:00
|
|
|
hosts[i] = config.Host{Hostname: hn}
|
2019-11-21 01:23:51 +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
|
|
|
|
}
|
2021-10-08 18:59:57 +00:00
|
|
|
return strings.ReplaceAll(cmd, "<principal>", principal)
|
2019-08-03 00:48:34 +00:00
|
|
|
}
|
2022-09-30 00:16:26 +00:00
|
|
|
|
|
|
|
func callEnrichingWebhooksSSH(webhookCtl webhookController, cr sshutil.CertificateRequest) error {
|
|
|
|
if webhookCtl == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
whEnrichReq, err := webhook.NewRequestBody(
|
|
|
|
webhook.WithSSHCertificateRequest(cr),
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := webhookCtl.Enrich(whEnrichReq); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func callAuthorizingWebhooksSSH(webhookCtl webhookController, cert *sshutil.Certificate, certTpl *ssh.Certificate) error {
|
|
|
|
if webhookCtl == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
whAuthBody, err := webhook.NewRequestBody(
|
|
|
|
webhook.WithSSHCertificate(cert, certTpl),
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := webhookCtl.Authorize(whAuthBody); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|