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

Viewed 5665

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,
        }

2 Answers

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)
    }
}

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.

Using your example URL, my_url, here's an example of how to parse and validate JWTs.

package main

import (
    "log"
    "time"

    "github.com/dgrijalva/jwt-go"

    "github.com/MicahParks/keyfunc"
)

func main() {

    // Get the JWKS URL.
    jwksURL := "my_url"

    // Create the keyfunc options. Refresh the JWKS every hour and log errors.
    refreshInterval := time.Hour
    options := keyfunc.Options{
        RefreshInterval: &refreshInterval,
        RefreshErrorHandler: func(err error) {
            log.Printf("There was an error with the jwt.KeyFunc\nError: %s", err.Error())
        },
    }

    // 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 := "eyJhbGciOiJQUzM4NCIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJMeDFGbWF5UDJZQnR4YXFTMVNLSlJKR2lYUktudzJvdjVXbVlJTUctQkxFIn0.eyJleHAiOjE2MTU0MDY5ODIsImlhdCI6MTYxNTQwNjkyMiwianRpIjoiMGY2NGJjYTktYjU4OC00MWFhLWFkNDEtMmFmZDM2OGRmNTFkIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL2F1dGgvcmVhbG1zL21hc3RlciIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiJhZDEyOGRmMS0xMTQwLTRlNGMtYjA5Ny1hY2RjZTcwNWJkOWIiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJ0b2tlbmRlbG1lIiwiYWNyIjoiMSIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwiY2xpZW50SG9zdCI6IjE3Mi4yMC4wLjEiLCJjbGllbnRJZCI6InRva2VuZGVsbWUiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsInByZWZlcnJlZF91c2VybmFtZSI6InNlcnZpY2UtYWNjb3VudC10b2tlbmRlbG1lIiwiY2xpZW50QWRkcmVzcyI6IjE3Mi4yMC4wLjEifQ.Rxrq41AxbWKIQHWv-Tkb7rqwel3sKT_R_AGvn9mPIHqhw1m7nsQWcL9t2a_8MI2hCwgWtYdgTF1xxBNmb2IW3CZkML5nGfcRrFvNaBHd3UQEqbFKZgnIX29h5VoxekyiwFaGD-0RXL83jF7k39hytEzTatwoVjZ-frga0KFl-nLce3OwncRXVCGmxoFzUsyu9TQFS2Mm_p0AMX1y1MAX1JmLC3WFhH3BohhRqpzBtjSfs_f46nE1-HKjqZ1ERrAc2fmiVJjmG7sT702JRuuzrgUpHlMy2juBG4DkVcMlj4neJUmCD1vZyZBRggfaIxNkwUhHtmS2Cp9tOcwNu47tSg"

    // 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.")
}
Related