aboutsummaryrefslogtreecommitdiff
path: root/app/models/session_key.go
blob: b75d6c4338ab3e6542601e49ed50be032bc7cf0a (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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
package models

import (
	"crypto"
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/x509"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"time"

	"go.mongodb.org/mongo-driver/bson"
)

// SessionKey represents a public and sometimes private key-pair for a user
// that will be stored on the user's record in the user store. These keys are
// used for signing authentication JWTs.
//
// This object is designed to be serialized to and from BSON and JSON. Other
// serializations can be added in the future as needed.
//
// The ExposePrivateKeysInJSON controls how JSON serialization of this struct
// works. When the field is set to false (the default) then serialization into
// JSON will never encode a private key, but may encode a public key. If this
// is set to true then the private key will be encoded into the JSON value and
// not the public key. SETTING THIS TO TRUE AND EXPOSING THE RESULTS TO THE
// USER IS A SECURITY ERROR so this should normally not be changed. This value
// of this field will never be persisted in any form.
//
// There are two flavors of this record. A record with a private key (which
// implies a public key) is a key that the service generated and is used by the
// service to sign JWTs for the user. The private key is never given to the
// user. The private key is only used in the CreateToken flow, never the Verify
// flow. Currently (as of Nov 2021) the application sets a near-future NotAfter
// date and these get garbage collected. It might be nice to re-use them in the
// future for a while but it's not all that important.
//
// The other flavor of this key will have a public key but no private key.
// These are service keys. Service keys are given to programmatic actors that
// need to be able to mint their own JWTs for authentication to the service.
// For these keys the client will construct their own JWT and sign it with the
// private key and the service will validate the signature with the public key.
// These keys (as of Nov 2021) do not expire, though they can be revoked.
type SessionKey struct {
	KeyId                   string
	Description             string
	Revoked                 *time.Time
	NotAfter                *time.Time
	NotBefore               *time.Time
	PublicKey               crypto.PublicKey
	PrivateKey              *ecdsa.PrivateKey
	ExposePrivateKeysInJSON bool `json:"-" bson:"-"`
}

func GenerateSessionKey(ttl time.Duration) (*SessionKey, error) {
	pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	if err != nil {
		return nil, err
	}

	key := make([]byte, 8)
	if _, err := rand.Read(key); err != nil {
		return nil, err
	}

	now := time.Now()
	notAfter := now.Add(ttl)

	return &SessionKey{
		KeyId:      hex.EncodeToString(key),
		Revoked:    nil,
		NotAfter:   &notAfter,
		NotBefore:  &now,
		PublicKey:  pk.Public(),
		PrivateKey: pk,
	}, nil
}

// IsGarbage checks to determine if a key is garbage that should be collected.
// The definition of garbage is similar to the inversion of the definition of
// vaild but revoked keys are not considered to be garbage since they may be
// useful for auditing later. Also keys that are not yet valid are not garbage.
func (s *SessionKey) IsGarbage() bool {
	if s.Revoked != nil {
		return false
	}

	if s.NotBefore != nil && time.Now().Before(*s.NotBefore) {
		return false
	}

	if s.NotAfter != nil && time.Now().After(*s.NotAfter) {
		return true
	}

	return false
}

// IsValid checks the various dates in the SessionKey to verify that they are
// valid and in-range for use. This should be called before trusting this key
// for any use.
func (s *SessionKey) IsValid() bool {
	if s.Revoked != nil {
		return false
	}

	if s.NotBefore != nil && time.Now().Before(*s.NotBefore) {
		return false
	}

	if s.NotAfter != nil && time.Now().After(*s.NotAfter) {
		return false
	}

	return true
}

// MarshalPrivateKey marshals the private key to a X509 encoded base64 string
func (s *SessionKey) MarshalPrivateKey() (string, error) {
	priv, err := x509.MarshalECPrivateKey(s.PrivateKey)
	if err != nil {
		return "", err
	}
	return base64.StdEncoding.EncodeToString(priv), nil
}

// MarshalPublicKey marshals the public key to an X509 encoded base64 string
func (s *SessionKey) MarshalPublicKey() (string, error) {
	pub, err := x509.MarshalPKIXPublicKey(s.PublicKey)
	if err != nil {
		return "", err
	}
	return base64.StdEncoding.EncodeToString(pub), nil
}

// UnmarshalPrivateKey unmarshals the private key from a base64 encoded X509
// string into the public and private key fields.
func (s *SessionKey) UnmarshalPrivateKey(k string) error {
	privb, err := base64.StdEncoding.DecodeString(k)
	if err != nil {
		return err
	}

	priv, err := x509.ParseECPrivateKey(privb)
	if err != nil {
		return err
	}

	s.PrivateKey = priv
	s.PublicKey = priv.Public()

	return nil
}

// UnmarshalPublicKey unmarshals the public key from a base64 encoded X509
// string into the public key field.
func (s *SessionKey) UnmarshalPublicKey(k string) error {
	pubb, err := base64.StdEncoding.DecodeString(k)
	if err != nil {
		return err
	}

	pubp, err := x509.ParsePKIXPublicKey(pubb)
	if err != nil {
		return err
	}

	pub, ok := pubp.(*ecdsa.PublicKey)
	if !ok {
		return fmt.Errorf("Failed to convert public key to *ecdsa.PublicKey")
	}

	s.PublicKey = pub
	return nil
}

// MarshalJSON marshals a struct to JSON
//
// This method will have different behavior if the ExposePrivateKeysInJSON
// field is set in the struct (the default is false). If this field is set to
// true the private keys will be exposed in the JSON results. If it is false
// then private keys will not be exposed. The ExposePrivateKeysInJSON itself
// will never be serialized.
func (s *SessionKey) MarshalJSON() ([]byte, error) {
	var err error
	var privKey *string
	var pub, priv string

	if s.PrivateKey != nil && s.ExposePrivateKeysInJSON {
		priv, err = s.MarshalPrivateKey()
		if err != nil {
			return nil, err
		}
		if priv != "" {
			privKey = &priv
		}
	}

	// If there's a private key and a public key set, and exposing the private
	// key is allowed, then just save the private key. The private key already
	// contains a copy of the public key.
	if s.PublicKey != nil && (s.PrivateKey == nil || !s.ExposePrivateKeysInJSON) {
		pub, err = s.MarshalPublicKey()
		if err != nil {
			return nil, err
		}
	}

	return json.Marshal(struct {
		KeyId      string     `json:"key_id"`
		Revoked    *time.Time `json:"revoked,omitempty"`
		NotAfter   *time.Time `json:"not_after"`
		NotBefore  *time.Time `json:"not_before"`
		PublicKey  string     `json:"public_key"`
		PrivateKey *string    `json:"private_key,omitempty"`
	}{
		s.KeyId,
		s.Revoked, s.NotAfter, s.NotBefore,
		pub, privKey,
	})
}

// UnmarshalJSON unmarshals a struct from JSON.
//
// This method does attempt to unmarshal private keys.
func (s *SessionKey) UnmarshalJSON(d []byte) error {
	v := struct {
		KeyId      string     `json:"key_id"`
		Revoked    *time.Time `json:"revoked,omitempty"`
		NotAfter   *time.Time `json:"not_after"`
		NotBefore  *time.Time `json:"not_before"`
		PublicKey  string     `json:"public_key"`
		PrivateKey string     `json:"private_key"`
	}{}
	if err := json.Unmarshal(d, &v); err != nil {
		return err
	}

	s.KeyId = v.KeyId
	s.Revoked = v.Revoked
	s.NotAfter = v.NotAfter
	s.NotBefore = v.NotBefore

	if v.PrivateKey != "" {
		if err := s.UnmarshalPrivateKey(v.PrivateKey); err != nil {
			return err
		}
	}

	// If there was a private key then the public key was already set by
	// decoding that private key. No need to do this a second time (also it's
	// rather unlikely that both would be set).
	if v.PublicKey != "" && s.PublicKey == nil {
		if err := s.UnmarshalPublicKey(v.PublicKey); err != nil {
			return err
		}
	}

	return nil
}

func (s *SessionKey) MarshalBSON() ([]byte, error) {
	var err error
	var pub, priv string

	if s.PrivateKey != nil {
		priv, err = s.MarshalPrivateKey()
		if err != nil {
			return nil, err
		}
	}

	// If there's a private key and a public key set then just save the private
	// key. The private key already contains a copy of the public key.
	if s.PublicKey != nil && s.PrivateKey == nil {
		pub, err = s.MarshalPublicKey()
		if err != nil {
			return nil, err
		}
	}

	return bson.Marshal(struct {
		KeyId      string
		Revoked    *time.Time
		NotAfter   *time.Time
		NotBefore  *time.Time
		PublicKey  string
		PrivateKey string
	}{
		s.KeyId,
		s.Revoked, s.NotAfter, s.NotBefore,
		pub, priv,
	})
}

func (s *SessionKey) UnmarshalBSON(d []byte) error {
	v := struct {
		KeyId      string
		Revoked    *time.Time
		NotAfter   *time.Time
		NotBefore  *time.Time
		PublicKey  string
		PrivateKey string
	}{}
	if err := bson.Unmarshal(d, &v); err != nil {
		return err
	}

	s.KeyId = v.KeyId
	s.Revoked = v.Revoked
	s.NotAfter = v.NotAfter
	s.NotBefore = v.NotBefore

	if v.PrivateKey != "" {
		if err := s.UnmarshalPrivateKey(v.PrivateKey); err != nil {
			return err
		}
	}

	// If there was a private key then the public key was already set by
	// decoding that private key. No need to do this a second time (also it's
	// rather unlikely that both would be set).
	if v.PublicKey != "" && s.PublicKey == nil {
		if err := s.UnmarshalPublicKey(v.PublicKey); err != nil {
			return err
		}
	}

	return nil
}