Browse Source

Split authority.Issuer and CertificateAuthority

master
parent
commit
747872b214
9 changed files with 370 additions and 159 deletions
  1. +195
    -0
      authority/issuer.go
  2. +124
    -0
      authority/key.go
  3. +13
    -12
      client.go
  4. +30
    -16
      cmd/stronghold/main.go
  5. +1
    -1
      crypto.go
  6. +1
    -1
      io.go
  7. +1
    -1
      proxy.go
  8. +5
    -5
      server.go
  9. +0
    -123
      server/hostkey/signer.go

+ 195
- 0
authority/issuer.go View File

@ -0,0 +1,195 @@
package authority
import (
"crypto/rand"
"io/ioutil"
"net"
"os"
"path/filepath"
"strings"
"time"
"golang.org/x/crypto/ssh"
)
const (
defaultCertificateLifetime = 5 * time.Minute
defaultLifetimeSplay = 5 * time.Second
defaultKeyPoolSize = 4
)
// Issuer for SSH keys.
type Issuer interface {
HostIssuer
UserIssuer
}
// HostIssuer can issue host SSH keys.
type HostIssuer interface {
// IssueHostKey returns the host key for the given host.
IssueHostKey(name string) (ssh.Signer, error)
}
// UserIssuer can issue user SSH keys.
type UserIssuer interface {
// IssueUserKey returns the user key for the given user.
IssueUserKey(name string) (ssh.Signer, error)
}
type ephemeralKeyIssuer struct {
pool *keyPool
}
// EphemeralKeyIssuer can issue ephemeral 2048-bit RSA keys.
func EphemeralKeyIssuer() Issuer {
return ephemeralKeyIssuer{pool: newHostKeyPool(defaultKeyPoolSize)}
}
func (issuer ephemeralKeyIssuer) IssueHostKey(_ string) (ssh.Signer, error) {
return issuer.pool.Signer(), nil
}
func (issuer ephemeralKeyIssuer) IssueUserKey(_ string) (ssh.Signer, error) {
return issuer.pool.Signer(), nil
}
type cachedKeyIssuer struct {
root string
pool *keyPool
}
// CachedKeyIssuer can issue keys from cache, or create a new 2048-bit RSA key.
func CachedKeyIssuer(root string) (Issuer, error) {
for _, dir := range []string{
root,
filepath.Join(root, "host"),
filepath.Join(root, "user"),
} {
if err := os.MkdirAll(dir, 0700); err != nil && !os.IsExist(err) {
return nil, err
}
}
return &cachedKeyIssuer{
root: root,
pool: newHostKeyPool(defaultKeyPoolSize),
}, nil
}
func (issuer cachedKeyIssuer) IssueHostKey(name string) (ssh.Signer, error) {
return issuer.getOrCreateKey(filepath.Join(issuer.root, "host", strings.ToLower(name)+".key"))
}
func (issuer cachedKeyIssuer) IssueUserKey(name string) (ssh.Signer, error) {
return issuer.getOrCreateKey(filepath.Join(issuer.root, "user", strings.ToLower(name)+".key"))
}
func (issuer cachedKeyIssuer) getOrCreateKey(name string) (ssh.Signer, error) {
if signer, err := loadSigner(name); err != nil && !os.IsNotExist(err) {
return nil, err
} else if err == nil {
return signer, nil
}
privateKey := issuer.pool.Key()
if err := savePrivateKey(name, privateKey); err != nil {
return nil, err
}
return ssh.NewSignerFromKey(privateKey)
}
// CertificateAuthority authority can issue host and user certificates.
type CertificateAuthority struct {
signer ssh.Signer
issuer Issuer
lifetime time.Duration
}
func NewCertificateAuthority(caKeyFile string, issuer Issuer) (*CertificateAuthority, error) {
if issuer == nil {
issuer = EphemeralKeyIssuer()
}
pemBytes, err := ioutil.ReadFile(caKeyFile)
if err != nil {
return nil, err
}
var signer ssh.Signer
if signer, err = ssh.ParsePrivateKey(pemBytes); err != nil {
return nil, err
}
return &CertificateAuthority{
signer: signer,
lifetime: defaultCertificateLifetime,
issuer: issuer,
}, nil
}
func (ca *CertificateAuthority) SignCertificate(cert *ssh.Certificate) error {
return cert.SignCert(rand.Reader, ca.signer)
}
func (ca *CertificateAuthority) SignUserCertificate(cert *ssh.Certificate) error {
cert.CertType = ssh.UserCert
return ca.SignCertificate(cert)
}
func (ca *CertificateAuthority) SignHostCertificate(cert *ssh.Certificate) error {
cert.CertType = ssh.HostCert
return ca.SignCertificate(cert)
}
func (ca *CertificateAuthority) HostCertificate(name string) ([]ssh.Signer, error) {
ips, err := net.LookupIP(name)
if err != nil {
return nil, err
}
principals := []string{name}
for _, ip := range ips {
principals = append(principals, ip.String())
}
var signer ssh.Signer
if signer, err = ca.issuer.IssueHostKey(name); err != nil {
return nil, err
}
var (
now = time.Now()
validAfter = now.Add(-defaultLifetimeSplay)
validBefore = now.Add(defaultLifetimeSplay + ca.lifetime)
cert = &ssh.Certificate{
Nonce: nil,
Key: signer.PublicKey(),
Serial: uint64(now.UnixNano()),
CertType: ssh.HostCert,
KeyId: name,
ValidPrincipals: uniqueStrings(principals),
ValidAfter: uint64(validAfter.Unix()),
ValidBefore: uint64(validBefore.Unix()),
}
certSigner ssh.Signer
)
if err = cert.SignCert(rand.Reader, ca.signer); err != nil {
return nil, err
}
if certSigner, err = ssh.NewCertSigner(cert, signer); err != nil {
return nil, err
}
return []ssh.Signer{certSigner, signer}, nil
}
func uniqueStrings(values []string) []string {
var (
keys = make(map[string]bool)
uniq []string
)
for _, key := range values {
if !keys[key] {
keys[key] = true
uniq = append(uniq, key)
}
}
return uniq
}

+ 124
- 0
authority/key.go View File

@ -0,0 +1,124 @@
package authority
import (
"crypto"
"crypto/dsa"
"crypto/ecdsa"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/asn1"
"encoding/pem"
"fmt"
"io/ioutil"
"math/big"
"github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh"
)
type keyPool struct {
keys chan *rsa.PrivateKey
}
func newHostKeyPool(size int) *keyPool {
pool := &keyPool{keys: make(chan *rsa.PrivateKey, size)}
go pool.fill()
return pool
}
func (pool *keyPool) fill() {
log := logrus.WithField("tag", "key_pool")
for {
log.Debug("generating RSA key")
k, err := rsa.GenerateKey(rand.Reader, 2048)
if err == nil {
pool.keys <- k
}
}
}
func (pool *keyPool) Key() *rsa.PrivateKey {
return <-pool.keys
}
func (pool *keyPool) Signer() ssh.Signer {
signer, _ := ssh.NewSignerFromKey(<-pool.keys)
return signer
}
func loadPrivateKey(name string) (key crypto.PrivateKey, err error) {
var pemBytes []byte
if pemBytes, err = ioutil.ReadFile(name); err != nil {
return
}
var block *pem.Block
for {
if block, pemBytes = pem.Decode(pemBytes); block == nil {
return nil, fmt.Errorf("authority: no private key found in %s", name)
}
switch block.Type {
case "DSA PRIVATE KEY":
return ssh.ParseDSAPrivateKey(block.Bytes)
case "RSA PRIVATE KEY":
return x509.ParsePKCS1PrivateKey(block.Bytes)
case "EC PRIVATE KEY":
return x509.ParseECPrivateKey(block.Bytes)
case "PRIVATE KEY":
return x509.ParsePKCS8PrivateKey(block.Bytes)
}
}
}
func loadSigner(name string) (signer ssh.Signer, err error) {
var privateKey crypto.PrivateKey
if privateKey, err = loadPrivateKey(name); err != nil {
return
}
return ssh.NewSignerFromKey(privateKey)
}
func savePrivateKey(name string, key crypto.PrivateKey) (err error) {
var pemBytes []byte
switch key := key.(type) {
case *dsa.PrivateKey:
var k struct {
Version int
P *big.Int
Q *big.Int
G *big.Int
Pub *big.Int
Priv *big.Int
}
k.Version = 1
k.P = key.P
k.Q = key.Q
k.G = key.G
k.Pub = key.PublicKey.Y
k.Priv = key.X
var derBytes []byte
if derBytes, err = asn1.Marshal(&k); err != nil {
return
}
pemBytes = pem.EncodeToMemory(&pem.Block{
Type: "DSA PRIVATE KEY",
Bytes: derBytes,
})
case *ecdsa.PrivateKey:
var derBytes []byte
if derBytes, err = x509.MarshalECPrivateKey(key); err != nil {
return
}
pemBytes = pem.EncodeToMemory(&pem.Block{
Type: "EC PRIVATE KEY",
Bytes: derBytes,
})
case *rsa.PrivateKey:
pemBytes = pem.EncodeToMemory(&pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(key),
})
}
return ioutil.WriteFile(name, pemBytes, 0600)
}

server/client.go → client.go View File


+ 30
- 16
cmd/stronghold/main.go View File

@ -8,23 +8,25 @@ import (
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/terminal"
"git.maze.io/maze/stronghold/server"
"git.maze.io/maze/stronghold/server/hostkey"
"git.maze.io/maze/stronghold"
"git.maze.io/maze/stronghold/authority"
)
var (
defaultHostKeyFile = "testdata/ssh_host_rsa_key"
defaultCAKeyFile = "testdata/ssh_host_rsa_key"
defaultRootPath = "testdata/session"
defaultKeyCacheDir = ""
)
func main() {
var (
listen = flag.String("listen", server.DefaultAddr, "server listen address")
hostKey = flag.String("key", defaultHostKeyFile, "server host key")
caKey = flag.String("ca-key", defaultCAKeyFile, "server certificate authority key")
root = flag.String("root", defaultRootPath, "server recording root")
debug = flag.Bool("debug", false, "enable debug messages")
listen = flag.String("listen", stronghold.DefaultAddr, "server listen address")
hostKey = flag.String("key", defaultHostKeyFile, "server host key")
caKey = flag.String("ca-key", defaultCAKeyFile, "server certificate authority key")
keyCacheDir = flag.String("key-cache", defaultKeyCacheDir, "issuer key cache directory")
root = flag.String("root", defaultRootPath, "server recording root")
debug = flag.Bool("debug", false, "enable debug messages")
)
flag.Parse()
@ -32,27 +34,39 @@ func main() {
logrus.SetLevel(logrus.DebugLevel)
}
ca, err := hostkey.NewCerticateSigner(*caKey)
if err != nil {
var (
issuer authority.Issuer
err error
)
if *keyCacheDir == "" {
issuer = authority.EphemeralKeyIssuer()
} else {
if issuer, err = authority.CachedKeyIssuer(*keyCacheDir); err != nil {
logrus.WithField("root", *keyCacheDir).Fatalln(err)
}
}
var ca *authority.CertificateAuthority
if ca, err = authority.NewCertificateAuthority(*caKey, issuer); err != nil {
logrus.WithError(err).Fatal("could not load CA key")
}
s, err := server.New(*root, *hostKey)
if err != nil {
var server *stronghold.Server
if server, err = stronghold.New(*root, *hostKey); err != nil {
logrus.Fatalln(err)
}
s.PublicKeyHandler = server.PermitAllPublicKeys
s.ProxyHostKeySigner = ca
s.SessionHandler = handleSession
server.PublicKeyHandler = stronghold.PermitAllPublicKeys
server.SessionHandler = handleSession
server.CertificateAuthority = ca
logrus.WithField("addr", *listen).Info("server starting")
if err = s.ListenAndServe(*listen); err != nil {
if err = server.ListenAndServe(*listen); err != nil {
logrus.Fatalln(err)
}
}
func handleSession(metadata ssh.ConnMetadata, channel ssh.Channel, pty *server.PTY, winch <-chan *server.WindowChange) error {
func handleSession(metadata ssh.ConnMetadata, channel ssh.Channel, pty *stronghold.PTY, winch <-chan *stronghold.WindowChange) error {
term := terminal.NewTerminal(channel, "stronghold% ")
_, _ = fmt.Fprintln(term, "Welcome to the Stronghold SSH proxy,", metadata.User())


server/crypto.go → crypto.go View File


server/io.go → io.go View File


server/proxy.go → proxy.go View File


server/server.go → server.go View File


+ 0
- 123
server/hostkey/signer.go View File

@ -1,123 +0,0 @@
package hostkey
import (
"crypto/rand"
"crypto/rsa"
"io"
"io/ioutil"
"net"
"time"
"github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh"
)
const (
defaultCertificateLifetime = 5 * time.Minute
defaultLifetimeSplay = 5 * time.Second
defaultHostKeyPoolSize = 4
)
// Signer for SSH host keys.
type Signer interface {
// HostKeys for the given address.
HostKeys(address string) ([]ssh.Signer, error)
}
type hostKeyPool struct {
keys chan ssh.Signer
}
func newHostKeyPool(size int) *hostKeyPool {
pool := &hostKeyPool{keys: make(chan ssh.Signer, size)}
go pool.fill()
return pool
}
func (pool *hostKeyPool) fill() {
log := logrus.WithField("tag", "hostkey_pool")
for {
log.Debug("generating RSA key")
k, err := rsa.GenerateKey(rand.Reader, 2048)
if err == nil {
if s, err := ssh.NewSignerFromKey(k); err == nil {
pool.keys <- s
}
}
}
}
func (pool *hostKeyPool) Key() ssh.Signer {
return <-pool.keys
}
type CertificateSigner struct {
ca ssh.Signer
lifetime time.Duration
pool *hostKeyPool
}
func NewCerticateSigner(caKeyFile string) (*CertificateSigner, error) {
b, err := ioutil.ReadFile(caKeyFile)
if err != nil {
return nil, err
}
ca, err := ssh.ParsePrivateKey(b)
if err != nil {
return nil, err
}
return &CertificateSigner{
ca: ca,
lifetime: defaultCertificateLifetime,
pool: newHostKeyPool(defaultHostKeyPoolSize),
}, nil
}
func (signer *CertificateSigner) HostKeys(address string) ([]ssh.Signer, error) {
host, _, err := net.SplitHostPort(address)
if err != nil {
return nil, err
}
ips, err := net.LookupIP(host)
if err != nil {
return nil, err
}
principals := []string{host}
for _, ip := range ips {
principals = append(principals, ip.String())
}
var (
key = signer.pool.Key()
now = time.Now()
validAfter = now.Add(-defaultLifetimeSplay)
validBefore = now.Add(defaultLifetimeSplay + signer.lifetime)
cert = &ssh.Certificate{
Nonce: nil,
Key: key.PublicKey(),
Serial: 0,
CertType: ssh.HostCert,
KeyId: host,
ValidPrincipals: principals,
ValidAfter: uint64(validAfter.Unix()),
ValidBefore: uint64(validBefore.Unix()),
}
)
cert.Nonce = make([]byte, 16)
_, _ = io.ReadFull(rand.Reader, cert.Nonce)
if err = cert.SignCert(rand.Reader, signer.ca); err != nil {
return nil, err
}
logrus.Printf("cert: %#+v", cert)
certSigner, err := ssh.NewCertSigner(cert, key)
if err != nil {
return nil, err
}
return []ssh.Signer{certSigner, key}, nil
}

Loading…
Cancel
Save