Skip to content

Commit e55a4bd

Browse files
claudehulto
authored andcommitted
Consolidate cryptographic keys: use ed25519 master key for both JWT and ECDH
This commit refactors the key management to use a single ed25519 master key for both JWT signing and ECDH key exchange: Changes: - Created keyservice package to manage the master ed25519 key - Implemented ed25519 to x25519 key derivation following RFC 7748 - Private key: hash ed25519 seed and apply Curve25519 clamping - Public key: convert Edwards curve point to Montgomery curve - Updated JWT service to accept keys instead of managing its own - Updated app.go to use keyservice for both JWT and cryptocodec - Removed duplicate key generation functions (generateKeyPair, getKeyPair, etc.) - Removed separate storage keys: - Old: tavern_jwt_ed25519_private_key (JWT only) - Old: tavern_encryption_private_key (ECDH only) - New: tavern_master_ed25519_key (both JWT and derived ECDH) Benefits: - Single source of truth for cryptographic keys - Ed25519 master key enables both signing (JWT) and ECDH (x25519) - Simpler key management with one persistent key - Maintains backward compatibility with existing x25519 ECDH protocol
1 parent f3d6147 commit e55a4bd

File tree

3 files changed

+202
-162
lines changed

3 files changed

+202
-162
lines changed

tavern/app.go

Lines changed: 15 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,6 @@ package main
22

33
import (
44
"context"
5-
"crypto/ecdh"
6-
"crypto/ed25519"
7-
"crypto/rand"
8-
"crypto/x509"
95
"encoding/base64"
106
"fmt"
117
"log"
@@ -35,10 +31,10 @@ import (
3531
tavernhttp "realm.pub/tavern/internal/http"
3632
"realm.pub/tavern/internal/http/stream"
3733
"realm.pub/tavern/internal/jwt"
34+
"realm.pub/tavern/internal/keyservice"
3835
"realm.pub/tavern/internal/portals"
3936
"realm.pub/tavern/internal/portals/mux"
4037
"realm.pub/tavern/internal/redirectors"
41-
"realm.pub/tavern/internal/secrets"
4238
"realm.pub/tavern/internal/www"
4339
"realm.pub/tavern/portals/portalpb"
4440
"realm.pub/tavern/tomes"
@@ -423,85 +419,6 @@ func newGraphQLHandler(client *ent.Client, repoImporter graphql.RepoImporter) ht
423419
})
424420
}
425421

426-
func generateKeyPair() (*ecdh.PublicKey, *ecdh.PrivateKey, error) {
427-
curve := ecdh.X25519()
428-
privateKey, err := curve.GenerateKey(rand.Reader)
429-
if err != nil {
430-
slog.Error(fmt.Sprintf("failed to generate private key: %v\n", err))
431-
return nil, nil, err
432-
}
433-
publicKey, err := curve.NewPublicKey(privateKey.PublicKey().Bytes())
434-
if err != nil {
435-
slog.Error(fmt.Sprintf("failed to generate public key: %v\n", err))
436-
return nil, nil, err
437-
}
438-
439-
return publicKey, privateKey, nil
440-
}
441-
442-
func GetPubKey() (*ecdh.PublicKey, error) {
443-
pub, _, err := getKeyPair()
444-
if err != nil {
445-
return nil, err
446-
}
447-
return pub, nil
448-
}
449-
450-
func newSecretsManager() (secrets.SecretsManager, error) {
451-
if EnvGCPProjectID.String() == "" && EnvSecretsManagerPath.String() == "" {
452-
slog.Error("No configuration provided for secret manager path, using a potentially insecure default.")
453-
return secrets.NewDebugFileSecrets("/tmp/tavern-secrets")
454-
}
455-
if EnvSecretsManagerPath.String() == "" {
456-
return secrets.NewGcp(EnvGCPProjectID.String())
457-
}
458-
459-
return secrets.NewDebugFileSecrets(EnvSecretsManagerPath.String())
460-
}
461-
462-
func getKeyPair() (*ecdh.PublicKey, *ecdh.PrivateKey, error) {
463-
curve := ecdh.X25519()
464-
465-
secretsManager, err := newSecretsManager()
466-
if err != nil || secretsManager == nil {
467-
return nil, nil, fmt.Errorf("failed to configure secret manager: %w", err)
468-
}
469-
470-
// Check if we already have a key
471-
privateKeyString, err := secretsManager.GetValue("tavern_encryption_private_key")
472-
if err != nil {
473-
// Generate a new one if it doesn't exist
474-
pubKey, privateKey, err := generateKeyPair()
475-
if err != nil {
476-
return nil, nil, fmt.Errorf("key generation failed: %v", err)
477-
}
478-
479-
privateKeyBytes, err := x509.MarshalPKCS8PrivateKey(privateKey)
480-
if err != nil {
481-
return nil, nil, fmt.Errorf("unable to marshal private key: %v", err)
482-
}
483-
_, err = secretsManager.SetValue("tavern_encryption_private_key", privateKeyBytes)
484-
if err != nil {
485-
return nil, nil, fmt.Errorf("unable to set 'tavern_encryption_private_key' using secrets manager: %v", err)
486-
}
487-
return pubKey, privateKey, nil
488-
}
489-
490-
// Parse private key bytes
491-
tmp, err := x509.ParsePKCS8PrivateKey(privateKeyString)
492-
if err != nil {
493-
return nil, nil, fmt.Errorf("unable to parse private key: %v", err)
494-
}
495-
privateKey := tmp.(*ecdh.PrivateKey)
496-
497-
publicKey, err := curve.NewPublicKey(privateKey.PublicKey().Bytes())
498-
if err != nil {
499-
return nil, nil, fmt.Errorf("failed to generate public key: %v", err)
500-
}
501-
502-
return publicKey, privateKey, nil
503-
}
504-
505422
func newPortalGRPCHandler(graph *ent.Client, portalMux *mux.Mux) http.Handler {
506423
portalSrv := portals.New(graph, portalMux)
507424
grpcSrv := grpc.NewServer(
@@ -525,22 +442,31 @@ func newPortalGRPCHandler(graph *ent.Client, portalMux *mux.Mux) http.Handler {
525442
}
526443

527444
func newGRPCHandler(client *ent.Client, grpcShellMux *stream.Mux, portalMux *mux.Mux) http.Handler {
528-
pub, priv, err := getKeyPair()
445+
// Initialize key service (manages master ed25519 key and derives x25519 key)
446+
keyService, err := keyservice.NewKeyService()
529447
if err != nil {
448+
slog.Error("failed to initialize key service", "err", err)
530449
panic(err)
531450
}
532-
slog.Info(fmt.Sprintf("public key: %s", base64.StdEncoding.EncodeToString(pub.Bytes())))
533451

534-
// Initialize JWT service
535-
jwtService, err := jwt.NewService()
452+
// Get derived x25519 keys for encryption
453+
x25519Pub := keyService.GetX25519PublicKey()
454+
x25519Priv := keyService.GetX25519PrivateKey()
455+
slog.Info(fmt.Sprintf("x25519 public key (derived from ed25519): %s", base64.StdEncoding.EncodeToString(x25519Pub.Bytes())))
456+
457+
// Initialize JWT service with ed25519 keys
458+
jwtService, err := jwt.NewService(
459+
keyService.GetEd25519PrivateKey(),
460+
keyService.GetEd25519PublicKey(),
461+
)
536462
if err != nil {
537463
slog.Error("failed to initialize JWT service", "err", err)
538464
panic(err)
539465
}
540466

541467
c2srv := c2.New(client, grpcShellMux, portalMux, jwtService)
542468
xchacha := cryptocodec.StreamDecryptCodec{
543-
Csvc: cryptocodec.NewCryptoSvc(priv),
469+
Csvc: cryptocodec.NewCryptoSvc(x25519Priv),
544470
}
545471
grpcSrv := grpc.NewServer(
546472
grpc.ForceServerCodecV2(xchacha),

tavern/internal/jwt/jwt.go

Lines changed: 4 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,10 @@ package jwt
22

33
import (
44
"crypto/ed25519"
5-
"crypto/rand"
6-
"crypto/x509"
75
"fmt"
8-
"log/slog"
9-
"os"
106
"time"
117

128
"github.com/golang-jwt/jwt"
13-
"realm.pub/tavern/internal/secrets"
149
)
1510

1611
// Service provides JWT generation and validation using ed25519 keys
@@ -26,56 +21,12 @@ type TaskClaims struct {
2621
jwt.StandardClaims
2722
}
2823

29-
// NewService creates a new JWT service with persistent ed25519 keys
30-
func NewService() (*Service, error) {
31-
secretsManager, err := newSecretsManager()
32-
if err != nil || secretsManager == nil {
33-
return nil, fmt.Errorf("failed to configure secret manager: %w", err)
24+
// NewService creates a new JWT service with the provided ed25519 keys
25+
func NewService(privateKey ed25519.PrivateKey, publicKey ed25519.PublicKey) (*Service, error) {
26+
if privateKey == nil || publicKey == nil {
27+
return nil, fmt.Errorf("private key and public key must not be nil")
3428
}
3529

36-
// Check if we already have a key
37-
privateKeyBytes, err := secretsManager.GetValue("tavern_jwt_ed25519_private_key")
38-
if err != nil {
39-
// Generate a new key pair if it doesn't exist
40-
pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
41-
if err != nil {
42-
return nil, fmt.Errorf("failed to generate ed25519 keypair: %w", err)
43-
}
44-
45-
// Marshal and store the private key
46-
privateKeyBytes, err := x509.MarshalPKCS8PrivateKey(privKey)
47-
if err != nil {
48-
return nil, fmt.Errorf("unable to marshal private key: %w", err)
49-
}
50-
51-
_, err = secretsManager.SetValue("tavern_jwt_ed25519_private_key", privateKeyBytes)
52-
if err != nil {
53-
return nil, fmt.Errorf("unable to set 'tavern_jwt_ed25519_private_key' using secrets manager: %w", err)
54-
}
55-
56-
slog.Info("Generated new ed25519 keypair for JWT signing")
57-
58-
return &Service{
59-
privateKey: privKey,
60-
publicKey: pubKey,
61-
}, nil
62-
}
63-
64-
// Parse existing private key
65-
tmp, err := x509.ParsePKCS8PrivateKey(privateKeyBytes)
66-
if err != nil {
67-
return nil, fmt.Errorf("unable to parse private key: %w", err)
68-
}
69-
70-
privateKey, ok := tmp.(ed25519.PrivateKey)
71-
if !ok {
72-
return nil, fmt.Errorf("expected ed25519.PrivateKey, got %T", tmp)
73-
}
74-
75-
publicKey := privateKey.Public().(ed25519.PublicKey)
76-
77-
slog.Info("Loaded existing ed25519 keypair for JWT signing")
78-
7930
return &Service{
8031
privateKey: privateKey,
8132
publicKey: publicKey,
@@ -129,23 +80,3 @@ func (s *Service) ValidateTaskToken(tokenString string) (*TaskClaims, error) {
12980
func (s *Service) GetPublicKey() ed25519.PublicKey {
13081
return s.publicKey
13182
}
132-
133-
// newSecretsManager creates a secrets manager instance
134-
// This function is intentionally package-private and duplicates logic from app.go
135-
// to avoid circular dependencies
136-
func newSecretsManager() (secrets.SecretsManager, error) {
137-
// Read environment variables directly to avoid circular imports
138-
gcpProjectID := os.Getenv("GCP_PROJECT_ID")
139-
secretsPath := os.Getenv("SECRETS_FILE_PATH")
140-
141-
if gcpProjectID == "" && secretsPath == "" {
142-
slog.Warn("No configuration provided for secret manager path, using a potentially insecure default.")
143-
return secrets.NewDebugFileSecrets("/tmp/tavern-secrets")
144-
}
145-
146-
if secretsPath == "" {
147-
return secrets.NewGcp(gcpProjectID)
148-
}
149-
150-
return secrets.NewDebugFileSecrets(secretsPath)
151-
}

0 commit comments

Comments
 (0)