diff --git a/README.md b/README.md index 39bc995..5fa42e0 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,11 @@ HTTPS Wrench, a wrench not to bench

-**HTTPS Wrench** is a CLI program to make Yaml defined HTTPS requests and to -inspect x.509 certificates and keys.\ +**HTTPS Wrench** is a tool for maintainers of secure HTTP endpoints. +It enables executing YAML-defined HTTPS requests, inspecting x.509 certificates, private keys, JSON Web Tokens (JWT), and +generating JSON Web Key Sets (JWKS).\ **HTTPS Wrench** was born from the desire of a disposable Bash script to become -a reliable tool for mechanics of the World Wide Web.\ +a reliable companion for mechanics of the World Wide Web.\ `https-wrench` will, one day, take the place of `curl` in the hearts and the eyes of whoever is about to migrate a DNS record from a webserver to a load balancer, reverse proxy, Ingress Gateway, CloudFront distribution. @@ -26,30 +27,37 @@ Check the help: ```plain ❯ https-wrench -h -HTTPS Wrench is a tool to make HTTPS requests according to a Yaml configuration file and to inspect x.509 certificates and keys. +HTTPS Wrench is a tool for maintainers of secure HTTP endpoints. +It enables executing YAML-defined HTTPS requests and performing in-depth +inspection of x.509 certificates, private keys, and JSON Web Tokens. -https-wrench has two subcommands: requests and certinfo. +https-wrench provides several specialized subcommands: -requests is the subcommand that does HTTPS requests according to the configuration provided -by the --config flag. +requests: Execute HTTPS requests according to a structured YAML configuration, +supporting custom CA bundles and verbose output. -certinfo is a subcommand that reads information from PEM encoded x.509 certificates and keys. The certificates -can be read from local files or TLS enabled endpoints. +certinfo: Inspect PEM-encoded certificates and keys from local files or remote +TLS endpoints. Verify certificate chains and key pairings. -certinfo can compare public keys extracted from certificates and private keys to check if they match. +jwtinfo: Decode, inspect, and validate JSON Web Tokens (JWT) using local files +or remote JWKS endpoints. -HTTPS Wrench is distributed with an open source license and available at the following address: -https://github.com/xenOs76/https-wrench +jwks: Generate pretty-printed JSON Web Key Sets (JWKS) from public keys for +exposure on well-known endpoints. + +Distributed under an open-source license: https://github.com/xenOs76/https-wrench Usage: https-wrench [flags] https-wrench [command] Available Commands: - certinfo Shows information about x.509 certificates and keys + certinfo Inspect and verify x.509 certificates and keys completion Generate the autocompletion script for the specified shell help Help about any command - requests Make HTTPS requests defined in the YAML configuration file + jwks Generate a JSON Web Key Set (JWKS) from a public key + jwtinfo Inspect and validate JSON Web Tokens (JWT) + requests Execute YAML-defined HTTPS requests Flags: --config string config file (default is $HOME/.https-wrench.yaml) @@ -71,15 +79,15 @@ Get the help: ```plain ❯ https-wrench requests -h -https-wrench requests is the subcommand that does HTTPS requests according to the configuration +https-wrench requests is the subcommand that does HTTPS requests according to the configuration pointed by the --config flag. A sample configuration can be generated as a starting point (--show-sample-config). -The Github repository has more configuration examples: +The Github repository has more configuration examples: https://github.com/xenOs76/https-wrench/tree/main/assets/examples -It also provides a JSON schema that can be used to validate new configuration files: +It also provides a JSON schema that can be used to validate new configuration files: https://github.com/xenOs76/https-wrench/blob/main/https-wrench.schema.json Examples: @@ -90,7 +98,7 @@ Usage: https-wrench requests [flags] Flags: - --ca-bundle string Path to bundle file with CA certificates + --ca-bundle string Path to bundle file with CA certificates to use for validation -h, --help help for requests --show-sample-config Show a sample YAML configuration @@ -131,16 +139,16 @@ Get the help: ```plain ❯ https-wrench certinfo -h -HTTPS Wrench certinfo: shows information about PEM certificates and keys. +Inspect and verify PEM encoded x.509 certificates and keys. -https-wrench certinfo can fetch certificates from a TLS endpoint, read from a PEM bundle file, and check if a +https-wrench certinfo can fetch certificates from a TLS endpoint, read from a PEM bundle file, and check if a private key matches any of the certificates. -The certificates can be verified against the system root CAs or a custom CA bundle file. +The certificates can be verified against the system root CAs or a custom CA bundle file. The validation can be skipped. -If the private key is password protected, the password can be provided via the CERTINFO_PKEY_PW +If the private key is password protected, the password can be provided via the CERTINFO_PKEY_PW environment variable or will be prompted on stdin. Examples: @@ -159,13 +167,13 @@ Usage: https-wrench certinfo [flags] Flags: - --ca-bundle string Path to bundle file with CA certificates + --ca-bundle string Path to bundle file with CA certificates to use for validation --cert-bundle string Path to PEM Certificate bundle file -h, --help help for certinfo --key-file string Path to PEM Key file - --tls-endpoint string TLS enabled endpoint exposing certificates to fetch. - Forms: 'host:port', '[host]:port'. + --tls-endpoint string TLS enabled endpoint exposing certificates to fetch. + Forms: 'host:port', '[host]:port'. IPv6 addresses must be enclosed in square brackets, as in '[::1]:80' --tls-insecure Skip certificate validation when connecting to a TLS endpoint --tls-servername string ServerName to use when connecting to an SNI enabled TLS endpoint @@ -197,10 +205,106 @@ been used to generate the certificate: ❯ https-wrench certinfo --tls-endpoint localhost:9443 --ca-bundle rootCA.pem --key-file key.pem ``` +### HTTPS Wrench jwtinfo + +`jwtinfo` allows you to decode and inspect the claims of a JSON Web Token. It can also validate the token signature if a JWKS endpoint is provided. + +
+View Jwtinfo Help (`https-wrench jwtinfo -h`) + +```plain +❯ https-wrench jwtinfo -h + +Inspect and validate JSON Web Tokens (JWT) from files or remote providers. + +Examples: + export REQ_URL="https://sample.provider/oauth/token" + export REQ_VALUES="{\"login\":\"values\"}" + export VALIDATION_URL="https://url.to/jwks.json" + + # Read a JWT token from a local file + https-wrench jwtinfo --token-file /var/run/secrets/kubernetes.io/serviceaccount/token + + # Request a JWT token using inline values + https-wrench jwtinfo --request-url $REQ_URL --request-values-json $REQ_VALUES + + # Request a JWT token using values file + https-wrench jwtinfo --request-url $REQ_URL --request-values-file request-values.json + + # Request and validate a JWT token + https-wrench jwtinfo --request-url $REQ_URL --request-values-json $REQ_VALUES --validation-url $VALIDATION_URL + +Usage: + https-wrench jwtinfo [flags] + +Flags: + -h, --help help for jwtinfo + --request-url string HTTP address to use for the JWT token request + --request-values-file string File containing the JSON encoded values to use for the JWT token request + --request-values-json string JSON encoded values to use for the JWT token request + --token-file string File containing the JWT token + --validation-url string Url of the JSON Web Key Set (JWKS) to use for validating the JWT token + +Global Flags: + --config string config file (default is $HOME/.https-wrench.yaml) + --version Display the version +``` + +
+ +Decode a token from a file: + +```shell +❯ https-wrench jwtinfo --token-file mytoken.jwt +``` + +### HTTPS Wrench jwks + +`jwks` generates a public JSON Web Key Set from a PEM-encoded public key. This is useful for exposing your public keys at a `.well-known/jwks.json` endpoint. + +
+View Jwks Help (`https-wrench jwks -h`) + +```plain +❯ https-wrench jwks -h + +Generate a pretty-printed JSON Web Key Set (JWKS) from a public key file. + +The generated JWKS contains only public key parameters and is safe +to be exposed (e.g. at a /.well-known/jwks.json endpoint). + +Examples: + # Generate a public JWKS from an RSA public key + https-wrench jwks --public-key-file rsa-public.pem + + # Generate a public JWKS with a custom Key ID (kid) + https-wrench jwks --public-key-file ec-public.pem --kid "my-custom-key-id" + +Usage: + https-wrench jwks [flags] + +Flags: + -h, --help help for jwks + --kid string Optional explicit Key ID (kid) to use. If not provided, a SHA-256-derived ID is generated. + --public-key-file string File containing the PEM-encoded public key + +Global Flags: + --config string config file (default is $HOME/.https-wrench.yaml) + --version Display the version +``` + +
+ +Generate a JWKS with a SHA-256-derived KID: + +```shell +❯ https-wrench jwks --public-key-file public.pem +``` + ### Sample output
-HTTPS Wrench requests, (long) sample configuration output +HTTPS Wrench requests, sample configuration output HTTPS Wrench requests - sample config output
@@ -219,6 +323,16 @@ been used to generate the certificate: HTTPS Wrench certinfo - TLS Endpoint +
+HTTPS Wrench jwtinfo, request token +HTTPS Wrench jwtinfo - Request Token +
+ +
+HTTPS Wrench jwtinfo, read token and validate +HTTPS Wrench jwtinfo - Read Token +
+ ## How to install
diff --git a/assets/img/https-wrench_jwtinfo_read_validate_token.png b/assets/img/https-wrench_jwtinfo_read_validate_token.png new file mode 100644 index 0000000..80eb27f Binary files /dev/null and b/assets/img/https-wrench_jwtinfo_read_validate_token.png differ diff --git a/assets/img/https-wrench_jwtinfo_request_token.png b/assets/img/https-wrench_jwtinfo_request_token.png new file mode 100644 index 0000000..ff416d0 Binary files /dev/null and b/assets/img/https-wrench_jwtinfo_request_token.png differ diff --git a/cmd/certinfo.go b/cmd/certinfo.go index e5f8497..03cac8c 100644 --- a/cmd/certinfo.go +++ b/cmd/certinfo.go @@ -19,9 +19,8 @@ var ( var certinfoCmd = &cobra.Command{ Use: "certinfo", - Short: "Shows information about x.509 certificates and keys", - Long: ` -HTTPS Wrench certinfo: shows information about PEM encoded x.509 certificates and keys. + Short: "Inspect and verify x.509 certificates and keys", + Long: `Inspect and verify PEM encoded x.509 certificates and keys. https-wrench certinfo can fetch certificates from a TLS endpoint, read from a PEM bundle file, and check if a private key matches any of the certificates. diff --git a/cmd/jwks.go b/cmd/jwks.go new file mode 100644 index 0000000..07c99b4 --- /dev/null +++ b/cmd/jwks.go @@ -0,0 +1,67 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" + "github.com/xenos76/https-wrench/internal/jwks" + "github.com/xenos76/https-wrench/internal/style" +) + +var ( + jwksPublicKeyFile string + jwksKID string +) + +var jwksCmd = &cobra.Command{ + Use: "jwks", + Short: "Generate a JSON Web Key Set (JWKS) from a public key", + Long: `Generate a pretty-printed JSON Web Key Set (JWKS) from a public key file. + +The generated JWKS contains only public key parameters and is safe +to be exposed (e.g. at a /.well-known/jwks.json endpoint). + +Examples: + # Generate a public JWKS from an RSA public key + https-wrench jwks --public-key-file rsa-public.pem + + # Generate a public JWKS with a custom Key ID (kid) + https-wrench jwks --public-key-file ec-public.pem --kid "my-custom-key-id" +`, + Run: func(cmd *cobra.Command, _ []string) { + jwksJSON, err := jwks.GenerateJWKS(cmd.Context(), jwksPublicKeyFile, jwksKID) + if err != nil { + cmd.PrintErrf("Error generating JWKS: %s\n", err) + + return + } + + // Print a nice title and then the formatted JSON + w := cmd.OutOrStdout() + fmt.Fprintln(w) + fmt.Fprintln(w, style.LgSprintf(style.Cmd, "Jwks")) + fmt.Fprintln(w) + + fmt.Fprint(w, style.CodeSyntaxHighlight("json", jwksJSON)) + fmt.Fprintln(w) + }, +} + +func init() { + rootCmd.AddCommand(jwksCmd) + + jwksCmd.Flags().StringVar( + &jwksPublicKeyFile, + "public-key-file", + "", + "File containing the PEM-encoded public key", + ) + _ = jwksCmd.MarkFlagRequired("public-key-file") + + jwksCmd.Flags().StringVar( + &jwksKID, + "kid", + "", + "Optional explicit Key ID (kid) to use. If not provided, a SHA-256-derived ID is generated.", + ) +} diff --git a/cmd/jwtinfo.go b/cmd/jwtinfo.go index 07ca6ac..c9e26f6 100644 --- a/cmd/jwtinfo.go +++ b/cmd/jwtinfo.go @@ -29,13 +29,13 @@ var ( var jwtinfoCmd = &cobra.Command{ Use: "jwtinfo", - Short: "JwtInfo shows data from a JWT token", - Long: `JwtInfo shows data from a JWT token + Short: "Inspect and validate JSON Web Tokens (JWT)", + Long: `Inspect and validate JSON Web Tokens (JWT) from files or remote providers. Examples: export REQ_URL="https://sample.provider/oauth/token" export REQ_VALUES="{\"login\":\"values\"}" - export VALIDATION_URL="https://url.to/jkws.json" + export VALIDATION_URL="https://url.to/jwks.json" # Read a JWT token from a local file https-wrench jwtinfo --token-file /var/run/secrets/kubernetes.io/serviceaccount/token @@ -50,11 +50,12 @@ Examples: https-wrench jwtinfo --request-url $REQ_URL --request-values-json $REQ_VALUES --validation-url $VALIDATION_URL `, Run: func(cmd *cobra.Command, _ []string) { - // TODO: remove global --config option - - var err error - var tokenData jwtinfo.JwtTokenData + var ( + err error + tokenData jwtinfo.JwtTokenData + ) + // TODO: remove global --config option if tokenFile != "" { tokenData, err = jwtinfo.ReadTokenFromFile(tokenFile) if err != nil { @@ -62,6 +63,7 @@ Examples: "error while reading token value from file: %s", err, ) + return } } @@ -80,6 +82,7 @@ Examples: "error while reading request's values from file: %s", err, ) + return } } @@ -94,11 +97,13 @@ Examples: "error while parsing request's values JSON string: %s", err, ) + return } } tokenData, err = jwtinfo.RequestToken( + cmd.Context(), requestURL, requestValuesMap, client, @@ -118,7 +123,7 @@ Examples: } if jwksURL != "" { - err = tokenData.ParseWithJWKS(jwksURL, keyfuncDefOverride) + err = tokenData.ParseWithJWKS(cmd.Context(), jwksURL, keyfuncDefOverride) if err != nil { cmd.Printf("error while parsing token data: %s\n", err) return diff --git a/cmd/man.go b/cmd/man.go index 41c664a..69da0db 100644 --- a/cmd/man.go +++ b/cmd/man.go @@ -27,6 +27,7 @@ var manCmd = &cobra.Command{ Date: &now, Source: "https-wrench", } + err := doc.GenManTree(rootCmd, rootHeader, manPagesDestDir) if err != nil { fmt.Print(err) diff --git a/cmd/requests.go b/cmd/requests.go index 3d6469c..13a7e0e 100644 --- a/cmd/requests.go +++ b/cmd/requests.go @@ -22,7 +22,7 @@ var ( var requestsCmd = &cobra.Command{ Use: "requests", - Short: "Make HTTPS requests defined in the YAML configuration file", + Short: "Execute YAML-defined HTTPS requests", Long: ` https-wrench requests is the subcommand that does HTTPS requests according to the configuration pointed by the --config flag. @@ -62,6 +62,7 @@ Examples: if err != nil { cmd.Printf("\nConfig file not found: %s\n", viper.ConfigFileUsed()) _ = cmd.Help() + return } diff --git a/cmd/root.go b/cmd/root.go index fcc1222..1cd977a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -49,23 +49,27 @@ var ( var rootCmd = &cobra.Command{ Use: "https-wrench", - Short: "HTTPS Wrench, a tool to make Yaml defined HTTPS requests and inspect x.509 certificates and keys", + Short: "HTTPS Wrench, a tool for maintainers of secure HTTP endpoints", Long: ` -HTTPS Wrench is a tool to make HTTPS requests according to a Yaml configuration file -and to inspect x.509 certificates and keys. +HTTPS Wrench is a tool for maintainers of secure HTTP endpoints. +It enables executing YAML-defined HTTPS requests and performing in-depth +inspection of x.509 certificates, private keys, and JSON Web Tokens. -https-wrench has two subcommands: requests and certinfo. +https-wrench provides several specialized subcommands: -requests is the subcommand that does HTTPS requests according to the configuration provided -by the --config flag. +requests: Execute HTTPS requests according to a structured YAML configuration, +supporting custom CA bundles and verbose output. -certinfo is a subcommand that reads information from PEM encoded x.509 certificates and keys. The certificates -can be read from local files or TLS enabled endpoints. +certinfo: Inspect PEM-encoded certificates and keys from local files or remote +TLS endpoints. Verify certificate chains and key pairings. -certinfo can compare public keys extracted from certificates and private keys to check if they match. +jwtinfo: Decode, inspect, and validate JSON Web Tokens (JWT) using local files +or remote JWKS endpoints. -HTTPS Wrench is distributed with an open source license and available at the following address: -https://github.com/xenOs76/https-wrench`, +jwks: Generate pretty-printed JSON Web Key Sets (JWKS) from public keys for +exposure on well-known endpoints. + +Distributed under an open-source license: https://github.com/xenOs76/https-wrench`, Run: func(cmd *cobra.Command, _ []string) { showVersion, _ := cmd.Flags().GetBool("version") diff --git a/devenv.nix b/devenv.nix index bb96e39..3114c12 100644 --- a/devenv.nix +++ b/devenv.nix @@ -205,17 +205,28 @@ in { test -f $CAROOT/dhparam || curl https://ssl-config.mozilla.org/ffdhe2048.txt > $CAROOT/dhparam test -f $CAROOT/cert.pem || mkcert -key-file $CAROOT/key.pem -cert-file $CAROOT/cert.pem localhost 127.0.0.1 ::1 example.com *.example.com test -f $CAROOT/full-cert.pem || cat $CAROOT/cert.pem $CAROOT/rootCA.pem > $CAROOT/full-cert.pem + test -f $CAROOT/key.pub || openssl rsa -in $CAROOT/key.pem -pubout -out $CAROOT/key.pub + test -f $CAROOT/rsa-private_traditional.key || openssl rsa -in $CAROOT/key.pem -traditional -out $CAROOT/rsa-private_traditional.key + test -f $CAROOT/rsa-private_traditional.pub || openssl rsa -in $CAROOT/rsa-private_traditional.key -pubout -out $CAROOT/rsa-private_traditional.pub test -f $CAROOT/rsa-private_traditional_encrypted.key || openssl rsa -passout pass:$KEY_TEST_PW -in $CAROOT/rsa-private_traditional.key -out $CAROOT/rsa-private_traditional_encrypted.key -aes256 + test -f $CAROOT/rsa-private_traditional_encrypted.pub || openssl rsa -passin pass:$KEY_TEST_PW -in $CAROOT/rsa-private_traditional_encrypted.key -pubout -out $CAROOT/rsa-private_traditional_encrypted.pub test -f $CAROOT/private.ec.key || openssl ecparam -name prime256v1 -genkey -noout -out $CAROOT/private.ec.key + test -f $CAROOT/private.ec.pub || openssl ec -in $CAROOT/private.ec.key -pubout -out $CAROOT/private.ec.pub + test -f $CAROOT/encrypted.rsa.key || openssl genrsa -aes128 -passout pass:$KEY_TEST_PW -out $CAROOT/encrypted.rsa.key 4096 + test -f $CAROOT/encrypted.rsa.pub || openssl rsa -passin pass:$KEY_TEST_PW -in $CAROOT/encrypted.rsa.key -pubout -out $CAROOT/encrypted.rsa.pub # ECDSA_DIR=$CAROOT/ecdsa-cert test -d $ECDSA_DIR || mkdir $ECDSA_DIR test -f $ECDSA_DIR/ecdsa.key || openssl ecparam -name prime256v1 -genkey -noout -out $ECDSA_DIR/ecdsa.key + test -f $ECDSA_DIR/ecdsa.pub || openssl ec -in $ECDSA_DIR/ecdsa.key -pubout -out $ECDSA_DIR/ecdsa.pub + test -f $ECDSA_DIR/encrypted.ecdsa.key || openssl ec -in $ECDSA_DIR/ecdsa.key -out $ECDSA_DIR/encrypted.ecdsa.key -aes256 -passout pass:$KEY_TEST_PW + test -f $ECDSA_DIR/encrypted.ecdsa.pub || openssl ec -passin pass:$KEY_TEST_PW -in $ECDSA_DIR/encrypted.ecdsa.key -pubout -out $ECDSA_DIR/encrypted.ecdsa.pub + test -f $ECDSA_DIR/ecdsa.crt || openssl req -new -x509 -key $ECDSA_DIR/ecdsa.key -days 825 -out $ECDSA_DIR/ecdsa.crt \ -subj "/CN=example.com/O=Example Org" \ -addext "subjectAltName=DNS:example.com,DNS:alt.example.com,IP:10.0.0.5" @@ -223,7 +234,11 @@ in { # ED25519_DIR=$CAROOT/ed25519_cert test -d $ED25519_DIR || mkdir $ED25519_DIR test -f $ED25519_DIR/ed25519.key || openssl genpkey -algorithm Ed25519 -out $ED25519_DIR/ed25519.key + test -f $ED25519_DIR/ed25519.pub || openssl pkey -in $ED25519_DIR/ed25519.key -pubout -out $ED25519_DIR/ed25519.pub + test -f $ED25519_DIR/encrypted.ed25519.key || openssl pkey -in $ED25519_DIR/ed25519.key -out $ED25519_DIR/encrypted.ed25519.key -aes256 -passout pass:$KEY_TEST_PW + test -f $ED25519_DIR/encrypted.ed25519.pub || openssl pkey -passin pass:$KEY_TEST_PW -in $ED25519_DIR/encrypted.ed25519.key -pubout -out $ED25519_DIR/encrypted.ed25519.pub + test -f $ED25519_DIR/ed25519.crt || openssl req -new -x509 -key $ED25519_DIR/ed25519.key -days 365 -out $ED25519_DIR/ed25519.crt \ -subj "/CN=example.com/O=Example Org" -addext "subjectAltName=DNS:example.com,IP:127.0.0.1" ''; @@ -600,6 +615,15 @@ in { ./dist/https-wrench jwtinfo --request-url "$REQ_URL" --request-values-json "$JWTINFO_TEST_AUTH0" --validation-url "$VALIDATION_URL" ''; + scripts.run-jwtinfo-test-auth0-wrong-validation-url.exec = '' + gum format "### JwtInfo request against Auth0 with wrong validation URL" + + REQ_URL="https://dev-x3cci6dykofnlj5z.eu.auth0.com/oauth/token" + VALIDATION_URL="https://keycloak.k3s.os76.xyz/realms/os76/protocol/openid-connect/certs" + + ./dist/https-wrench jwtinfo --request-url "$REQ_URL" --request-values-json "$JWTINFO_TEST_AUTH0" --validation-url "$VALIDATION_URL" + ''; + scripts.run-jwtinfo-test-auth0-no-validation.exec = '' gum format "### JwtInfo request against Auth0: no validation" @@ -667,13 +691,6 @@ in { enterShell = '' gum format "# Devenv shell" - export GITEA_TOKEN=$(cat ~/.config/goreleaser/gitea_token) - export GITHUB_TOKEN=$(cat ~/.config/goreleaser/github_token) - - # JwtInfo tests against authentication providers when not on CI - # test -f ~/.config/https-wrench/jwtinfo_test_auth0_req_values.json && export JWTINFO_TEST_AUTH0=$(cat ~/.config/https-wrench/jwtinfo_test_auth0_req_values.json) - # test -f ~/.config/https-wrench/jwtinfo_test_keycloak_req_values.json && export JWTINFO_TEST_KEYCLOAK=$(cat ~/.config/https-wrench/jwtinfo_test_keycloak_req_values.json) - go version create-certs ''; diff --git a/go.mod b/go.mod index b5f7a67..65de9e0 100644 --- a/go.mod +++ b/go.mod @@ -6,44 +6,43 @@ require ( github.com/MicahParks/jwkset v0.11.0 github.com/MicahParks/keyfunc/v3 v3.8.0 github.com/alecthomas/assert/v2 v2.11.0 - github.com/alecthomas/chroma/v2 v2.23.1 - github.com/breml/rootcerts v0.3.4 + github.com/alecthomas/chroma/v2 v2.24.0 + github.com/breml/rootcerts v0.3.5 github.com/catppuccin/go v0.3.0 github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 github.com/dustin/go-humanize v1.0.1 - github.com/goforj/godump v1.9.1 github.com/golang-jwt/jwt/v5 v5.3.1 github.com/google/go-cmp v0.7.0 - github.com/gookit/goutil v0.7.3 - github.com/pires/go-proxyproto v0.11.0 + github.com/gookit/goutil v0.7.4 + github.com/pires/go-proxyproto v0.12.0 github.com/spf13/cobra v1.10.2 github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 - golang.org/x/term v0.40.0 + golang.org/x/term v0.42.0 ) require ( github.com/alecthomas/repr v0.5.2 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/charmbracelet/colorprofile v0.4.2 // indirect - github.com/charmbracelet/x/ansi v0.11.6 // indirect + github.com/charmbracelet/colorprofile v0.4.3 // indirect + github.com/charmbracelet/x/ansi v0.11.7 // indirect github.com/charmbracelet/x/cellbuf v0.0.15 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/dlclark/regexp2 v1.11.5 // indirect + github.com/dlclark/regexp2 v1.12.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/hexops/gotextdiff v1.0.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/lucasb-eyer/go-colorful v1.3.0 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.20 // indirect + github.com/lucasb-eyer/go-colorful v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.22 // indirect + github.com/mattn/go-runewidth v0.0.23 // indirect github.com/muesli/termenv v0.16.0 // indirect - github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pelletier/go-toml/v2 v2.3.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect @@ -54,10 +53,10 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.48.0 // indirect - golang.org/x/sys v0.41.0 // indirect - golang.org/x/text v0.34.0 // indirect - golang.org/x/time v0.14.0 // indirect + golang.org/x/crypto v0.50.0 // indirect + golang.org/x/sys v0.43.0 // indirect + golang.org/x/text v0.36.0 // indirect + golang.org/x/time v0.15.0 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index a9c19c7..071bf91 100644 --- a/go.sum +++ b/go.sum @@ -4,24 +4,24 @@ github.com/MicahParks/keyfunc/v3 v3.8.0 h1:Hx2dgIjAXGk9slakM6rV9BOeaWDPEXXZ4Us8g github.com/MicahParks/keyfunc/v3 v3.8.0/go.mod h1:z66bkCviwqfg2YUp+Jcc/xRE9IXLcMq6DrgV/+Htru0= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= -github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY= -github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o= +github.com/alecthomas/chroma/v2 v2.24.0 h1:zrg+k0tAaVbM8whaT2hR5DOUqAdopsDaH998EGi6Llk= +github.com/alecthomas/chroma/v2 v2.24.0/go.mod h1:l+ohZ9xRXIbGe7cIW+YZgOGbvuVLjMps/FYN/CwuabI= github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= -github.com/breml/rootcerts v0.3.4 h1:9i7WNl/ctd9OEAOaTfLy//Wrlfxq/tRQ7v4okYFN9Ys= -github.com/breml/rootcerts v0.3.4/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw= +github.com/breml/rootcerts v0.3.5 h1:oi7YiZ25HH52+mrKyjrMkcAFfnRDUf6HO8aUDr7RlJI= +github.com/breml/rootcerts v0.3.5/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw= github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= -github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY= -github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8= +github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q= +github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q= github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE= github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA= -github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= -github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= +github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI= +github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ= github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30= @@ -37,8 +37,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3 github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= -github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8= +github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -47,14 +47,12 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/goforj/godump v1.9.1 h1:9OGpb978Ytz3B59d5Yi2PzRYYLid6UkmhYDIDNiF15Y= -github.com/goforj/godump v1.9.1/go.mod h1:JsuL6AEZfKIU+iR5ewL6iQ2fIuhvLtPmJDH47M9Ptrc= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/gookit/goutil v0.7.3 h1:nXDd/AB17nEjqVCNDGioDhVL/gVqdlqRMfFergKDjHE= -github.com/gookit/goutil v0.7.3/go.mod h1:vJS9HXctYTCLtCsZot5L5xF+O1oR17cDYO9R0HxBmnU= +github.com/gookit/goutil v0.7.4 h1:OWgUngToNz+bPlX5aP+EMG31DraEU63uvKMwwT3vseM= +github.com/gookit/goutil v0.7.4/go.mod h1:vJS9HXctYTCLtCsZot5L5xF+O1oR17cDYO9R0HxBmnU= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -63,18 +61,18 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= -github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ= -github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4= +github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= +github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= +github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw= +github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= -github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= -github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4= -github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= +github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM= +github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pires/go-proxyproto v0.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM= +github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= @@ -106,19 +104,18 @@ github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zU github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= -golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= -golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= -golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= -golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/certinfo/certinfo.go b/internal/certinfo/certinfo.go index 0d4e28f..7e308f0 100644 --- a/internal/certinfo/certinfo.go +++ b/internal/certinfo/certinfo.go @@ -120,7 +120,8 @@ func (c *CertinfoConfig) SetCertsFromFile(filePath string, fileReader Reader) er } // SetPrivateKeyFromFile loads a private key from the specified PEM file. -// If the key is encrypted, it will attempt to retrieve the passphrase from an environment variable or interactive prompt. +// If the key is encrypted, it will attempt to retrieve the passphrase from an environment variable or +// interactive prompt. func (c *CertinfoConfig) SetPrivateKeyFromFile( filePath string, keyPwEnvVar string, diff --git a/internal/jwks/jwks.go b/internal/jwks/jwks.go new file mode 100644 index 0000000..f90832c --- /dev/null +++ b/internal/jwks/jwks.go @@ -0,0 +1,91 @@ +// Package jwks provides functionality for generating JSON Web Key Sets (JWKS) from public keys. +package jwks + +import ( + "bytes" + "context" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/json" + "encoding/pem" + "errors" + "fmt" + "os" + + "github.com/MicahParks/jwkset" +) + +// GenerateJWKS reads a public key from a file, parses it, and returns its JSON Web Key Set (JWKS) representation. +// If kid is provided, it sets the Key ID explicitly; otherwise, it computes a SHA-256-derived kid from the public key. +func GenerateJWKS(ctx context.Context, publicKeyFile string, kid string) (string, error) { + keyPEM, err := os.ReadFile(publicKeyFile) + if err != nil { + return "", fmt.Errorf("unable to read public key from %s: %w", publicKeyFile, err) + } + + block, _ := pem.Decode(keyPEM) + if block == nil { + return "", errors.New("failed to decode PEM block from public key file") + } + + key, err := jwkset.LoadX509KeyInfer(block) + if err != nil { + return "", fmt.Errorf("unsupported or invalid public key format: %w", err) + } + + // Ensure the key is a public key + switch key.(type) { + case *rsa.PublicKey, *ecdsa.PublicKey, ed25519.PublicKey: + // Valid public key types + default: + return "", errors.New("the provided file does not contain a supported public key " + + "(it might be a private key or an unsupported format)") + } + + if kid == "" { + pubBytes, err := x509.MarshalPKIXPublicKey(key) + if err != nil { + return "", fmt.Errorf("failed to marshal public key for SHA-256-derived kid: %w", err) + } + + hash := sha256.Sum256(pubBytes) + kid = base64.RawURLEncoding.EncodeToString(hash[:]) + } + + options := jwkset.JWKOptions{ + Metadata: jwkset.JWKMetadataOptions{ + KID: kid, + }, + } + + // Create JWK from the parsed public key + jwk, err := jwkset.NewJWKFromKey(key, options) + if err != nil { + return "", fmt.Errorf("failed to create JWK from public key: %w", err) + } + + // Initialize in-memory storage for the JWK Set + storage := jwkset.NewMemoryStorage() + + err = storage.KeyWrite(ctx, jwk) + if err != nil { + return "", fmt.Errorf("failed to write key to JWK Set storage: %w", err) + } + + jwksBytes, err := storage.JSONPublic(ctx) + if err != nil { + return "", fmt.Errorf("failed to generate JWKS JSON: %w", err) + } + + // Pretty-print the JSON + var prettyJWKS bytes.Buffer + if err := json.Indent(&prettyJWKS, jwksBytes, "", " "); err != nil { + return string(jwksBytes), nil + } + + return prettyJWKS.String(), nil +} diff --git a/internal/jwks/jwks_test.go b/internal/jwks/jwks_test.go new file mode 100644 index 0000000..0226e4d --- /dev/null +++ b/internal/jwks/jwks_test.go @@ -0,0 +1,135 @@ +package jwks + +import ( + "context" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGenerateJWKS_Success(t *testing.T) { + tmpDir := t.TempDir() + + // Helper to create a PEM file + createPEM := func(t *testing.T, filename, blockType string, bytes []byte) string { + t.Helper() + + path := filepath.Join(tmpDir, filename) + block := &pem.Block{ + Type: blockType, + Bytes: bytes, + } + file, err := os.Create(path) + require.NoError(t, err) + err = pem.Encode(file, block) + require.NoError(t, err) + file.Close() + + return path + } + + // RSA Setup + rsaPriv, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + rsaPubBytes, err := x509.MarshalPKIXPublicKey(&rsaPriv.PublicKey) + require.NoError(t, err) + rsaPubFile := createPEM(t, "rsa_public.pem", "PUBLIC KEY", rsaPubBytes) + + // ECDSA Setup + ecdsaPriv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + ecdsaPubBytes, err := x509.MarshalPKIXPublicKey(&ecdsaPriv.PublicKey) + require.NoError(t, err) + ecdsaPubFile := createPEM(t, "ecdsa_public.pem", "PUBLIC KEY", ecdsaPubBytes) + + // Ed25519 Setup + edPub, _, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err) + edPubBytes, err := x509.MarshalPKIXPublicKey(edPub) + require.NoError(t, err) + edPubFile := createPEM(t, "ed25519_public.pem", "PUBLIC KEY", edPubBytes) + + t.Run("RSA", func(t *testing.T) { + jwksJSON, err := GenerateJWKS(context.Background(), rsaPubFile, "") + require.NoError(t, err) + require.Contains(t, jwksJSON, `"kty": "RSA"`) + require.Contains(t, jwksJSON, `"kid":`) + }) + + t.Run("ECDSA", func(t *testing.T) { + jwksJSON, err := GenerateJWKS(context.Background(), ecdsaPubFile, "") + require.NoError(t, err) + require.Contains(t, jwksJSON, `"kty": "EC"`) + require.Contains(t, jwksJSON, `"crv": "P-256"`) + require.Contains(t, jwksJSON, `"kid":`) + }) + + t.Run("Ed25519", func(t *testing.T) { + jwksJSON, err := GenerateJWKS(context.Background(), edPubFile, "") + require.NoError(t, err) + require.Contains(t, jwksJSON, `"kty": "OKP"`) + require.Contains(t, jwksJSON, `"crv": "Ed25519"`) + require.Contains(t, jwksJSON, `"kid":`) + }) + + t.Run("Explicit KID", func(t *testing.T) { + expectedKID := "my-custom-kid" + jwksJSON, err := GenerateJWKS(context.Background(), rsaPubFile, expectedKID) + require.NoError(t, err) + require.Contains(t, jwksJSON, `"kid": "`+expectedKID+`"`) + }) +} + +func TestGenerateJWKS_Errors(t *testing.T) { + tmpDir := t.TempDir() + + t.Run("Invalid file", func(t *testing.T) { + _, err := GenerateJWKS(context.Background(), "non-existent-file.pem", "") + require.Error(t, err) + require.ErrorContains(t, err, "unable to read public key from") + }) + + t.Run("Invalid PEM", func(t *testing.T) { + invalidFile := filepath.Join(tmpDir, "invalid.pem") + err := os.WriteFile(invalidFile, []byte("not a pem"), 0644) + require.NoError(t, err) + + _, err = GenerateJWKS(context.Background(), invalidFile, "") + require.Error(t, err) + require.ErrorContains(t, err, "failed to decode PEM block") + }) + + t.Run("Unsupported block type", func(t *testing.T) { + path := filepath.Join(tmpDir, "unsupported.pem") + block := &pem.Block{Type: "NOT A KEY", Bytes: []byte("random data")} + file, _ := os.Create(path) + _ = pem.Encode(file, block) + file.Close() + + _, err := GenerateJWKS(context.Background(), path, "") + require.Error(t, err) + require.ErrorContains(t, err, "unsupported or invalid public key format") + }) + + t.Run("Private key rejected", func(t *testing.T) { + priv, _ := rsa.GenerateKey(rand.Reader, 2048) + path := filepath.Join(tmpDir, "private.pem") + block := &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)} + file, _ := os.Create(path) + _ = pem.Encode(file, block) + file.Close() + + _, err := GenerateJWKS(context.Background(), path, "") + require.Error(t, err) + require.ErrorContains(t, err, "does not contain a supported public key") + }) +} diff --git a/internal/jwtinfo/jwtinfo.go b/internal/jwtinfo/jwtinfo.go index 0517812..92eb532 100644 --- a/internal/jwtinfo/jwtinfo.go +++ b/internal/jwtinfo/jwtinfo.go @@ -49,7 +49,7 @@ type allReader func(io.Reader) ([]byte, error) // response types. // //nolint:revive -func RequestToken(reqURL string, reqValues map[string]string, client *http.Client, readAll allReader) (JwtTokenData, error) { +func RequestToken(ctx context.Context, reqURL string, reqValues map[string]string, client *http.Client, readAll allReader) (JwtTokenData, error) { if reqURL == emptyString { return JwtTokenData{}, errors.New("empty string provided as request URL") } @@ -65,7 +65,8 @@ func RequestToken(reqURL string, reqValues map[string]string, client *http.Clien urlReqValues.Add(k, v) } - req, err := http.NewRequest( + req, err := http.NewRequestWithContext( + ctx, "POST", reqURL, strings.NewReader(urlReqValues.Encode()), @@ -177,9 +178,14 @@ func ParseRequestJSONValues( return nil, fmt.Errorf("unable to parse Json request values: %w", err) } - maps.Copy(reqValuesMap, objmap) + newMap := maps.Clone(reqValuesMap) + if newMap == nil { + newMap = make(map[string]string) + } + + maps.Copy(newMap, objmap) - return reqValuesMap, nil + return newMap, nil } // ReadRequestValuesFile reads request values from a JSON file and merges them @@ -204,10 +210,18 @@ func ReadRequestValuesFile( return returnValuesMap, nil } -// isValidJSON checks if the provided byte slice contains valid JSON data. +// isValidJSON checks if the provided byte slice contains valid JSON object data. func isValidJSON(data []byte) bool { - var v any - return json.Unmarshal(data, &v) == nil + if !json.Valid(data) { + return false + } + + trimmed := bytes.TrimSpace(data) + if len(trimmed) < 2 { + return false + } + + return trimmed[0] == '{' && trimmed[len(trimmed)-1] == '}' } // DecodeBase64 decodes the base64-encoded header and claims of the access and @@ -313,14 +327,11 @@ func (jtd *JwtTokenData) ParseUnverified() error { // ParseWithJWKS parses and verifies the access token against the JSON Web Key Set (JWKS) // provided at the given URL. -func (jtd *JwtTokenData) ParseWithJWKS(jwksURL string, keyfuncOverride keyfunc.Override) error { +func (jtd *JwtTokenData) ParseWithJWKS(ctx context.Context, jwksURL string, keyfuncOverride keyfunc.Override) error { if jwksURL == emptyString { return errors.New("emptyString string provided as JWKS url") } - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - jwks, err := keyfunc.NewDefaultOverrideCtx( ctx, []string{jwksURL}, @@ -395,7 +406,7 @@ func PrintTokenInfo(jtd JwtTokenData, w io.Writer) error { fmt.Fprintln(w, style.LgSprintf(style.Title2, "%s", token.name)) fmt.Fprintln(w) - if token.name == "AccessToken" { + if token.name == "AccessToken" && jtd.AccessTokenJwt != nil { fmt.Fprintln(w, style.LgSprintf(style.ItemKey, "Valid %s", validString)) fmt.Fprintln(w) } @@ -417,9 +428,9 @@ func PrintTokenInfo(jtd JwtTokenData, w io.Writer) error { fmt.Fprintln(w) fmt.Fprintln(w, style.LgSprintf(style.ItemKey, "Claims")) - tokenTimeClaims, err := unmarshallTokenTimeClaims(token.claims) + tokenTimeClaims, err := unmarshalTokenTimeClaims(token.claims) if err != nil { - return fmt.Errorf("unable to unmashall time claims from %s: %w", token.name, err) + return fmt.Errorf("unable to unmarshal time claims from %s: %w", token.name, err) } cTable := table.New().Border(style.LGDefBorder) @@ -442,15 +453,15 @@ func PrintTokenInfo(jtd JwtTokenData, w io.Writer) error { return nil } -// unmarshallTokenTimeClaims extracts and converts numeric "iat" and "exp" claims +// unmarshalTokenTimeClaims extracts and converts numeric "iat" and "exp" claims // from a JSON byte slice into human-readable date strings. -func unmarshallTokenTimeClaims(claims []byte) (map[string]string, error) { +func unmarshalTokenTimeClaims(claims []byte) (map[string]string, error) { tokenClaims := make(map[string]string) genericClaims := make(map[string]any) if err := json.Unmarshal(claims, &genericClaims); err != nil { - return nil, fmt.Errorf("unable to unmarshall claims: %w", err) + return nil, fmt.Errorf("unable to unmarshal claims: %w", err) } if _, ok := genericClaims["iat"]; !ok { @@ -470,174 +481,15 @@ func unmarshallTokenTimeClaims(claims []byte) (map[string]string, error) { } for k, v := range genericClaims { - vi := v - - if vf, ok := vi.(float64); ok { - vInt64 := int64(vf) - t := time.Unix(vInt64, 0) - dateUTC := t.UTC().Format(time.UnixDate) - tokenClaims[k] = fmt.Sprintf("%v", dateUTC) - - continue + if k == "iat" || k == "exp" || k == "nbf" { + if vf, ok := v.(float64); ok { + vInt64 := int64(vf) + t := time.Unix(vInt64, 0) + dateUTC := t.UTC().Format(time.UnixDate) + tokenClaims[k] = dateUTC + } } } return tokenClaims, nil } - -// func unmarshallTokenClaims(claims []byte) (map[string]string, error) { -// tokenClaims := make(map[string]string) -// -// genericClaims := make(map[string]any) -// -// if err := json.Unmarshal(claims, &genericClaims); err != nil { -// return nil, err -// } -// -// for k, v := range genericClaims { -// var vi any = v -// -// if vs, ok := vi.(map[string]any); ok { -// tokenClaims[k] = fmt.Sprintf("%s", vs) -// continue -// } -// -// if vf, ok := vi.(float64); ok { -// vInt64 := int64(vf) -// t := time.Unix(vInt64, 0) -// dateUtc := t.UTC().String() -// -// outString := fmt.Sprintf("%v (%s)", int64(vf), dateUtc) -// -// tokenClaims[k] = fmt.Sprintf("%v", outString) -// -// continue -// } -// -// if vls, ok := vi.([]string); ok { -// tokenClaims[k] = strings.Join(vls, ",") -// continue -// } -// -// if vla, ok := vi.([]any); ok { -// tokenClaims[k] = fmt.Sprintf("%v", vla) -// continue -// } -// -// if vb, ok := vi.(bool); ok { -// tokenClaims[k] = fmt.Sprintf("%v", vb) -// continue -// } -// -// if vs, ok := vi.(string); ok { -// tokenClaims[k] = vs -// } else { -// fmt.Printf("not asserted: %v\n", v) -// } -// } -// -// return tokenClaims, nil -// } -// -// func unmarshallTokenHeader(header []byte) (map[string]string, error) { -// tokenHeader := make(map[string]string) -// -// if err := json.Unmarshal(header, &tokenHeader); err != nil { -// return nil, err -// } -// -// return tokenHeader, nil -// } -// -// func getTokenClaimsMap(t *jwt.Token) (map[string]string, error) { -// m := make(map[string]string) -// -// // Mandatory Registered Claims -// issuer, err := t.Claims.GetIssuer() -// if err != nil || issuer == emptyString { -// return nil, fmt.Errorf("unable to get issuer: %w", err) -// } -// -// subject, err := t.Claims.GetSubject() -// if err != nil || subject == emptyString { -// return nil, fmt.Errorf("unable to get subject: %w", err) -// } -// -// issuedAt, err := t.Claims.GetIssuedAt() -// if err != nil || issuedAt == nil { -// return nil, fmt.Errorf("unable to get issuedAt: %w", err) -// } -// -// expiresAt, err := t.Claims.GetExpirationTime() -// if err != nil || expiresAt == nil { -// return nil, fmt.Errorf("unable to get expiration time: %w", err) -// } -// -// audienceElems, err := t.Claims.GetAudience() -// if err != nil { -// return nil, fmt.Errorf("unable to get audience: %w", err) -// } -// -// audience := strings.Join(audienceElems, ",") -// -// m["iss"] = issuer -// m["sub"] = subject -// m["iat"] = issuedAt.UTC().String() -// m["exp"] = expiresAt.UTC().String() -// m["aud"] = audience -// -// // Optional Registered Claims -// notBefore, err := t.Claims.GetNotBefore() -// if err != nil { -// return nil, fmt.Errorf("unable to get notBefore time: %w", err) -// } -// -// if notBefore != nil { -// m["nbf"] = notBefore.UTC().String() -// } -// -// return m, nil -// } -// -// func getUnregisteredClaimsMap(t *jwt.Token, existingClaims map[string]string) map[string]string { -// unregistreredClaims := make(map[string]string) -// -// var claimsInt any = t.Claims -// -// if claimsMap, ok := claimsInt.(jwt.MapClaims); ok { -// for ck := range claimsMap { -// if _, alreadyPresent := existingClaims[ck]; alreadyPresent { -// continue -// } -// -// cki := claimsMap[ck] -// -// if cStringValue, ok := cki.(string); ok { -// unregistreredClaims[ck] = cStringValue -// } -// -// if cIntList, ok := cki.([]any); ok { -// unregistreredClaims[ck] = fmt.Sprintf("%s", cIntList) -// } -// } -// } -// -// return unregistreredClaims -// } -// -// func getTokenHeadersMap(t *jwt.Token) map[string]string { -// m := make(map[string]string) -// -// for k, v := range t.Header { -// headerValue := "undefined" -// i := v -// -// if v, ok := i.(string); ok { -// headerValue = v -// } -// -// m[k] = headerValue -// } -// -// return m -// } diff --git a/internal/jwtinfo/jwtinfo_test.go b/internal/jwtinfo/jwtinfo_test.go index 3a964cf..5cfbe0e 100644 --- a/internal/jwtinfo/jwtinfo_test.go +++ b/internal/jwtinfo/jwtinfo_test.go @@ -2,6 +2,7 @@ package jwtinfo import ( "bytes" + "context" "encoding/base64" "fmt" "io" @@ -281,6 +282,7 @@ func TestRequestToken(t *testing.T) { } _, err = RequestToken( + context.Background(), serverJwtEndpoint, reqValues, client, @@ -398,6 +400,7 @@ func TestParseWithJWKS(t *testing.T) { reqValues["scope"] = tt.scope td, err := RequestToken( + context.Background(), serverJwtEndpoint, reqValues, client, @@ -442,6 +445,7 @@ func TestParseWithJWKS(t *testing.T) { ) err = td.ParseWithJWKS( + context.Background(), serverJwksEmptyEndpoint, keyfuncOverrideTesting, ) @@ -470,6 +474,7 @@ func TestParseWithJWKS(t *testing.T) { ) err = td.ParseWithJWKS( + context.Background(), serverJwksFaultyEndpoint, keyfuncOverrideTesting, ) @@ -483,6 +488,7 @@ func TestParseWithJWKS(t *testing.T) { } err = td.ParseWithJWKS( + context.Background(), serverJwksEndpoint, keyfuncOverrideTesting, ) @@ -536,6 +542,7 @@ func TestParseWithJWKS_Errors(t *testing.T) { td := JwtTokenData{AccessTokenRaw: token} err = td.ParseWithJWKS( + context.Background(), "", keyfunc.Override{}, ) @@ -555,6 +562,7 @@ func TestParseWithJWKS_Errors(t *testing.T) { td := JwtTokenData{AccessTokenRaw: token} err = td.ParseWithJWKS( + context.Background(), "https://localhost:54321/jkws.wrong.json", keyfunc.Override{}, ) @@ -574,7 +582,8 @@ func TestParseWithJWKS_Errors(t *testing.T) { td := JwtTokenData{AccessTokenRaw: token} err = td.ParseWithJWKS( - "https://loca#$%^/jkws.json", + context.Background(), + "https://loca#$%^/jwks.json", keyfunc.Override{}, ) require.ErrorContains( @@ -688,8 +697,8 @@ func TestDecodeBase64(t *testing.T) { } } -func TestUnmarshallTokenTimeClaims(t *testing.T) { - t.Run("unmarshallTokenTimeClaims", func(t *testing.T) { +func TestUnmarshalTokenTimeClaims(t *testing.T) { + t.Run("unmarshalTokenTimeClaims", func(t *testing.T) { t.Parallel() var jtd JwtTokenData @@ -722,7 +731,7 @@ func TestUnmarshallTokenTimeClaims(t *testing.T) { err = jtd.DecodeBase64() require.NoError(t, err) - claimsMap, err := unmarshallTokenTimeClaims( + claimsMap, err := unmarshalTokenTimeClaims( jtd.AccessTokenClaims, ) require.NoError(t, err) @@ -740,7 +749,7 @@ func TestUnmarshallTokenTimeClaims(t *testing.T) { }) } -func TestUnmarshallTokenTimeClaims_MapErrors(t *testing.T) { +func TestUnmarshalTokenTimeClaims_MapErrors(t *testing.T) { invalidJSONClaims := "can not unmarshal" noIatClaims := "{\"exp\":1}" @@ -757,7 +766,7 @@ func TestUnmarshallTokenTimeClaims_MapErrors(t *testing.T) { { name: "invalid JSON", claims: []byte(invalidJSONClaims), - errMsg: "unable to unmarshall claims", + errMsg: "unable to unmarshal claims", }, { name: "missing Issued At", @@ -787,7 +796,7 @@ func TestUnmarshallTokenTimeClaims_MapErrors(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - _, err := unmarshallTokenTimeClaims(tt.claims) + _, err := unmarshalTokenTimeClaims(tt.claims) require.ErrorContains(t, err, tt.errMsg) //nolint:revive }) @@ -801,13 +810,14 @@ func TestUnmarshallTokenTimeClaims_MapErrors(t *testing.T) { //nolint:revive func TestPrintTokenInfo(t *testing.T) { tests := []struct { - name string - user string - pass string - scope string - bodyReader allReader - expError bool - expReqError bool + name string + user string + pass string + scope string + bodyReader allReader + skipValidation bool + expError bool + expReqError bool }{ { name: "default case", @@ -816,6 +826,14 @@ func TestPrintTokenInfo(t *testing.T) { bodyReader: io.ReadAll, scope: "default", }, + { + name: "without validation", + user: "test", + pass: "known", + bodyReader: io.ReadAll, + scope: "default", + skipValidation: true, + }, } for _, tc := range tests { @@ -850,6 +868,7 @@ func TestPrintTokenInfo(t *testing.T) { reqValues["scope"] = tt.scope td, err := RequestToken( + context.Background(), serverJwtEndpoint, reqValues, client, @@ -857,20 +876,23 @@ func TestPrintTokenInfo(t *testing.T) { ) require.NoError(t, err) - keyfuncOverrideTesting := keyfunc.Override{ - Client: server.Client(), - } + if !tt.skipValidation { + keyfuncOverrideTesting := keyfunc.Override{ + Client: server.Client(), + } - err = td.ParseWithJWKS( - serverJwksEndpoint, - keyfuncOverrideTesting, - ) - require.NoError(t, err) - require.True( - t, - td.AccessTokenJwt.Valid, - "JWT token must be valid", - ) + err = td.ParseWithJWKS( + context.Background(), + serverJwksEndpoint, + keyfuncOverrideTesting, + ) + require.NoError(t, err) + require.True( + t, + td.AccessTokenJwt.Valid, + "JWT token must be valid", + ) + } err = td.DecodeBase64() require.NoError(t, err) @@ -883,7 +905,6 @@ func TestPrintTokenInfo(t *testing.T) { stringsToCheck := []string{ "JwtInfo", - "Valid", "Header", "Claims", "alg", @@ -896,9 +917,17 @@ func TestPrintTokenInfo(t *testing.T) { "iat", } + if !tt.skipValidation { + stringsToCheck = append(stringsToCheck, "Valid") + } + for _, outStr := range stringsToCheck { require.Contains(t, got, outStr) } + + if tt.skipValidation { + require.NotContains(t, got, "Valid") + } }) } } @@ -939,7 +968,7 @@ func TestPrintTokenInfo_Errors(t *testing.T) { //nolint:revive buffer := bytes.Buffer{} - // Valid claims so unmarshallTokenTimeClaims succeeds. + // Valid claims so unmarshalTokenTimeClaims succeeds. now := time.Now().Unix() exp := now + 3600 claimsJSON := fmt.Sprintf(`{"iat": %d, "exp": %d}`, now, exp) @@ -957,7 +986,7 @@ func TestPrintTokenInfo_Errors(t *testing.T) { require.Positive(t, buffer.Len()) }) - t.Run("unmarshallTokenTimeClaims error", func(t *testing.T) { + t.Run("unmarshalTokenTimeClaims error", func(t *testing.T) { buffer := bytes.Buffer{} jtd := JwtTokenData{ @@ -967,6 +996,6 @@ func TestPrintTokenInfo_Errors(t *testing.T) { err := PrintTokenInfo(jtd, &buffer) require.Error(t, err) - require.ErrorContains(t, err, "unable to unmashall time claims from AccessToken") + require.ErrorContains(t, err, "unable to unmarshal time claims from AccessToken") }) } diff --git a/internal/style/style.go b/internal/style/style.go index 2a5eb13..ce659d1 100644 --- a/internal/style/style.go +++ b/internal/style/style.go @@ -8,7 +8,7 @@ import ( var ( glamourDefStyle = "tokyo-night" - chromaDefStyle = "dracula" + chromaDefStyle = "catppuccin-frappe" // LGDefBorder is the default hidden border for lipgloss tables. LGDefBorder = lipgloss.HiddenBorder()