diff --git a/developer_tools/scripts/jwt_issuer/README.md b/developer_tools/scripts/jwt_issuer/README.md index b6afa1115..8652698be 100644 --- a/developer_tools/scripts/jwt_issuer/README.md +++ b/developer_tools/scripts/jwt_issuer/README.md @@ -33,6 +33,10 @@ make release-jwt-issuer BUMP=patch # or minor, major -jwt-out my-token.jwt \ -jwk-out my-jwks.json \ -priv-out my-private.jwk + +# Reuse an existing private key (avoids generating a new key each run) +./bin/jwt_issuer \ + -priv-in my-private.jwk ``` ## Flags @@ -49,6 +53,7 @@ make release-jwt-issuer BUMP=patch # or minor, major | `-kid` | `test-key-1` | Key ID in JWKS | | `-jwt-out` | `token.jwt` | Output file for the signed JWT | | `-jwk-out` | `jwks.json` | Output file for the JWKS | +| `-priv-in` | (none) | Input file for an existing private JWK (reuse key) | | `-priv-out` | (none) | Output file for the private JWK | ## Example apigw config diff --git a/developer_tools/scripts/jwt_issuer/main.go b/developer_tools/scripts/jwt_issuer/main.go index 3f3347b77..37502cb42 100644 --- a/developer_tools/scripts/jwt_issuer/main.go +++ b/developer_tools/scripts/jwt_issuer/main.go @@ -29,6 +29,7 @@ func main() { jwtFile := flag.String("jwt-out", "token.jwt", "output file for the signed JWT") jwkFile := flag.String("jwk-out", "jwks.json", "output file for the JWKS (public keys)") privFile := flag.String("priv-out", "", "output file for the private JWK (optional)") + privInFile := flag.String("priv-in", "", "input file for an existing private JWK (optional, generates new key if not set)") flag.Parse() @@ -41,17 +42,48 @@ func main() { *email = *subject } - // Generate ECDSA P-256 key pair - rawKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - fatal("generate key: %v", err) + // Guard against input/output file overlap + if *privInFile != "" { + for _, outFile := range []string{*jwtFile, *jwkFile, *privFile} { + if outFile != "" && outFile == *privInFile { + fatal("output file %q must not be the same as -priv-in to avoid overwriting the private key", outFile) + } + } } - // Build private JWK - privJWK, err := jwk.Import(rawKey) - if err != nil { - fatal("import private key: %v", err) + var privJWK jwk.Key + + if *privInFile != "" { + // Load existing private key from file + privJSON, err := os.ReadFile(*privInFile) + if err != nil { + fatal("read private key file: %v", err) + } + privJWK, err = jwk.ParseKey(privJSON) + if err != nil { + fatal("parse private key: %v", err) + } + // Validate the loaded key is an EC P-256 private key + var rawKey ecdsa.PrivateKey + if err := jwk.Export(privJWK, &rawKey); err != nil { + fatal("-priv-in must contain an EC private key (not a public key or other key type): %v", err) + } + if rawKey.Curve != elliptic.P256() { + fatal("-priv-in must be an EC P-256 key, got %s", rawKey.Curve.Params().Name) + } + fmt.Printf("Private key loaded from %s\n", *privInFile) + } else { + // Generate ECDSA P-256 key pair + rawKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + fatal("generate key: %v", err) + } + privJWK, err = jwk.Import(rawKey) + if err != nil { + fatal("import private key: %v", err) + } } + if err := privJWK.Set(jwk.KeyIDKey, *kid); err != nil { fatal("set kid: %v", err) }