forked from TrueCloudLab/certificates
Add initial support for check-host endpoint.
This commit is contained in:
parent
a50d59338e
commit
3ee0dcec93
6 changed files with 170 additions and 4 deletions
|
@ -256,6 +256,7 @@ func (h *caHandler) Route(r Router) {
|
||||||
r.MethodFunc("GET", "/ssh/federation", h.SSHFederation)
|
r.MethodFunc("GET", "/ssh/federation", h.SSHFederation)
|
||||||
r.MethodFunc("POST", "/ssh/config", h.SSHConfig)
|
r.MethodFunc("POST", "/ssh/config", h.SSHConfig)
|
||||||
r.MethodFunc("POST", "/ssh/config/{type}", h.SSHConfig)
|
r.MethodFunc("POST", "/ssh/config/{type}", h.SSHConfig)
|
||||||
|
r.MethodFunc("POST", "/ssh/check-host", h.SSHCheckHost)
|
||||||
|
|
||||||
// For compatibility with old code:
|
// For compatibility with old code:
|
||||||
r.MethodFunc("POST", "/re-sign", h.Renew)
|
r.MethodFunc("POST", "/re-sign", h.Renew)
|
||||||
|
|
49
api/ssh.go
49
api/ssh.go
|
@ -20,6 +20,7 @@ type SSHAuthority interface {
|
||||||
GetSSHRoots() (*authority.SSHKeys, error)
|
GetSSHRoots() (*authority.SSHKeys, error)
|
||||||
GetSSHFederation() (*authority.SSHKeys, error)
|
GetSSHFederation() (*authority.SSHKeys, error)
|
||||||
GetSSHConfig(typ string, data map[string]string) ([]templates.Output, error)
|
GetSSHConfig(typ string, data map[string]string) ([]templates.Output, error)
|
||||||
|
CheckSSHHost(principal string) (bool, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SSHSignRequest is the request body of an SSH certificate request.
|
// SSHSignRequest is the request body of an SSH certificate request.
|
||||||
|
@ -170,6 +171,32 @@ type SSHConfigResponse struct {
|
||||||
HostTemplates []Template `json:"hostTemplates,omitempty"`
|
HostTemplates []Template `json:"hostTemplates,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SSHCheckPrincipalRequest is the request body used to check if a principal
|
||||||
|
// certificate has been created. Right now it only supported for hosts
|
||||||
|
// certificates.
|
||||||
|
type SSHCheckPrincipalRequest struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Principal string `json:"principal"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks the check principal request.
|
||||||
|
func (r *SSHCheckPrincipalRequest) Validate() error {
|
||||||
|
switch {
|
||||||
|
case r.Type != provisioner.SSHHostCert:
|
||||||
|
return errors.Errorf("unsupported type %s", r.Type)
|
||||||
|
case r.Principal == "":
|
||||||
|
return errors.New("missing or empty principal")
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSHCheckPrincipalResponse is the response body used to check if a principal
|
||||||
|
// exists.
|
||||||
|
type SSHCheckPrincipalResponse struct {
|
||||||
|
Exists bool `json:"exists"`
|
||||||
|
}
|
||||||
|
|
||||||
// SSHSign is an HTTP handler that reads an SignSSHRequest with a one-time-token
|
// SSHSign is an HTTP handler that reads an SignSSHRequest with a one-time-token
|
||||||
// (ott) from the body and creates a new SSH certificate with the information in
|
// (ott) from the body and creates a new SSH certificate with the information in
|
||||||
// the request.
|
// the request.
|
||||||
|
@ -320,3 +347,25 @@ func (h *caHandler) SSHConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
JSON(w, config)
|
JSON(w, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SSHCheckHost is the HTTP handler that returns if a hosts certificate exists or not.
|
||||||
|
func (h *caHandler) SSHCheckHost(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var body SSHCheckPrincipalRequest
|
||||||
|
if err := ReadJSON(r.Body, &body); err != nil {
|
||||||
|
WriteError(w, BadRequest(errors.Wrap(err, "error reading request body")))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := body.Validate(); err != nil {
|
||||||
|
WriteError(w, BadRequest(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
exists, err := h.Authority.CheckSSHHost(body.Principal)
|
||||||
|
if err != nil {
|
||||||
|
WriteError(w, InternalServerError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
JSON(w, &SSHCheckPrincipalResponse{
|
||||||
|
Exists: exists,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/smallstep/certificates/authority/provisioner"
|
"github.com/smallstep/certificates/authority/provisioner"
|
||||||
|
"github.com/smallstep/certificates/db"
|
||||||
"github.com/smallstep/certificates/templates"
|
"github.com/smallstep/certificates/templates"
|
||||||
"github.com/smallstep/cli/crypto/randutil"
|
"github.com/smallstep/cli/crypto/randutil"
|
||||||
"github.com/smallstep/cli/jose"
|
"github.com/smallstep/cli/jose"
|
||||||
|
@ -263,6 +264,13 @@ func (a *Authority) SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, sign
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return cert, nil
|
return cert, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -276,13 +284,13 @@ func (a *Authority) SignSSHAddUser(key ssh.PublicKey, subject *ssh.Certificate)
|
||||||
}
|
}
|
||||||
if subject.CertType != ssh.UserCert {
|
if subject.CertType != ssh.UserCert {
|
||||||
return nil, &apiError{
|
return nil, &apiError{
|
||||||
err: errors.New("signSSHProxy: certificate is not a user certificate"),
|
err: errors.New("signSSHAddUser: certificate is not a user certificate"),
|
||||||
code: http.StatusForbidden,
|
code: http.StatusForbidden,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(subject.ValidPrincipals) != 1 {
|
if len(subject.ValidPrincipals) != 1 {
|
||||||
return nil, &apiError{
|
return nil, &apiError{
|
||||||
err: errors.New("signSSHProxy: certificate does not have only one principal"),
|
err: errors.New("signSSHAddUser: certificate does not have only one principal"),
|
||||||
code: http.StatusForbidden,
|
code: http.StatusForbidden,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -295,7 +303,7 @@ func (a *Authority) SignSSHAddUser(key ssh.PublicKey, subject *ssh.Certificate)
|
||||||
var serial uint64
|
var serial uint64
|
||||||
if err := binary.Read(rand.Reader, binary.BigEndian, &serial); err != nil {
|
if err := binary.Read(rand.Reader, binary.BigEndian, &serial); err != nil {
|
||||||
return nil, &apiError{
|
return nil, &apiError{
|
||||||
err: errors.Wrap(err, "signSSHProxy: error reading random number"),
|
err: errors.Wrap(err, "signSSHAddUser: error reading random number"),
|
||||||
code: http.StatusInternalServerError,
|
code: http.StatusInternalServerError,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -331,9 +339,36 @@ func (a *Authority) SignSSHAddUser(key ssh.PublicKey, subject *ssh.Certificate)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
cert.Signature = sig
|
cert.Signature = sig
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return cert, nil
|
return cert, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
func (a *Authority) getAddUserPrincipal() (cmd string) {
|
func (a *Authority) getAddUserPrincipal() (cmd string) {
|
||||||
if a.config.SSH.AddUserPrincipal == "" {
|
if a.config.SSH.AddUserPrincipal == "" {
|
||||||
return SSHAddUserPrincipal
|
return SSHAddUserPrincipal
|
||||||
|
|
27
ca/client.go
27
ca/client.go
|
@ -21,6 +21,8 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/smallstep/certificates/authority/provisioner"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/smallstep/certificates/api"
|
"github.com/smallstep/certificates/api"
|
||||||
"github.com/smallstep/certificates/authority"
|
"github.com/smallstep/certificates/authority"
|
||||||
|
@ -585,6 +587,31 @@ func (c *Client) SSHConfig(req *api.SSHConfigRequest) (*api.SSHConfigResponse, e
|
||||||
return &config, nil
|
return &config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SSHCheckHost performs the POST /ssh/check-host request to the CA with the
|
||||||
|
// given principal.
|
||||||
|
func (c *Client) SSHCheckHost(principal string) (*api.SSHCheckPrincipalResponse, error) {
|
||||||
|
body, err := json.Marshal(&api.SSHCheckPrincipalRequest{
|
||||||
|
Type: provisioner.SSHHostCert,
|
||||||
|
Principal: principal,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "error marshaling request")
|
||||||
|
}
|
||||||
|
u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/check-host"})
|
||||||
|
resp, err := c.client.Post(u.String(), "application/json", bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "client POST %s failed", u)
|
||||||
|
}
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
return nil, readError(resp.Body)
|
||||||
|
}
|
||||||
|
var check api.SSHCheckPrincipalResponse
|
||||||
|
if err := readJSON(resp.Body, &check); err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "error reading %s", u)
|
||||||
|
}
|
||||||
|
return &check, nil
|
||||||
|
}
|
||||||
|
|
||||||
// RootFingerprint is a helper method that returns the current root fingerprint.
|
// RootFingerprint is a helper method that returns the current root fingerprint.
|
||||||
// It does an health connection and gets the fingerprint from the TLS verified
|
// It does an health connection and gets the fingerprint from the TLS verified
|
||||||
// chains.
|
// chains.
|
||||||
|
|
45
db/db.go
45
db/db.go
|
@ -3,17 +3,23 @@ package db
|
||||||
import (
|
import (
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/smallstep/nosql"
|
"github.com/smallstep/nosql"
|
||||||
"github.com/smallstep/nosql/database"
|
"github.com/smallstep/nosql/database"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
certsTable = []byte("x509_certs")
|
certsTable = []byte("x509_certs")
|
||||||
revokedCertsTable = []byte("revoked_x509_certs")
|
revokedCertsTable = []byte("revoked_x509_certs")
|
||||||
usedOTTTable = []byte("used_ott")
|
usedOTTTable = []byte("used_ott")
|
||||||
|
sshCertsTable = []byte("ssh_certs")
|
||||||
|
sshHostsTable = []byte("ssh_hosts")
|
||||||
|
sshUsersTable = []byte("ssh_users")
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrAlreadyExists can be returned if the DB attempts to set a key that has
|
// ErrAlreadyExists can be returned if the DB attempts to set a key that has
|
||||||
|
@ -34,6 +40,8 @@ type AuthDB interface {
|
||||||
Revoke(rci *RevokedCertificateInfo) error
|
Revoke(rci *RevokedCertificateInfo) error
|
||||||
StoreCertificate(crt *x509.Certificate) error
|
StoreCertificate(crt *x509.Certificate) error
|
||||||
UseToken(id, tok string) (bool, error)
|
UseToken(id, tok string) (bool, error)
|
||||||
|
IsSSHHost(name string) (bool, error)
|
||||||
|
StoreSSHCertificate(crt *ssh.Certificate) error
|
||||||
Shutdown() error
|
Shutdown() error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,7 +63,10 @@ func New(c *Config) (AuthDB, error) {
|
||||||
return nil, errors.Wrapf(err, "Error opening database of Type %s with source %s", c.Type, c.DataSource)
|
return nil, errors.Wrapf(err, "Error opening database of Type %s with source %s", c.Type, c.DataSource)
|
||||||
}
|
}
|
||||||
|
|
||||||
tables := [][]byte{revokedCertsTable, certsTable, usedOTTTable}
|
tables := [][]byte{
|
||||||
|
revokedCertsTable, certsTable, usedOTTTable,
|
||||||
|
sshCertsTable, sshHostsTable, sshUsersTable,
|
||||||
|
}
|
||||||
for _, b := range tables {
|
for _, b := range tables {
|
||||||
if err := db.CreateTable(b); err != nil {
|
if err := db.CreateTable(b); err != nil {
|
||||||
return nil, errors.Wrapf(err, "error creating table %s",
|
return nil, errors.Wrapf(err, "error creating table %s",
|
||||||
|
@ -138,6 +149,38 @@ func (db *DB) UseToken(id, tok string) (bool, error) {
|
||||||
return swapped, nil
|
return swapped, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsSSHHost returns if a principal is present in the ssh hosts table.
|
||||||
|
func (db *DB) IsSSHHost(principal string) (bool, error) {
|
||||||
|
if _, err := db.Get(sshHostsTable, []byte(strings.ToLower(principal))); err != nil {
|
||||||
|
if database.IsErrNotFound(err) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return false, errors.Wrap(err, "database Get error")
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StoreSSHCertificate stores an SSH certificate.
|
||||||
|
func (db *DB) StoreSSHCertificate(crt *ssh.Certificate) error {
|
||||||
|
var table []byte
|
||||||
|
serial := strconv.FormatUint(crt.Serial, 10)
|
||||||
|
tx := new(database.Tx)
|
||||||
|
tx.Set(sshCertsTable, []byte(serial), crt.Marshal())
|
||||||
|
if crt.CertType == ssh.HostCert {
|
||||||
|
table = sshHostsTable
|
||||||
|
} else {
|
||||||
|
table = sshUsersTable
|
||||||
|
}
|
||||||
|
for _, p := range crt.ValidPrincipals {
|
||||||
|
tx.Set(table, []byte(strings.ToLower(p)), []byte(serial))
|
||||||
|
}
|
||||||
|
if err := db.Update(tx); err != nil {
|
||||||
|
return errors.Wrap(err, "database Update error")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// Shutdown sends a shutdown message to the database.
|
// Shutdown sends a shutdown message to the database.
|
||||||
func (db *DB) Shutdown() error {
|
func (db *DB) Shutdown() error {
|
||||||
if db.isUp {
|
if db.isUp {
|
||||||
|
|
11
db/simple.go
11
db/simple.go
|
@ -7,6 +7,7 @@ import (
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/smallstep/nosql/database"
|
"github.com/smallstep/nosql/database"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrNotImplemented is an error returned when an operation is Not Implemented.
|
// ErrNotImplemented is an error returned when an operation is Not Implemented.
|
||||||
|
@ -58,6 +59,16 @@ func (s *SimpleDB) UseToken(id, tok string) (bool, error) {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsSSHHost returns a "NotImplemented" error.
|
||||||
|
func (s *SimpleDB) IsSSHHost(principal string) (bool, error) {
|
||||||
|
return false, ErrNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
// StoreSSHCertificate returns a "NotImplemented" error.
|
||||||
|
func (s *SimpleDB) StoreSSHCertificate(crt *ssh.Certificate) error {
|
||||||
|
return ErrNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
// Shutdown returns nil
|
// Shutdown returns nil
|
||||||
func (s *SimpleDB) Shutdown() error {
|
func (s *SimpleDB) Shutdown() error {
|
||||||
return nil
|
return nil
|
||||||
|
|
Loading…
Reference in a new issue