将JWK json转换为公钥golang(lestrrat-go)

6

我正在使用JWKS格式,从认证服务中提供公钥,以验证来自该认证服务的令牌。但是,为了执行验证,我需要从JWK重建公钥。如何将其转换?

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

更新

我尝试使用 github.com/lestrrat-go/jwx/jwk 库解析 JWKs,但是我无法找到如何继续的方法:

set,err := jwk.Parse(bodyBytes)

key,err2 := set.Get(0)

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

最后我手动转换了它:

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


github.com/lestrrat-go/jwx/jwk 可以为您完成此操作(示例 - Brits
嗨,我正在使用那个库,但我不知道如何将其转换为*rsa.PublicKey格式。 - AndreaCostanzo1
FetchParseRawKey 应该提供您所需的功能(根据您的用例);如果不是,请使用该软件包进行尝试并在问题中附上您的尝试(如果您的 URL 是私有的,可以使用 https://www.googleapis.com/oauth2/v3/certs)。 - Brits
请参见https://stackoverflow.com/questions/64897846/convert-a-json-public-private-key-pair-to-rsa-privatekey-and-rsa-publickey/64901713#64901713 - jps
问题在于我有一个基于rsa.PublicKey的验证算法,而我需要一种将密钥转换的方法。 - AndreaCostanzo1
2个回答

7
我写了一个Go包,专门为此目的而设计:github.com/MicahParks/keyfunc

转为*rsa.PublicKey

在这个包中,JSON Web Key (JWK) 的数据格式采用了 Go 结构体。它支持 ECDSA 和 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{}
}

在将原始的 JSON 消息解组为上述结构后,此方法将其转换为 *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
}

从 JSON Web Key Set (JWKS) 解析和验证 JWT。

我制作了这个包来与 github.com/dgrijalva/jwt-go 一起使用,以更轻松地解析和验证 JWT。

以下是解析和验证 JWT 的示例。

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

当我使用这个示例时,我会收到以下错误:
jwks.KeyFunc未定义(类型*keyfunc.JWKS没有KeyFunc字段或方法)
- FishingIsLife
我今天稍后会更新StackOverflow的示例。请查看GitHub项目中的此示例:https://github.com/MicahParks/keyfunc/blob/master/examples/recommended_options/main.go - Micah Parks
谢谢更新。现在它可以工作了,但你能解释一下为什么需要刷新例程吗?在无状态应用程序中,我们应该总是根据请求获取密钥,或者我漏掉了一些细节吗? - FishingIsLife
JWK集在密钥轮换时会被更新。背景协程用于自动缓存JWK集并跟踪JWK集的更改。使用缓存还可以减少处理请求所需的时间。该软件包默认不会更新其缓存。如果您的用例需要,可以查看Go文档中的keyfunc.Options以启用自动缓存。 - Micah Parks

7

了解您已有解决方案,但是由于您正在使用 github.com/lestrrat-go/jwx/jwk 进行尝试,在这里提供一个使用该包的方法(基本上就是示例中的内容):

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接