aboutsummaryrefslogtreecommitdiff
path: root/app/models/session_key.go
diff options
context:
space:
mode:
Diffstat (limited to 'app/models/session_key.go')
-rw-r--r--app/models/session_key.go202
1 files changed, 202 insertions, 0 deletions
diff --git a/app/models/session_key.go b/app/models/session_key.go
new file mode 100644
index 0000000..64ac7e0
--- /dev/null
+++ b/app/models/session_key.go
@@ -0,0 +1,202 @@
1package models
2
3import (
4 "crypto"
5 "crypto/ecdsa"
6 "crypto/elliptic"
7 "crypto/rand"
8 "crypto/x509"
9 "encoding/base64"
10 "encoding/hex"
11 "fmt"
12 "time"
13
14 "go.mongodb.org/mongo-driver/bson"
15)
16
17// SessionKey represents a public and sometimes private key-pair for a user
18// that will be stored on the user's record in the user store. These keys are
19// used for signing authentication JWTs.
20//
21// This object is designed to be serialized to BSON. Other serializations can
22// be added in the future as needed.
23//
24// There are two flavors of this record. A record with a private key (which
25// implies a public key) is a key that the service generated and is used by the
26// service to sign JWTs for the user. The private key is never given to the
27// user. The private key is only used in the CreateToken flow, never the Verify
28// flow. Currently (as of Nov 2021) the application sets a near-future NotAfter
29// date and these get garbage collected. It might be nice to re-use them in the
30// future for a while but it's not all that important.
31//
32// The other flavor of this key will have a public key but no private key.
33// These are service keys. Service keys are given to programmatic actors that
34// need to be able to mint their own JWTs for authentication to the service.
35// For these keys the client will construct their own JWT and sign it with the
36// private key and the service will validate the signature with the public key.
37// These keys (as of Nov 2021) do not expire, though they can be revoked.
38type SessionKey struct {
39 KeyId string
40 Description string
41 Revoked *time.Time
42 NotAfter *time.Time
43 NotBefore *time.Time
44 PublicKey crypto.PublicKey
45 PrivateKey *ecdsa.PrivateKey
46}
47
48func GenerateSessionKey(ttl time.Duration) (*SessionKey, error) {
49 pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
50 if err != nil {
51 return nil, err
52 }
53
54 key := make([]byte, 8)
55 if _, err := rand.Read(key); err != nil {
56 return nil, err
57 }
58
59 now := time.Now()
60 notAfter := now.Add(ttl)
61
62 return &SessionKey{
63 KeyId: hex.EncodeToString(key),
64 Revoked: nil,
65 NotAfter: &notAfter,
66 NotBefore: &now,
67 PublicKey: pk.Public(),
68 PrivateKey: pk,
69 }, nil
70}
71
72// IsGarbage checks to determine if a key is garbage that should be collected.
73// The definition of garbage is similar to the inversion of the definition of
74// vaild but revoked keys are not considered to be garbage since they may be
75// useful for auditing later. Also keys that are not yet valid are not garbage.
76func (s *SessionKey) IsGarbage() bool {
77 if s.Revoked != nil {
78 return false
79 }
80
81 if s.NotBefore != nil && s.NotBefore.Before(time.Now()) {
82 return false
83 }
84
85 if s.NotAfter != nil && s.NotAfter.After(time.Now()) {
86 return true
87 }
88
89 return false
90}
91
92// IsValid checks the various dates in the SessionKey to verify that they are
93// valid and in-range for use. This should be called before trusting this key
94// for any use.
95func (s *SessionKey) IsValid() bool {
96 if s.Revoked != nil {
97 return false
98 }
99
100 if s.NotBefore != nil && s.NotBefore.Before(time.Now()) {
101 return false
102 }
103
104 if s.NotAfter != nil && s.NotAfter.After(time.Now()) {
105 return false
106 }
107
108 return true
109}
110
111func (s *SessionKey) MarshalBSON() ([]byte, error) {
112 var err error
113 var pub, priv []byte
114
115 if s.PrivateKey != nil {
116 priv, err = x509.MarshalECPrivateKey(s.PrivateKey)
117 if err != nil {
118 return nil, err
119 }
120 }
121
122 // If there's a private key and a public key set then just save the private
123 // key. The private key already contains a copy of the public key.
124 if s.PublicKey != nil && s.PrivateKey == nil {
125 pub, err = x509.MarshalPKIXPublicKey(s.PublicKey)
126 if err != nil {
127 return nil, err
128 }
129 }
130
131 return bson.Marshal(struct {
132 KeyId string
133 Revoked *time.Time
134 NotAfter *time.Time
135 NotBefore *time.Time
136 PublicKey string
137 PrivateKey string
138 }{
139 s.KeyId,
140 s.Revoked, s.NotAfter, s.NotBefore,
141 base64.StdEncoding.EncodeToString(pub),
142 base64.StdEncoding.EncodeToString(priv),
143 })
144}
145
146func (s *SessionKey) UnmarshalBSON(d []byte) error {
147 v := struct {
148 KeyId string
149 Revoked *time.Time
150 NotAfter *time.Time
151 NotBefore *time.Time
152 PublicKey string
153 PrivateKey string
154 }{}
155 if err := bson.Unmarshal(d, &v); err != nil {
156 return err
157 }
158
159 s.KeyId = v.KeyId
160 s.Revoked = v.Revoked
161 s.NotAfter = v.NotAfter
162 s.NotBefore = v.NotBefore
163
164 if v.PrivateKey != "" {
165 privb, err := base64.StdEncoding.DecodeString(v.PrivateKey)
166 if err != nil {
167 return err
168 }
169
170 priv, err := x509.ParseECPrivateKey(privb)
171 if err != nil {
172 return err
173 }
174
175 s.PrivateKey = priv
176 s.PublicKey = priv.Public()
177 }
178
179 // If there was a private key then the public key was already set by
180 // decoding that private key. No need to do this a second time (also it's
181 // rather unlikely that both would be set).
182 if v.PublicKey != "" && s.PublicKey == nil {
183 pubb, err := base64.StdEncoding.DecodeString(v.PublicKey)
184 if err != nil {
185 return err
186 }
187
188 pubp, err := x509.ParsePKIXPublicKey(pubb)
189 if err != nil {
190 return err
191 }
192
193 pub, ok := pubp.(*ecdsa.PublicKey)
194 if !ok {
195 return fmt.Errorf("Failed to convert public key to *ecdsa.PublicKey")
196 }
197
198 s.PublicKey = pub
199 }
200
201 return nil
202}