Skip to content

Commit

Permalink
Always output keysets for both public and private keys
Browse files Browse the repository at this point in the history
  • Loading branch information
moskyb committed Oct 9, 2023
1 parent 8cd4fc8 commit a930017
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 58 deletions.
64 changes: 36 additions & 28 deletions clicommand/tool_keygen.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,16 @@ import (
)

type KeygenConfig struct {
Alg string `cli:"alg" validate:"required"`
KeyID string `cli:"key-id" validate:"required"`
PrivateKeyFilename string `cli:"private-key-filename" normalize:"filepath"`
PublicKeysetFilename string `cli:"public-keyset-filename" normalize:"filepath"`

LogLevel string `cli:"log-level"`
Debug bool `cli:"debug"`
Alg string `cli:"alg" validate:"required"`
KeyID string `cli:"key-id" validate:"required"`
PrivateKeySetFilename string `cli:"private-keyset-filename" normalize:"filepath"`
PublicKeysetFilename string `cli:"public-keyset-filename" normalize:"filepath"`

NoColor bool `cli:"no-color"`
Debug bool `cli:"debug"`
LogLevel string `cli:"log-level"`
Experiments []string `cli:"experiment"`
Profile string `cli:"profile"`
}

var KeygenCommand = cli.Command{
Expand All @@ -37,7 +40,7 @@ var KeygenCommand = cli.Command{
Usage: "The ID to use for the keys generated",
},
cli.StringFlag{
Name: "private-key-filename",
Name: "private-keyset-filename",
EnvVar: "BUILDKITE_AGENT_KEYGEN_PRIVATE_KEY_FILENAME",
Usage: "The filename to write the private key to. Defaults to a name based on the key id in the current directory",
},
Expand All @@ -47,12 +50,15 @@ var KeygenCommand = cli.Command{
Usage: "The filename to write the public keyset to. Defaults to a name based on the key id in the current directory",
},

// Global flags
NoColorFlag,
DebugFlag,
LogLevelFlag,
ExperimentsFlag,
ProfileFlag,
},
Action: func(c *cli.Context) {
ctx := context.Background()
_, cfg, l, _, done := setupLoggerAndConfig[KeygenConfig](ctx, c)
_, cfg, l, _, done := setupLoggerAndConfig[KeygenConfig](context.Background(), c)
defer done()

sigAlg := jwa.SignatureAlgorithm(cfg.Alg)
Expand All @@ -66,46 +72,48 @@ var KeygenCommand = cli.Command{
l.Fatal("Failed to generate key pair: %v", err)
}

if cfg.PrivateKeyFilename == "" {
cfg.PrivateKeyFilename = fmt.Sprintf("./%s-%s-private.json", cfg.Alg, cfg.KeyID)
if cfg.PrivateKeySetFilename == "" {
cfg.PrivateKeySetFilename = fmt.Sprintf("./%s-%s-private.json", cfg.Alg, cfg.KeyID)
}

if cfg.PublicKeysetFilename == "" {
cfg.PublicKeysetFilename = fmt.Sprintf("./%s-%s-public.json", cfg.Alg, cfg.KeyID)
}

privFile, err := os.Create(cfg.PrivateKeyFilename)
l.Info("Writing private key set to %s...", cfg.PrivateKeySetFilename)
pKey, err := json.Marshal(priv)
if err != nil {
l.Fatal("Failed to open private key file: %v", err)
l.Fatal("Failed to marshal private key: %v", err)
}

defer privFile.Close()

err = json.NewEncoder(privFile).Encode(priv)
err = writeIfNotExists(cfg.PrivateKeySetFilename, pKey)
if err != nil {
l.Fatal("Failed to encode private key file: %v", err)
l.Fatal("Failed to write private key file: %v", err)
}

l.Info("Wrote private key to %s", cfg.PrivateKeyFilename)

pubFile, err := os.Create(cfg.PublicKeysetFilename)
l.Info("Writing public key set to %s...", cfg.PublicKeysetFilename)
pubKey, err := json.Marshal(pub)
if err != nil {
l.Fatal("Failed to open public key file: %v", err)
l.Fatal("Failed to marshal private key: %v", err)
}

defer pubFile.Close()

err = json.NewEncoder(pubFile).Encode(pub)
err = writeIfNotExists(cfg.PublicKeysetFilename, pubKey)
if err != nil {
l.Fatal("Failed to encode public key file: %v", err)
l.Fatal("Failed to write private key file: %v", err)
}

l.Info("Wrote public key set to %s", cfg.PublicKeysetFilename)

l.Info("Done! Enjoy your new keys ^_^")

if slices.Contains(ValidOctetAlgorithms, sigAlg) {
l.Info(`Note: Because you're using the %s algorithm, which is symmetric, the public and private keys are identical, save for the fact that the "public" key has been output as a Key Set, rather than a single key.`, sigAlg)
}
},
}

func writeIfNotExists(filename string, data []byte) error {
if _, err := os.Stat(filename); err == nil {
return fmt.Errorf("file %s already exists", filename)
}

return os.WriteFile(filename, data, 0o600)
}
29 changes: 17 additions & 12 deletions internal/jwkutil/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (

const symmetricKeyLength = 2048

func NewKeyPair(keyID string, alg jwa.SignatureAlgorithm) (jwk.Key, jwk.Set, error) {
func NewKeyPair(keyID string, alg jwa.SignatureAlgorithm) (jwk.Set, jwk.Set, error) {
switch alg {
case jwa.HS256, jwa.HS384, jwa.HS512:
key := make([]byte, symmetricKeyLength)
Expand Down Expand Up @@ -53,11 +53,11 @@ func NewKeyPair(keyID string, alg jwa.SignatureAlgorithm) (jwk.Key, jwk.Set, err
}
}

func NewSymmetricKeyPairFromString(id, key string, alg jwa.SignatureAlgorithm) (jwk.Key, jwk.Set, error) {
func NewSymmetricKeyPairFromString(id, key string, alg jwa.SignatureAlgorithm) (jwk.Set, jwk.Set, error) {
return newSymmetricKeyPair(id, []byte(key), alg)
}

func newSymmetricKeyPair(id string, key []byte, alg jwa.SignatureAlgorithm) (jwk.Key, jwk.Set, error) {
func newSymmetricKeyPair(id string, key []byte, alg jwa.SignatureAlgorithm) (jwk.Set, jwk.Set, error) {
skey, err := jwk.FromRaw(key)
if err != nil {
return nil, nil, fmt.Errorf("failed to create symmetric key: %s", err)
Expand All @@ -77,10 +77,10 @@ func newSymmetricKeyPair(id string, key []byte, alg jwa.SignatureAlgorithm) (jwk
return nil, nil, fmt.Errorf("failed to add key to set: %s", err)
}

return skey, set, err
return set, set, err
}

func newRSAKeyPair(id string, alg jwa.SignatureAlgorithm) (jwk.Key, jwk.Set, error) {
func newRSAKeyPair(id string, alg jwa.SignatureAlgorithm) (jwk.Set, jwk.Set, error) {
priv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, nil, fmt.Errorf("failed to generate RSA private key: %s", err)
Expand All @@ -89,7 +89,7 @@ func newRSAKeyPair(id string, alg jwa.SignatureAlgorithm) (jwk.Key, jwk.Set, err
return newKeyPair(id, alg, priv)
}

func newECKeyPair(id string, alg jwa.SignatureAlgorithm, crv elliptic.Curve) (jwk.Key, jwk.Set, error) {
func newECKeyPair(id string, alg jwa.SignatureAlgorithm, crv elliptic.Curve) (jwk.Set, jwk.Set, error) {

priv, err := ecdsa.GenerateKey(crv, rand.Reader)
if err != nil {
Expand All @@ -99,7 +99,7 @@ func newECKeyPair(id string, alg jwa.SignatureAlgorithm, crv elliptic.Curve) (jw
return newKeyPair(id, alg, priv)
}

func newEdwardsKeyPair(id string, alg jwa.SignatureAlgorithm) (jwk.Key, jwk.Set, error) {
func newEdwardsKeyPair(id string, alg jwa.SignatureAlgorithm) (jwk.Set, jwk.Set, error) {
_, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return nil, nil, fmt.Errorf("failed to generate Edwards private key: %s", err)
Expand All @@ -108,7 +108,7 @@ func newEdwardsKeyPair(id string, alg jwa.SignatureAlgorithm) (jwk.Key, jwk.Set,
return newKeyPair(id, alg, priv)
}

func newKeyPair(id string, alg jwa.SignatureAlgorithm, privKey any) (jwk.Key, jwk.Set, error) {
func newKeyPair(id string, alg jwa.SignatureAlgorithm, privKey any) (jwk.Set, jwk.Set, error) {
privJWK, err := jwk.FromRaw(privKey)
if err != nil {
return nil, nil, fmt.Errorf("jwk.FromRaw(%v) error = %v", privKey, err)
Expand All @@ -128,12 +128,17 @@ func newKeyPair(id string, alg jwa.SignatureAlgorithm, privKey any) (jwk.Key, jw
return nil, nil, fmt.Errorf("jwk.PublicKeyOf(%v) error = %v", privJWK, err)
}

set := jwk.NewSet()
if err := set.AddKey(pubJWK); err != nil {
return nil, nil, fmt.Errorf("failed to add key to set: %s", err)
pubSet := jwk.NewSet()
if err := pubSet.AddKey(pubJWK); err != nil {
return nil, nil, fmt.Errorf("failed to add public key to set: %s", err)
}

privSet := jwk.NewSet()
if err := privSet.AddKey(privJWK); err != nil {
return nil, nil, fmt.Errorf("failed to add private key to set: %s", err)
}

return privJWK, set, nil
return privSet, pubSet, nil
}

func setAll(key jwk.Key, values map[string]interface{}) error {
Expand Down
69 changes: 51 additions & 18 deletions internal/pipeline/sign_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,67 +45,67 @@ func TestSignVerify(t *testing.T) {

cases := []struct {
name string
generateSigner func(alg jwa.SignatureAlgorithm) (jwk.Key, jwk.Set, error)
generateSigner func(alg jwa.SignatureAlgorithm) (jwk.Set, jwk.Set, error)
alg jwa.SignatureAlgorithm
expectedDeterministicSignature string
}{
{
name: "HMAC-SHA256",
generateSigner: func(alg jwa.SignatureAlgorithm) (jwk.Key, jwk.Set, error) {
generateSigner: func(alg jwa.SignatureAlgorithm) (jwk.Set, jwk.Set, error) {
return jwkutil.NewSymmetricKeyPairFromString(keyID, "alpacas", alg)
},
alg: jwa.HS256,
expectedDeterministicSignature: "eyJhbGciOiJIUzI1NiIsImtpZCI6ImNoYXJ0cmV1c2UifQ..SHbGJSyZadUIr8M591h_63VS-o0hwZ0n63YBfLfFxzo",
},
{
name: "HMAC-SHA384",
generateSigner: func(alg jwa.SignatureAlgorithm) (jwk.Key, jwk.Set, error) {
generateSigner: func(alg jwa.SignatureAlgorithm) (jwk.Set, jwk.Set, error) {
return jwkutil.NewSymmetricKeyPairFromString(keyID, "alpacas", alg)
},
alg: jwa.HS384,
expectedDeterministicSignature: "eyJhbGciOiJIUzM4NCIsImtpZCI6ImNoYXJ0cmV1c2UifQ..i1cy6E6JfYtoHYmYxJXObV4zr3UD3fPTRLvhu9oi9nq3Shz2eSmLGkdqH8lkL9gQ",
},
{
name: "HMAC-SHA512",
generateSigner: func(alg jwa.SignatureAlgorithm) (jwk.Key, jwk.Set, error) {
generateSigner: func(alg jwa.SignatureAlgorithm) (jwk.Set, jwk.Set, error) {
return jwkutil.NewSymmetricKeyPairFromString(keyID, "alpacas", alg)
},
alg: jwa.HS512,
expectedDeterministicSignature: "eyJhbGciOiJIUzUxMiIsImtpZCI6ImNoYXJ0cmV1c2UifQ..QzsnwhNotMQHSHozrJfkohrpYa9usXPoGQGFUjNoD8kJBWa7zsRMEePo4MnP89P0kMfKOBds3HKR3xMc7X7ZyA",
},
{
name: "RSA-PSS 256",
generateSigner: func(alg jwa.SignatureAlgorithm) (jwk.Key, jwk.Set, error) { return jwkutil.NewKeyPair(keyID, alg) },
generateSigner: func(alg jwa.SignatureAlgorithm) (jwk.Set, jwk.Set, error) { return jwkutil.NewKeyPair(keyID, alg) },
alg: jwa.PS256,
},
{
name: "RSA-PSS 384",
generateSigner: func(alg jwa.SignatureAlgorithm) (jwk.Key, jwk.Set, error) { return jwkutil.NewKeyPair(keyID, alg) },
generateSigner: func(alg jwa.SignatureAlgorithm) (jwk.Set, jwk.Set, error) { return jwkutil.NewKeyPair(keyID, alg) },
alg: jwa.PS384,
},
{
name: "RSA-PSS 512",
generateSigner: func(alg jwa.SignatureAlgorithm) (jwk.Key, jwk.Set, error) { return jwkutil.NewKeyPair(keyID, alg) },
generateSigner: func(alg jwa.SignatureAlgorithm) (jwk.Set, jwk.Set, error) { return jwkutil.NewKeyPair(keyID, alg) },
alg: jwa.PS512,
},
{
name: "ECDSA P-256",
generateSigner: func(alg jwa.SignatureAlgorithm) (jwk.Key, jwk.Set, error) { return jwkutil.NewKeyPair(keyID, alg) },
generateSigner: func(alg jwa.SignatureAlgorithm) (jwk.Set, jwk.Set, error) { return jwkutil.NewKeyPair(keyID, alg) },
alg: jwa.ES256,
},
{
name: "ECDSA P-384",
generateSigner: func(alg jwa.SignatureAlgorithm) (jwk.Key, jwk.Set, error) { return jwkutil.NewKeyPair(keyID, alg) },
generateSigner: func(alg jwa.SignatureAlgorithm) (jwk.Set, jwk.Set, error) { return jwkutil.NewKeyPair(keyID, alg) },
alg: jwa.ES384,
},
{
name: "ECDSA P-512",
generateSigner: func(alg jwa.SignatureAlgorithm) (jwk.Key, jwk.Set, error) { return jwkutil.NewKeyPair(keyID, alg) },
generateSigner: func(alg jwa.SignatureAlgorithm) (jwk.Set, jwk.Set, error) { return jwkutil.NewKeyPair(keyID, alg) },
alg: jwa.ES512,
},
{
name: "EdDSA Ed25519",
generateSigner: func(alg jwa.SignatureAlgorithm) (jwk.Key, jwk.Set, error) { return jwkutil.NewKeyPair(keyID, alg) },
generateSigner: func(alg jwa.SignatureAlgorithm) (jwk.Set, jwk.Set, error) { return jwkutil.NewKeyPair(keyID, alg) },
alg: jwa.EdDSA,
},
}
Expand All @@ -119,7 +119,12 @@ func TestSignVerify(t *testing.T) {
t.Fatalf("generateSigner(%v) error = %v", tc.alg, err)
}

sig, err := Sign(signEnv, step, signer)
key, ok := signer.Key(0)
if !ok {
t.Fatalf("signer.Key(0) = _, false, want true")
}

sig, err := Sign(signEnv, step, key)
if err != nil {
t.Fatalf("Sign(CommandStep, signer) error = %v", err)
}
Expand Down Expand Up @@ -190,8 +195,13 @@ func TestSignConcatenatedFields(t *testing.T) {
t.Fatalf("NewSymmetricKeyPairFromString(alpacas) error = %v", err)
}

key, ok := signer.Key(0)
if !ok {
t.Fatalf("signer.Key(0) = _, false, want true")
}

for _, m := range maps {
sig, err := Sign(nil, m, signer)
sig, err := Sign(nil, m, key)
if err != nil {
t.Fatalf("Sign(%v, pts) error = %v", m, err)
}
Expand All @@ -216,9 +226,14 @@ func TestUnknownAlgorithm(t *testing.T) {
t.Fatalf("NewSymmetricKeyPairFromString(alpacas) error = %v", err)
}

signer.Set(jwk.AlgorithmKey, "rot13")
key, ok := signer.Key(0)
if !ok {
t.Fatalf("signer.Key(0) = _, false, want true")
}

key.Set(jwk.AlgorithmKey, "rot13")

if _, err := Sign(nil, &CommandStep{Command: "llamas"}, signer); err == nil {
if _, err := Sign(nil, &CommandStep{Command: "llamas"}, key); err == nil {
t.Errorf("Sign(nil, CommandStep, signer) = %v, want non-nil error", err)
}
}
Expand Down Expand Up @@ -256,7 +271,12 @@ func TestSignUnknownStep(t *testing.T) {
t.Fatalf("NewSymmetricKeyPairFromString(alpacas) error = %v", err)
}

if err := steps.sign(nil, signer); !errors.Is(err, errSigningRefusedUnknownStepType) {
key, ok := signer.Key(0)
if !ok {
t.Fatalf("signer.Key(0) = _, false, want true")
}

if err := steps.sign(nil, key); !errors.Is(err, errSigningRefusedUnknownStepType) {
t.Errorf("steps.sign(signer) = %v, want %v", err, errSigningRefusedUnknownStepType)
}
}
Expand Down Expand Up @@ -325,8 +345,16 @@ func TestSignVerifyEnv(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
signer, verifier, err := jwkutil.NewSymmetricKeyPairFromString(keyID, "alpacas", jwa.HS256)
if err != nil {
t.Fatalf("NewSymmetricKeyPairFromString(alpacas) error = %v", err)
}

sig, err := Sign(tc.pipelineEnv, tc.step, signer)
key, ok := signer.Key(0)
if !ok {
t.Fatalf("signer.Key(0) = _, false, want true")
}

sig, err := Sign(tc.pipelineEnv, tc.step, key)
if err != nil {
t.Fatalf("Sign(CommandStep, signer) error = %v", err)
}
Expand Down Expand Up @@ -372,7 +400,12 @@ func TestSignatureStability(t *testing.T) {
t.Fatalf("NewKeyPair error = %v", err)
}

sig, err := Sign(env, step, signer)
key, ok := signer.Key(0)
if !ok {
t.Fatalf("signer.Key(0) = _, false, want true")
}

sig, err := Sign(env, step, key)
if err != nil {
t.Fatalf("Sign(env, CommandStep, signer) error = %v", err)
}
Expand Down

0 comments on commit a930017

Please sign in to comment.