如何在 Go 中验证应用商店服务器 api 的 JWS 事务

zan*_*ngw 4 go storekit ios jwt jwt-go

最近,应用商店服务器 API 中添加了一种新的 API查找订单 ID 。以及由 App Store 签名的此 API 响应的JWSTransaction ,采用 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)
    }
Run Code Online (Sandbox Code Playgroud)

然而,错误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
    })
Run Code Online (Sandbox Code Playgroud)

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

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

zan*_*ngw 5

谢谢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
}
Run Code Online (Sandbox Code Playgroud)

更新 01/26/2022

为了使用站点上的苹果根密钥验证 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
}
Run Code Online (Sandbox Code Playgroud)

更新 01/30/2022

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

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
}
Run Code Online (Sandbox Code Playgroud)

实现细节可以在这里找到:https://github.com/richzw/appstore