0

最近,一个新的 API Look Up Order ID被添加到应用商店服务器 API 中。以及此 API 响应的JWSTransaction由 App Store 签名,采用 JSON Web 签名格式。我们想用 go 来验证它。

我们尝试过的

  1. 使用了jwt-go ,我们尝试根据这个问题从 pem 文件中提取公钥。同样根据此链接,应通过从私钥中提取公钥来解码响应
type JWSTransaction struct {
    BundleID             string `json:"bundleId"`
    InAppOwnershipType   string `json:"inAppOwnershipType"`
    TransactionID        string `json:"transactionId"`
    ProductID            string `json:"productId"`
    PurchaseDate         int64  `json:"purchaseDate"`
    Type                 string `json:"type"`
    OriginalPurchaseDate int64  `json:"originalPurchaseDate"`
}

func (ac *JWSTransaction) Valid() error {

    return nil
}

func (a *AppStore) readPrivateKeyFromFile(keyFile string) (*ecdsa.PrivateKey, error) {
    bytes, err := ioutil.ReadFile(keyFile)
    if err != nil {
        return nil, err
    }

    block, _ := pem.Decode(bytes)
    if block == nil {
        return nil, errors.New("appstore private key must be a valid .p8 PEM file")
    }

    key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
    if err != nil {
        return nil, err
    }

    switch pk := key.(type) {
    case *ecdsa.PrivateKey:
        return pk, nil
    default:
        return nil, errors.New("appstore private key must be of type ecdsa.PrivateKey")
    }
}

func (a *AppStore) ExtractClaims(tokenStr string) (*JWSTransaction, error) {
    privateKey, err := a.readPrivateKeyFromFile()
    if err != nil {
        return nil, err
    }
    
    publicKey, err := x509.MarshalPKIXPublicKey(privateKey.Public())
    if err != nil {
        return nil, err
    }
    fmt.Println(publicKey)

    tran := JWSTransaction{}

    token, err := jwt.ParseWithClaims(tokenStr, &tran, func(token *jwt.Token) (interface{}, error) {
        fmt.Println(token.Claims)
        fmt.Println(token.Method.Alg())

        return publicKey, nil
    })
    if err != nil {
        fmt.Println(err)
    }

但是,错误key is of invalid type来自jwt.ParseWithClaims.

  1. 通过此链接的 jwt-go 和 jwk 包验证它的另一种方法
    token, err := jwt.ParseWithClaims(tokenStr, &tran, func(token *jwt.Token) (interface{}, error) {
        fmt.Println(token.Claims)
        fmt.Println(token.Method.Alg())

        kid, ok := token.Header["kid"].(string)
        if !ok {
            return nil, errors.New("failed to find kid from headers")
        }
        key, found := keySet.LookupKeyID(kid)
        if !found {
            return nil, errors.New("failed to find kid from key set")
        }
        
        return publicKey, nil
    })

但是,我们未能在应用商店服务器 API 文档中找到公钥 URL。此外,kidJWSTransaction 的标头中也没有。

我们想知道如何在 Go 中验证应用商店服务器 api 的 JWS 事务?我有什么遗漏吗?

4

2 回答 2

1

谢谢Paulw11 ,根据文档

“x5c”(X.509 证书链)头参数包含与用于对 JWS 进行数字签名的密钥对应的 X.509 公钥证书或证书链 [RFC5280]。

func (a *AppStore) extractPublicKeyFromToken(tokenStr string) (*ecdsa.PublicKey, error) {
    tokenArr := strings.Split(tokenStr, ".")
    headerByte, err := base64.RawStdEncoding.DecodeString(tokenArr[0])
    if err != nil {
        return nil, err
    }

    type Header struct {
        Alg string   `json:"alg"`
        X5c []string `json:"x5c"`
    }
    var header Header
    err = json.Unmarshal(headerByte, &header)
    if err != nil {
        return nil, err
    }

    certByte, err := base64.StdEncoding.DecodeString(header.X5c[0])
    if err != nil {
        return nil, err
    }

    cert, err := x509.ParseCertificate(certByte)
    if err != nil {
        return nil, err
    }

    switch pk := cert.PublicKey.(type) {
    case *ecdsa.PublicKey:
        return pk, nil
    default:
        return nil, errors.New("appstore public key must be of type ecdsa.PublicKey")
    }
}

func (a *AppStore) ExtractClaims(tokenStr string) (*JWSTransaction, error) {
    tran := &JWSTransaction{}
    _, err := jwt.ParseWithClaims(tokenStr, tran, func(token *jwt.Token) (interface{}, error) {
        return a.extractPublicKeyFromToken(tokenStr)
    })
    if err != nil {
        return nil, err
    }

    return tran, nil
}

2022 年 1 月 26 日更新

为了使用来自站点的苹果根密钥验证 x5c 标头的根证书

参考这个循环。以下是示例代码

// Per doc: https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.6
func (a *AppStore) extractPublicKeyFromToken(tokenStr string) (*ecdsa.PublicKey, error) {
    certStr, err := a.extractHeaderByIndex(tokenStr, 0)
    if err != nil {
        return nil, err
    }

    cert, err := x509.ParseCertificate(certStr)
    if err != nil {
        return nil, err
    }

    switch pk := cert.PublicKey.(type) {
    case *ecdsa.PublicKey:
        return pk, nil
    default:
        return nil, errors.New("appstore public key must be of type ecdsa.PublicKey")
    }
}

func (a *AppStore) extractHeaderByIndex(tokenStr string, index int) ([]byte, error) {
    if index > 2 {
        return nil, errors.New("invalid index")
    }

    tokenArr := strings.Split(tokenStr, ".")
    headerByte, err := base64.RawStdEncoding.DecodeString(tokenArr[0])
    if err != nil {
        return nil, err
    }

    type Header struct {
        Alg string   `json:"alg"`
        X5c []string `json:"x5c"`
    }
    var header Header
    err = json.Unmarshal(headerByte, &header)
    if err != nil {
        return nil, err
    }

    certByte, err := base64.StdEncoding.DecodeString(header.X5c[index])
    if err != nil {
        return nil, err
    }

    return certByte, nil
}

// rootPEM is from `openssl x509 -inform der -in AppleRootCA-G3.cer -out apple_root.pem`
const rootPEM = `
-----BEGIN CERTIFICATE-----
MIICQzCCAcmgAwIBAgIILcX8iNLFS5UwCgYIKoZIzj0EAwMwZzEbMBkGA1UEAwwS
QXBwbGUgUm9vdCBDQSAtIEczMSYwJAYDVQQLDB1BcHBsZSBDZXJ0aWZpY2F0aW9u
IEF1dGhvcml0eTETMBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UEBhMCVVMwHhcN
MTQwNDMwMTgxOTA2WhcNMzkwNDMwMTgxOTA2WjBnMRswGQYDVQQDDBJBcHBsZSBS
....
-----END CERTIFICATE-----
`

func (a *AppStore) verifyCert(certByte []byte) error {
    roots := x509.NewCertPool()
    ok := roots.AppendCertsFromPEM([]byte(rootPEM))
    if !ok {
        return errors.New("failed to parse root certificate")
    }

    cert, err := x509.ParseCertificate(certByte)
    if err != nil {
        return err
    }

    opts := x509.VerifyOptions{
        Roots: roots,
    }

    if _, err := cert.Verify(opts); err != nil {
        return err
    }

    return nil
}

func (a *AppStore) ExtractClaims(tokenStr string) (*JWSTransaction, error) {
    tran := &JWSTransaction{}

    rootCertStr, err := a.extractHeaderByIndex(tokenStr, 2)
    if err != nil {
        return nil, err
    }
    if err = a.verifyCert(rootCertStr); err != nil {
        return nil, err
    }

    _, err = jwt.ParseWithClaims(tokenStr, tran, func(token *jwt.Token) (interface{}, error) {
        return a.extractPublicKeyFromToken(tokenStr)
    })
    if err != nil {
        return nil, err
    }

    return tran, nil
}

2022 年 1 月 30 日更新

添加验证中间证书逻辑如下

func (a *AppStore) verifyCert(certByte, intermediaCertStr []byte) error {
    roots := x509.NewCertPool()
    ok := roots.AppendCertsFromPEM([]byte(rootPEM))
    if !ok {
        return errors.New("failed to parse root certificate")
    }

    interCert, err := x509.ParseCertificate(intermediaCertStr)
    if err != nil {
        return errors.New("failed to parse intermedia certificate")
    }
    intermedia := x509.NewCertPool()
    intermedia.AddCert(interCert)

    cert, err := x509.ParseCertificate(certByte)
    if err != nil {
        return err
    }

    opts := x509.VerifyOptions{
        Roots:         roots,
        Intermediates: intermedia,
    }

    chains, err := cert.Verify(opts)
    if err != nil {
        return err
    }

    for _, ch := range chains {
        for _, c := range ch {
            fmt.Printf("%+v, %s, %+v \n", c.AuthorityKeyId, c.Subject.Organization, c.ExtKeyUsage)
        }
    }

    return nil
}

func (a *AppStore) ExtractClaims(tokenStr string) (*JWSTransaction, error) {
    tran := &JWSTransaction{}

    rootCertStr, err := a.extractHeaderByIndex(tokenStr, 2)
    if err != nil {
        return nil, err
    }
    intermediaCertStr, err := a.extractHeaderByIndex(tokenStr, 1)
    if err != nil {
        return nil, err
    }
    if err = a.verifyCert(rootCertStr, intermediaCertStr); err != nil {
        return nil, err
    }

    _, err = jwt.ParseWithClaims(tokenStr, tran, func(token *jwt.Token) (interface{}, error) {
        return a.extractPublicKeyFromToken(tokenStr)
    })
    if err != nil {
        return nil, err
    }

    return tran, nil
}
于 2021-10-27T13:13:48.123 回答
0

我们真的需要一个可以做到这一点的 golang 库,我目前正在实现一个服务器回调,可以将它组合到一个开源库中,这样它就更容易在 golang 中实现。

于 2022-03-03T15:40:56.620 回答