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

iugsix8n  于 2023-03-27  发布在  Go
关注(0)|答案(3)|浏览(170)

最近,应用商店服务器API中增加了一个新的API Look Up Order ID。并且该API响应的JWSTransaction由应用商店签名,为JSON Web签名格式。我们希望使用go验证它。
我们所尝试的
1.使用jwt-go,我们尝试按照this question从pem文件中提取公钥。同样按照this link,应该通过从私钥中提取公钥来解码响应

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.另一种方法是通过this link的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。而且,JWSTransaction的头中没有kid
我们想知道如何在Go中验证应用商店服务器API的JWS事务?我还缺少什么吗?

rkue9o1l

rkue9o1l1#

感谢Paulw11,根据文件
“x5 c”(X.509证书链)报头参数包含与用于对JWS进行数字签名的密钥相对应的X.509公钥证书或证书链[RFC 5280]。

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
}

更新01/26/2022

为了使用来自site的apple根密钥验证x5 c头的根证书
参考这个循环。下面是示例代码

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

更新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
}

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

ni65a41a

ni65a41a2#

我们真的需要一个golang库,可以做到这一点,我目前正在实现一个服务器回调,可以合并在一个开源库,使其更容易在golang实现。

t5zmwmid

t5zmwmid3#

如果我错了请纠正我,但最高分的答案似乎对我来说并不正确。证书链没有经过验证,用于验证消息的公钥来自根本没有经过验证的叶子证书。看起来我可以通过将证书链放在JWSDecodedHeader的x5c字段中来伪造一条假消息:
1.我自己颁发的证书
1.苹果的中级证书
1.苹果的根证书因为这个代码只验证最后2个的真实性,而不检查第一个是否是由第二个颁发的,所以我可以把我想要的任何东西放在那里。
我认为,为了验证链中的第一个证书是否有效,它缺少以下内容:

certStr, err := a.extractHeaderByIndex(tokenStr, 0)
if err != nil {
    return nil, err
}

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

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

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

相关问题