diff options
Diffstat (limited to 'app/models/session_key.go')
-rw-r--r-- | app/models/session_key.go | 202 |
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 @@ | |||
1 | package models | ||
2 | |||
3 | import ( | ||
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. | ||
38 | type 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 | |||
48 | func 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: ¬After, | ||
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. | ||
76 | func (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. | ||
95 | func (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 | |||
111 | func (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 | |||
146 | func (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 | } | ||