Skip to content

Commit 68890b5

Browse files
authored
Port Models and upgrade to EdDSA Signing (#8)
* Port Models * EdDSA Signing and Database Models
1 parent 0129c71 commit 68890b5

30 files changed

+2871
-162
lines changed

cmd/bosun/main.go

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package main
2+
3+
import (
4+
"database/sql/driver"
5+
"fmt"
6+
"log"
7+
"os"
8+
"time"
9+
10+
"github.com/joho/godotenv"
11+
12+
"github.com/urfave/cli/v2"
13+
"go.rtnl.ai/quarterdeck/pkg"
14+
"go.rtnl.ai/quarterdeck/pkg/auth"
15+
"go.rtnl.ai/quarterdeck/pkg/auth/passwords"
16+
"go.rtnl.ai/ulid"
17+
"go.rtnl.ai/x/vero"
18+
)
19+
20+
func main() {
21+
// If a dotenv file exists, load it for configuration
22+
godotenv.Load()
23+
24+
// Create a multi-command CLI application
25+
app := cli.NewApp()
26+
app.Name = "bosun"
27+
app.Version = pkg.Version(false)
28+
app.Usage = "helpers for quarterdeck testing, debugging, and code generation"
29+
app.Flags = []cli.Flag{}
30+
app.Commands = []*cli.Command{
31+
{
32+
Name: "argon2",
33+
Usage: "create a derived key to use as a fixture for testing",
34+
Category: "testing",
35+
Action: derkey,
36+
ArgsUsage: "password [password ...]",
37+
Flags: []cli.Flag{},
38+
},
39+
{
40+
Name: "keypair",
41+
Usage: "create a fake apikey client ID and secret to use as a fixture for testing",
42+
Category: "testing",
43+
Action: keypair,
44+
Flags: []cli.Flag{},
45+
},
46+
{
47+
Name: "mkkey",
48+
Usage: "generate an RSA token key pair and kid (ulid) for JWT token signing",
49+
Category: "testing",
50+
Action: mkkey,
51+
Flags: []cli.Flag{
52+
&cli.StringFlag{
53+
Name: "out",
54+
Aliases: []string{"o"},
55+
Usage: "path to write keys out to (optional, will be saved as [kid].pem by default)",
56+
},
57+
&cli.IntFlag{
58+
Name: "size",
59+
Aliases: []string{"s"},
60+
Usage: "number of bits for the generated keys",
61+
Value: 4096,
62+
},
63+
},
64+
},
65+
{
66+
Name: "vero",
67+
Usage: "generate a vero token serialized as a database input for testing",
68+
Category: "testing",
69+
Action: veroToken,
70+
Flags: []cli.Flag{},
71+
},
72+
}
73+
74+
if err := app.Run(os.Args); err != nil {
75+
log.Fatal(err)
76+
}
77+
}
78+
79+
//===========================================================================
80+
// Commands
81+
//===========================================================================
82+
83+
func derkey(c *cli.Context) error {
84+
if c.NArg() == 0 {
85+
return cli.Exit("specify password(s) to create argon2 derived key(s) from", 1)
86+
}
87+
88+
for i := 0; i < c.NArg(); i++ {
89+
pwdk, err := passwords.CreateDerivedKey(c.Args().Get(i))
90+
if err != nil {
91+
return cli.Exit(err, 1)
92+
}
93+
fmt.Println(pwdk)
94+
}
95+
96+
return nil
97+
}
98+
99+
func keypair(c *cli.Context) error {
100+
clientID := passwords.ClientID()
101+
secret := passwords.ClientSecret()
102+
fmt.Printf("%s.%s\n", clientID, secret)
103+
return nil
104+
}
105+
106+
func mkkey(c *cli.Context) (err error) {
107+
// Create ULID and determine outpath
108+
keyid := ulid.Make()
109+
110+
var out string
111+
if out = c.String("out"); out == "" {
112+
out = fmt.Sprintf("%s.pem", keyid)
113+
}
114+
115+
// Generate Signing Key Pair using Signing Key algorithm currently in use.
116+
var keypair auth.SigningKey
117+
if keypair, err = auth.GenerateKeys(); err != nil {
118+
return cli.Exit(err, 1)
119+
}
120+
121+
// Save the keypair to the specified file in PEM format.
122+
if err = keypair.Dump(out); err != nil {
123+
return cli.Exit(err, 1)
124+
}
125+
126+
fmt.Printf("signing key id: %s -- saved with PEM encoding to %s\n", keyid, out)
127+
return nil
128+
}
129+
130+
func veroToken(c *cli.Context) (err error) {
131+
resourceID := ulid.MakeSecure()
132+
expiration := time.Now().Add(87600 * time.Hour)
133+
134+
var token *vero.Token
135+
if token, err = vero.New(resourceID[:], expiration); err != nil {
136+
return cli.Exit(err, 1)
137+
}
138+
139+
var signature *vero.SignedToken
140+
if _, signature, err = token.Sign(); err != nil {
141+
return cli.Exit(err, 1)
142+
}
143+
144+
var value driver.Value
145+
if value, err = signature.Value(); err != nil {
146+
return cli.Exit(err, 1)
147+
}
148+
149+
fmt.Println(value)
150+
return nil
151+
}

cmd/quarterdeck/main.go

Lines changed: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
package main
22

33
import (
4-
"crypto/rand"
5-
"crypto/rsa"
6-
"crypto/x509"
7-
"encoding/pem"
84
"fmt"
95
"log"
106
"os"
@@ -14,6 +10,7 @@ import (
1410
confire "github.com/rotationalio/confire/usage"
1511
"github.com/urfave/cli/v2"
1612
"go.rtnl.ai/quarterdeck/pkg"
13+
"go.rtnl.ai/quarterdeck/pkg/auth"
1714
"go.rtnl.ai/quarterdeck/pkg/config"
1815
"go.rtnl.ai/quarterdeck/pkg/server"
1916
"go.rtnl.ai/ulid"
@@ -53,15 +50,15 @@ func main() {
5350
},
5451
},
5552
{
56-
Name: "tokenkey",
57-
Usage: "generate an RSA token key pair and ksuid for JWT token signing",
53+
Name: "mkkey",
54+
Usage: "generate an RSA token key pair and kid (ulid) for JWT token signing",
5855
Category: "utility",
59-
Action: generateTokenKey,
56+
Action: mkkey,
6057
Flags: []cli.Flag{
6158
&cli.StringFlag{
6259
Name: "out",
6360
Aliases: []string{"o"},
64-
Usage: "path to write keys out to (optional, will be saved as ulid.pem by default)",
61+
Usage: "path to write keys out to (optional, will be saved as [kid].pem by default)",
6562
},
6663
&cli.IntFlag{
6764
Name: "size",
@@ -117,7 +114,7 @@ func usage(c *cli.Context) (err error) {
117114
return nil
118115
}
119116

120-
func generateTokenKey(c *cli.Context) (err error) {
117+
func mkkey(c *cli.Context) (err error) {
121118
// Create ULID and determine outpath
122119
keyid := ulid.Make()
123120

@@ -126,25 +123,17 @@ func generateTokenKey(c *cli.Context) (err error) {
126123
out = fmt.Sprintf("%s.pem", keyid)
127124
}
128125

129-
// Generate RSA keys using crypto random
130-
var key *rsa.PrivateKey
131-
if key, err = rsa.GenerateKey(rand.Reader, c.Int("size")); err != nil {
126+
// Generate Signing Key Pair using Signing Key algorithm currently in use.
127+
var keypair auth.SigningKey
128+
if keypair, err = auth.GenerateKeys(); err != nil {
132129
return cli.Exit(err, 1)
133130
}
134131

135-
// Open file to PEM encode keys to
136-
var f *os.File
137-
if f, err = os.OpenFile(out, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600); err != nil {
132+
// Save the keypair to the specified file in PEM format.
133+
if err = keypair.Dump(out); err != nil {
138134
return cli.Exit(err, 1)
139135
}
140136

141-
if err = pem.Encode(f, &pem.Block{
142-
Type: "RSA PRIVATE KEY",
143-
Bytes: x509.MarshalPKCS1PrivateKey(key),
144-
}); err != nil {
145-
return cli.Exit(err, 1)
146-
}
147-
148-
fmt.Printf("RSA key id: %s -- saved with PEM encoding to %s\n", keyid, out)
137+
fmt.Printf("signing key id: %s -- saved with PEM encoding to %s\n", keyid, out)
149138
return nil
150139
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ require (
1515
github.com/urfave/cli/v2 v2.27.7
1616
go.rtnl.ai/ulid v1.1.1
1717
go.rtnl.ai/x v1.4.0
18+
golang.org/x/crypto v0.39.0
1819
golang.org/x/text v0.26.0
1920
)
2021

@@ -52,7 +53,6 @@ require (
5253
github.com/ugorji/go/codec v1.3.0 // indirect
5354
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
5455
golang.org/x/arch v0.18.0 // indirect
55-
golang.org/x/crypto v0.39.0 // indirect
5656
golang.org/x/net v0.41.0 // indirect
5757
golang.org/x/sys v0.33.0 // indirect
5858
google.golang.org/protobuf v1.36.6 // indirect

pkg/auth/issuer.go

Lines changed: 20 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
package auth
22

33
import (
4+
"crypto"
45
"crypto/rand"
5-
"crypto/rsa"
66
"fmt"
77
"net/url"
8-
"os"
98
"sync"
109
"time"
1110

@@ -18,7 +17,7 @@ import (
1817

1918
// Global variables that should not be changed except between major versions.
2019
var (
21-
signingMethod = jwt.SigningMethodRS384
20+
signingMethod = jwt.SigningMethodEdDSA
2221
refreshPath = "/v1/reauthenticate"
2322
entropy = ulid.Monotonic(rand.Reader, 1000)
2423
entropyMu sync.Mutex
@@ -27,8 +26,8 @@ var (
2726
type ClaimsIssuer struct {
2827
conf config.AuthConfig
2928
keyID ulid.ULID
30-
key *rsa.PrivateKey
31-
publicKeys map[ulid.ULID]*rsa.PublicKey
29+
key crypto.PrivateKey
30+
publicKeys map[ulid.ULID]crypto.PublicKey
3231
refreshAudience string
3332
}
3433

@@ -40,42 +39,39 @@ func NewIssuer(conf config.AuthConfig) (_ *ClaimsIssuer, err error) {
4039

4140
issuer := &ClaimsIssuer{
4241
conf: conf,
43-
publicKeys: make(map[ulid.ULID]*rsa.PublicKey, len(conf.Keys)),
42+
publicKeys: make(map[ulid.ULID]crypto.PublicKey, len(conf.Keys)),
4443
}
4544

46-
// Load the specified keys from the filesystem
47-
// TODO: support loading keys from a vault or other secure storage.
45+
// Load the specified keys from the filesystem.
4846
for kid, path := range conf.Keys {
4947
var keyID ulid.ULID
5048
if keyID, err = ulid.Parse(kid); err != nil {
5149
return nil, errors.Fmt("could not parse %s as a key id: %w", kid, err)
5250
}
5351

54-
var data []byte
55-
if data, err = os.ReadFile(path); err != nil {
56-
return nil, errors.Fmt("could not read %s: %w", path, err)
57-
}
58-
59-
var key *rsa.PrivateKey
60-
if key, err = jwt.ParseRSAPrivateKeyFromPEM(data); err != nil {
61-
return nil, errors.Fmt("could not parse private key %s: %w", path, err)
52+
keypair := &keys{}
53+
if err = keypair.Load(path); err != nil {
54+
return nil, err
6255
}
6356

64-
issuer.publicKeys[keyID] = &key.PublicKey
57+
issuer.publicKeys[keyID] = keypair.PublicKey()
6558
if issuer.key == nil || keyID.Time() > issuer.keyID.Time() {
66-
issuer.key = key
59+
issuer.key = keypair.PrivateKey()
6760
issuer.keyID = keyID
6861
}
6962
}
7063

7164
// If we have no keys, generate one for use (e.g. for testing or simple deployment)
7265
if issuer.key == nil {
73-
if issuer.key, err = rsa.GenerateKey(rand.Reader, 4096); err != nil {
66+
var keypair SigningKey
67+
if keypair, err = GenerateKeys(); err != nil {
7468
return nil, err
7569
}
7670

7771
issuer.keyID = ulid.MustNew(ulid.Now(), entropy)
78-
issuer.publicKeys[issuer.keyID] = &issuer.key.PublicKey
72+
issuer.key = keypair.PrivateKey()
73+
issuer.publicKeys[issuer.keyID] = keypair.PublicKey()
74+
7975
log.Warn().Str("keyID", issuer.keyID.String()).Msg("generated volatile claims issuer rsa key")
8076
}
8177

@@ -189,7 +185,7 @@ func (tm *ClaimsIssuer) CreateTokens(claims *Claims) (signedAccessToken, signedR
189185
}
190186

191187
// Keys returns the map of ulid to public key for use externally.
192-
func (tm *ClaimsIssuer) Keys() map[ulid.ULID]*rsa.PublicKey {
188+
func (tm *ClaimsIssuer) Keys() map[ulid.ULID]crypto.PublicKey {
193189
return tm.publicKeys
194190
}
195191

@@ -210,13 +206,13 @@ func (tm *ClaimsIssuer) RefreshAudience() string {
210206
return tm.refreshAudience
211207
}
212208

213-
// keyFunc is an jwt.KeyFunc that selects the RSA public key from the list of managed
209+
// keyFunc is an jwt.KeyFunc that selects the public key from the list of managed
214210
// internal keys based on the kid in the token header. If the kid does not exist an
215211
// error is returned and the token will not be able to be verified.
216212
func (tm *ClaimsIssuer) keyFunc(token *jwt.Token) (key interface{}, err error) {
217213
// Per JWT security notice: do not forget to validate alg is expected
218-
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
219-
return nil, errors.Fmt("unexpected signing method: %v", token.Header["alg"])
214+
if token.Method.Alg() != signingMethod.Alg() {
215+
return nil, errors.Fmt("unexpected signing method: %v", token.Method.Alg())
220216
}
221217

222218
// Fetch the kid from the header

0 commit comments

Comments
 (0)