Converting JWK json into a public key golang (lestrrat-go)
Asked Answered
W

2

6

I'm using JWKS format to provide from an authentication service the public key that can be used to validate tokens coming from that authentication service. However, to perform validation I need to rebuild the public key from the JWK. How can I convert it?

type JWKeys struct {
    Keys []JWKey `json:"keys"`
}

type JWKey struct {
    Kty string `json:"kty"`
    Use string `json:"use,omitempty"`
    Kid string `json:"kid,omitempty"`
    Alg string `json:"alg,omitempty"`

    Crv string `json:"crv,omitempty"`
    X   string `json:"x,omitempty"`
    Y   string `json:"y,omitempty"`
    D   string `json:"d,omitempty"`
    N   string `json:"n,omitempty"`
    E   string `json:"e,omitempty"`
    K   string `json:"k,omitempty"`
}

var PublicKey *rsa.PublicKey

func SetUpExternalAuth() {
    res, err := http.Get("my_url")

    if err != nil {
        log.Fatal("Can't retrieve the key for authentication")
    }

    bodyBytes, err := ioutil.ReadAll(res.Body)
    if err != nil {
        log.Fatal(err)
    }

    var keys JWKeys

    json.Unmarshal(bodyBytes, &keys)

    //CONVERT JWK TO *rsa.PUBLICKEY???
}

UPDATE

I tried to parse the JWKs using github.com/lestrrat-go/jwx/jwk library, however I couldn't find how to continue:

set,err := jwk.Parse(bodyBytes)

key,err2 := set.Get(0)

//HOW TO CONVERT KEY INTO A *rsa.PublicKey?

At the end I've manually converted it:

if singleJWK.Kty != "RSA" {
            log.Fatal("invalid key type:", singleJWK.Kty)
        }

        // decode the base64 bytes for n
        nb, err := base64.RawURLEncoding.DecodeString(singleJWK.N)
        if err != nil {
            log.Fatal(err)
        }

        e := 0
        // The default exponent is usually 65537, so just compare the
        // base64 for [1,0,1] or [0,1,0,1]
        if singleJWK.E == "AQAB" || singleJWK.E == "AAEAAQ" {
            e = 65537
        } else {
            // need to decode "e" as a big-endian int
            log.Fatal("need to deocde e:", singleJWK.E)
        }

        PublicKey = &rsa.PublicKey{
            N: new(big.Int).SetBytes(nb),
            E: e,
        }

Waldos answered 21/3, 2021 at 22:2 Comment(5)
github.com/lestrrat-go/jwx/jwk can do this for you (example)Giannagianni
Hi, I was using that library but I couldn't understand how to convert into a *rsa.PublicKey formatWaldos
Fetch or ParseRawKey should provide the functionality you need (depending upon your use-case); if not please amend your question with your attempt using the package (perhaps use https://www.googleapis.com/oauth2/v3/certs if your url is private).Giannagianni
see also #64898346Sometimes
The problem was that I had a validation algorithm bases on rsa.PublicKey and I needed a way to convert the key.Waldos
G
8

Understand you have a solution but as you were making the attempt using github.com/lestrrat-go/jwx/jwk here is an approach with that package (pretty much what is in the example):

package main

import (
    "context"
    "crypto/rsa"
    "fmt"
    "log"

    "github.com/lestrrat-go/jwx/jwk"
)

func main() {
    // Example jwk from https://www.googleapis.com/oauth2/v3/certs (but with only one cert for simplicity)
    jwkJSON := `{
  "keys": [ 
    {
      "kty": "RSA",
      "n": "o76AudS2rsCvlz_3D47sFkpuz3NJxgLbXr1cHdmbo9xOMttPMJI97f0rHiSl9stltMi87KIOEEVQWUgMLaWQNaIZThgI1seWDAGRw59AO5sctgM1wPVZYt40fj2Qw4KT7m4RLMsZV1M5NYyXSd1lAAywM4FT25N0RLhkm3u8Hehw2Szj_2lm-rmcbDXzvjeXkodOUszFiOqzqBIS0Bv3c2zj2sytnozaG7aXa14OiUMSwJb4gmBC7I0BjPv5T85CH88VOcFDV51sO9zPJaBQnNBRUWNLh1vQUbkmspIANTzj2sN62cTSoxRhSdnjZQ9E_jraKYEW5oizE9Dtow4EvQ",
      "use": "sig",
      "alg": "RS256",
      "e": "AQAB",
      "kid": "6a8ba5652a7044121d4fedac8f14d14c54e4895b"
    }
  ]
}
`

    set, err := jwk.Parse([]byte(jwkJSON))
    if err != nil {
        panic(err)
    }
    fmt.Println(set)
    for it := set.Iterate(context.Background()); it.Next(context.Background()); {
        pair := it.Pair()
        key := pair.Value.(jwk.Key)

        var rawkey interface{} // This is the raw key, like *rsa.PrivateKey or *ecdsa.PrivateKey
        if err := key.Raw(&rawkey); err != nil {
            log.Printf("failed to create public key: %s", err)
            return
        }

        // We know this is an RSA Key so...
        rsa, ok := rawkey.(*rsa.PublicKey)
        if !ok {
            panic(fmt.Sprintf("expected ras key, got %T", rawkey))
        }
        // As this is a demo just dump the key to the console
        fmt.Println(rsa)
    }
}
Giannagianni answered 22/3, 2021 at 2:43 Comment(1)
Perfect! I was having problems mixing the code from the "github.com/dgrijalva/jwt-go" package for parsing (specifically, ParseWithClaims), with the public keys fetched by the jwk package from "github.com/lestrrat-go/jwx/v2/jwk". I was missing the proper usage of the key.Raw method.Validity
S
8

I wrote a Go package exactly for this purpose: github.com/MicahParks/keyfunc

Converting to a *rsa.PublicKey

In this pacakge a JSON Web Key (JWK) looks like this Go struct. It supports both ECDSA and RSA JWK.

// JSONKey represents a raw key inside a JWKS.
type JSONKey struct {
    Curve       string `json:"crv"`
    Exponent    string `json:"e"`
    ID          string `json:"kid"`
    Modulus     string `json:"n"`
    X           string `json:"x"`
    Y           string `json:"y"`
    precomputed interface{}
}

After the raw JSON message is unmarshaled into the above struct, this method converts it to an *rsa.PublicKey.

package keyfunc

import (
    "crypto/rsa"
    "encoding/base64"
    "fmt"
    "math/big"
)

const (

    // rs256 represents a public cryptography key generated by a 256 bit RSA algorithm.
    rs256 = "RS256"

    // rs384 represents a public cryptography key generated by a 384 bit RSA algorithm.
    rs384 = "RS384"

    // rs512 represents a public cryptography key generated by a 512 bit RSA algorithm.
    rs512 = "RS512"

    // ps256 represents a public cryptography key generated by a 256 bit RSA algorithm.
    ps256 = "PS256"

    // ps384 represents a public cryptography key generated by a 384 bit RSA algorithm.
    ps384 = "PS384"

    // ps512 represents a public cryptography key generated by a 512 bit RSA algorithm.
    ps512 = "PS512"
)

// RSA parses a JSONKey and turns it into an RSA public key.
func (j *JSONKey) RSA() (publicKey *rsa.PublicKey, err error) {

    // Check if the key has already been computed.
    if j.precomputed != nil {
        return j.precomputed.(*rsa.PublicKey), nil
    }

    // Confirm everything needed is present.
    if j.Exponent == "" || j.Modulus == "" {
        return nil, fmt.Errorf("%w: rsa", ErrMissingAssets)
    }

    // Decode the exponent from Base64.
    //
    // According to RFC 7518, this is a Base64 URL unsigned integer.
    // https://tools.ietf.org/html/rfc7518#section-6.3
    var exponent []byte
    if exponent, err = base64.RawURLEncoding.DecodeString(j.Exponent); err != nil {
        return nil, err
    }

    // Decode the modulus from Base64.
    var modulus []byte
    if modulus, err = base64.RawURLEncoding.DecodeString(j.Modulus); err != nil {
        return nil, err
    }

    // Create the RSA public key.
    publicKey = &rsa.PublicKey{}

    // Turn the exponent into an integer.
    //
    // According to RFC 7517, these numbers are in big-endian format.
    // https://tools.ietf.org/html/rfc7517#appendix-A.1
    publicKey.E = int(big.NewInt(0).SetBytes(exponent).Uint64())

    // Turn the modulus into a *big.Int.
    publicKey.N = big.NewInt(0).SetBytes(modulus)

    // Keep the public key so it won't have to be computed every time.
    j.precomputed = publicKey

    return publicKey, nil
}

Parsing and validating a JWT from a JSON Web Key Set (JWKS).

I made this package to work with github.com/dgrijalva/jwt-go to more easily parse and validate JWTs with the most popular package.

Here's an example of how to parse and validate JWTs.

package main

import (
    "context"
    "log"
    "time"

    "github.com/golang-jwt/jwt/v4"

    "github.com/MicahParks/keyfunc"
)

func main() {
    // Get the JWKS URL.
    //
    // This is a sample JWKS service. Visit https://jwks-service.appspot.com/ and grab a token to test this example.
    jwksURL := "https://jwks-service.appspot.com/.well-known/jwks.json"

    // Create a context that, when cancelled, ends the JWKS background refresh goroutine.
    ctx, cancel := context.WithCancel(context.Background())

    // Create the keyfunc options. Use an error handler that logs. Refresh the JWKS when a JWT signed by an unknown KID
    // is found or at the specified interval. Rate limit these refreshes. Timeout the initial JWKS refresh request after
    // 10 seconds. This timeout is also used to create the initial context.Context for keyfunc.Get.
    options := keyfunc.Options{
        Ctx: ctx,
        RefreshErrorHandler: func(err error) {
            log.Printf("There was an error with the jwt.Keyfunc\nError: %s", err.Error())
        },
        RefreshInterval:   time.Hour,
        RefreshRateLimit:  time.Minute * 5,
        RefreshTimeout:    time.Second * 10,
        RefreshUnknownKID: true,
    }

    // Create the JWKS from the resource at the given URL.
    jwks, err := keyfunc.Get(jwksURL, options)
    if err != nil {
        log.Fatalf("Failed to create JWKS from resource at the given URL.\nError: %s", err.Error())
    }

    // Get a JWT to parse.
    jwtB64 := "eyJraWQiOiJlZThkNjI2ZCIsInR5cCI6IkpXVCIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiJXZWlkb25nIiwiYXVkIjoiVGFzaHVhbiIsImlzcyI6Imp3a3Mtc2VydmljZS5hcHBzcG90LmNvbSIsImlhdCI6MTYzMTM2OTk1NSwianRpIjoiNDY2M2E5MTAtZWU2MC00NzcwLTgxNjktY2I3NDdiMDljZjU0In0.LwD65d5h6U_2Xco81EClMa_1WIW4xXZl8o4b7WzY_7OgPD2tNlByxvGDzP7bKYA9Gj--1mi4Q4li4CAnKJkaHRYB17baC0H5P9lKMPuA6AnChTzLafY6yf-YadA7DmakCtIl7FNcFQQL2DXmh6gS9J6TluFoCIXj83MqETbDWpL28o3XAD_05UP8VLQzH2XzyqWKi97mOuvz-GsDp9mhBYQUgN3csNXt2v2l-bUPWe19SftNej0cxddyGu06tXUtaS6K0oe0TTbaqc3hmfEiu5G0J8U6ztTUMwXkBvaknE640NPgMQJqBaey0E4u0txYgyvMvvxfwtcOrDRYqYPBnA"

    // Parse the JWT.
    token, err := jwt.Parse(jwtB64, jwks.Keyfunc)
    if err != nil {
        log.Fatalf("Failed to parse the JWT.\nError: %s", err.Error())
    }

    // Check if the token is valid.
    if !token.Valid {
        log.Fatalf("The token is not valid.")
    }
    log.Println("The token is valid.")

    // End the background refresh goroutine when it's no longer needed.
    cancel()

    // This will be ineffectual because the line above this canceled the parent context.Context.
    // This method call is idempotent similar to context.CancelFunc.
    jwks.EndBackground()
}
Sorrento answered 22/3, 2021 at 0:41 Comment(4)
I get the following error when I use this example: > jwks.KeyFunc undefined (type *keyfunc.JWKS has no field or method KeyFunc)Overeager
I'll update the StackOverflow example later today. Please see this example from the GitHub project: github.com/MicahParks/keyfunc/blob/master/examples/…Sorrento
thanks for the update. It works now but can you explain why the refresh routine is needed? In a stateless application we should always fetch the key per request or am I missing some details here?Overeager
JWK Sets are updated when keys are rotated. The background goroutine is used to automatically cache the JWK Set and keep up with JWK Set changes. Using a cache will also decrease the amount of time it takes to process requests. This package does not update its cache by default. Please see the Go docs for the keyfunc.Options to enable automatic caching if your use case needs it.Sorrento
G
8

Understand you have a solution but as you were making the attempt using github.com/lestrrat-go/jwx/jwk here is an approach with that package (pretty much what is in the example):

package main

import (
    "context"
    "crypto/rsa"
    "fmt"
    "log"

    "github.com/lestrrat-go/jwx/jwk"
)

func main() {
    // Example jwk from https://www.googleapis.com/oauth2/v3/certs (but with only one cert for simplicity)
    jwkJSON := `{
  "keys": [ 
    {
      "kty": "RSA",
      "n": "o76AudS2rsCvlz_3D47sFkpuz3NJxgLbXr1cHdmbo9xOMttPMJI97f0rHiSl9stltMi87KIOEEVQWUgMLaWQNaIZThgI1seWDAGRw59AO5sctgM1wPVZYt40fj2Qw4KT7m4RLMsZV1M5NYyXSd1lAAywM4FT25N0RLhkm3u8Hehw2Szj_2lm-rmcbDXzvjeXkodOUszFiOqzqBIS0Bv3c2zj2sytnozaG7aXa14OiUMSwJb4gmBC7I0BjPv5T85CH88VOcFDV51sO9zPJaBQnNBRUWNLh1vQUbkmspIANTzj2sN62cTSoxRhSdnjZQ9E_jraKYEW5oizE9Dtow4EvQ",
      "use": "sig",
      "alg": "RS256",
      "e": "AQAB",
      "kid": "6a8ba5652a7044121d4fedac8f14d14c54e4895b"
    }
  ]
}
`

    set, err := jwk.Parse([]byte(jwkJSON))
    if err != nil {
        panic(err)
    }
    fmt.Println(set)
    for it := set.Iterate(context.Background()); it.Next(context.Background()); {
        pair := it.Pair()
        key := pair.Value.(jwk.Key)

        var rawkey interface{} // This is the raw key, like *rsa.PrivateKey or *ecdsa.PrivateKey
        if err := key.Raw(&rawkey); err != nil {
            log.Printf("failed to create public key: %s", err)
            return
        }

        // We know this is an RSA Key so...
        rsa, ok := rawkey.(*rsa.PublicKey)
        if !ok {
            panic(fmt.Sprintf("expected ras key, got %T", rawkey))
        }
        // As this is a demo just dump the key to the console
        fmt.Println(rsa)
    }
}
Giannagianni answered 22/3, 2021 at 2:43 Comment(1)
Perfect! I was having problems mixing the code from the "github.com/dgrijalva/jwt-go" package for parsing (specifically, ParseWithClaims), with the public keys fetched by the jwk package from "github.com/lestrrat-go/jwx/v2/jwk". I was missing the proper usage of the key.Raw method.Validity

© 2022 - 2024 — McMap. All rights reserved.