aboutsummaryrefslogtreecommitdiff
path: root/server/signer/signer.go
blob: 2a8fc98a7a7ed933b60b4aec9788a421b70c2ce7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
package signer

import (
	"crypto/rand"
	"fmt"
	"log"
	"strings"
	"time"

	"go4.org/wkfs"
	_ "go4.org/wkfs/gcs" // Register "/gcs/" as a wkfs.

	"github.com/golang/protobuf/ptypes"
	"github.com/nsheridan/cashier/lib"
	"github.com/nsheridan/cashier/proto"
	"github.com/nsheridan/cashier/server/config"
	"github.com/nsheridan/cashier/server/store"
	"github.com/stripe/krl"
	"golang.org/x/crypto/ssh"
)

var (
	defaultPermissions = map[string]string{
		"permit-X11-forwarding":   "",
		"permit-agent-forwarding": "",
		"permit-port-forwarding":  "",
		"permit-pty":              "",
		"permit-user-rc":          "",
	}
)

// KeySigner does the work of signing a ssh public key with the CA key.
type KeySigner struct {
	ca          ssh.Signer
	validity    time.Duration
	principals  []string
	permissions []string
}

func (s *KeySigner) setPermissions(cert *ssh.Certificate) {
	cert.CriticalOptions = make(map[string]string)
	cert.Extensions = make(map[string]string)
	for _, perm := range s.permissions {
		if strings.Contains(perm, "=") {
			opt := strings.Split(perm, "=")
			cert.CriticalOptions[strings.TrimSpace(opt[0])] = strings.TrimSpace(opt[1])
		} else {
			cert.Extensions[perm] = ""
		}
	}
	if len(cert.Extensions) == 0 {
		cert.Extensions = defaultPermissions
	}
}

// SignUserKeyFromRPC returns a signed ssh certificate.
func (s *KeySigner) SignUserKeyFromRPC(req *proto.SignRequest, username string) (*ssh.Certificate, error) {
	valid, err := ptypes.Timestamp(req.GetValidUntil())
	if err != nil {
		return nil, err
	}
	r := &lib.SignRequest{
		Key:        string(req.GetKey()),
		ValidUntil: valid,
	}
	return s.SignUserKey(r, username)
}

// SignUserKey returns a signed ssh certificate.
func (s *KeySigner) SignUserKey(req *lib.SignRequest, username string) (*ssh.Certificate, error) {
	pubkey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(req.Key))
	if err != nil {
		return nil, err
	}
	expires := time.Now().UTC().Add(s.validity)
	if req.ValidUntil.After(expires) {
		req.ValidUntil = expires
	}
	cert := &ssh.Certificate{
		CertType:        ssh.UserCert,
		Key:             pubkey,
		KeyId:           fmt.Sprintf("%s_%d", username, time.Now().UTC().Unix()),
		ValidAfter:      uint64(time.Now().UTC().Add(-5 * time.Minute).Unix()),
		ValidBefore:     uint64(req.ValidUntil.Unix()),
		ValidPrincipals: []string{username},
	}
	cert.ValidPrincipals = append(cert.ValidPrincipals, s.principals...)
	s.setPermissions(cert)
	if err := cert.SignCert(rand.Reader, s.ca); err != nil {
		return nil, err
	}
	log.Printf("Issued cert id: %s principals: %s fp: %s valid until: %s\n", cert.KeyId, cert.ValidPrincipals, ssh.FingerprintSHA256(pubkey), time.Unix(int64(cert.ValidBefore), 0).UTC())
	return cert, nil
}

// GenerateRevocationList returns an SSH key revocation list (KRL).
func (s *KeySigner) GenerateRevocationList(certs []*store.CertRecord) ([]byte, error) {
	revoked := &krl.KRLCertificateSection{
		CA: s.ca.PublicKey(),
	}
	ids := krl.KRLCertificateKeyID{}
	for _, c := range certs {
		ids = append(ids, c.KeyID)
	}
	revoked.Sections = append(revoked.Sections, &ids)
	k := &krl.KRL{
		Sections: []krl.KRLSection{revoked},
	}
	return k.Marshal(rand.Reader)
}

// New creates a new KeySigner from the supplied configuration.
func New(conf *config.SSH) (*KeySigner, error) {
	data, err := wkfs.ReadFile(conf.SigningKey)
	if err != nil {
		return nil, fmt.Errorf("unable to read CA key %s: %v", conf.SigningKey, err)
	}
	key, err := ssh.ParsePrivateKey(data)
	if err != nil {
		return nil, fmt.Errorf("unable to parse CA key: %v", err)
	}
	validity, err := time.ParseDuration(conf.MaxAge)
	if err != nil {
		return nil, fmt.Errorf("error parsing duration '%s': %v", conf.MaxAge, err)
	}
	return &KeySigner{
		ca:          key,
		validity:    validity,
		principals:  conf.AdditionalPrincipals,
		permissions: conf.Permissions,
	}, nil
}