How to verify JWS transaction of app store server api in Go

huangapple go评论92阅读模式
英文:

How to verify JWS transaction of app store server api in Go

问题

最近,App Store 服务器 API 中添加了一个新的 API Look Up Order ID。该 API 的响应 JWSTransaction 由 App Store 签名,格式为 JSON Web Signature。我们想要使用 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)
	}
}

然而,jwt.ParseWithClaims 报错 key is of invalid type

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

然而,我们在 App Store 服务器 API 文档中找不到公钥的 URL。而且,JWSTransaction 的头部中没有 kid 字段。

我们想知道如何在 Go 中验证 App Store 服务器 API 的 JWS 交易?是否有什么遗漏的地方?

英文:

Recently, one new API Look Up Order ID was added into app store server API. And the JWSTransaction of this API response signed by the App Store, in JSON Web Signature format. We want to verify it with go.

What we have tried

  1. The jwt-go is used and we try to extract public key from pem file per this question. Also per this link, the response should be decoded by extracting a public key from private key
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)
	}

However, the error key is of invalid type comes up from jwt.ParseWithClaims.

  1. Another way to verify it through the jwt-go and jwk packages per this link
	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
	})

However, we failed to find the public key URL in app store server API doc. Also, there is no kid from the headers of JWSTransaction.

We want to know how to verify JWS transaction of app store server api in Go? Is there anything am I missing?

答案1

得分: 4

感谢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
}

更新于01/26/2022

为了验证x5c头部的根证书与来自网站的苹果根密钥匹配,请参考此循环。以下是示例代码:

// 根据文档: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 是从 `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

英文:

Thanks Paulw11
, Per doc

> The "x5c" (X.509 certificate chain) Header Parameter contains the
X.509 public key certificate or certificate chain [RFC5280]
corresponding to the key used to digitally sign the JWS.

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
}

Update 01/26/2022

In order to verify the root cert of x5c headers with apple root key from site

Refer to this loop. Here are sample codes

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

Update 01/30/2022

Add verify intermediate certificate logic as below

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
}

The details of implementation could be found here https://github.com/richzw/appstore

答案2

得分: 2

我们真的需要一个能够做到这一点的 Golang 库,我目前正在实现一个服务器回调,可以将其结合到一个开源库中,以便更容易在 Golang 中实现。

英文:

We really need a golang library that can do this, i'm currently implementing a server callback, could combine it in an open source library so its easier to implement in golang.

答案3

得分: 2

如果我理解正确的话,最高分的答案对我来说似乎不正确。证书链没有经过验证,用于验证消息的公钥来自未经验证的叶子证书。

看起来我可以通过在JWSDecodedHeader的x5c字段中放置证书链来伪造一个假消息,如下所示:

  1. 我自己颁发的证书
  2. 苹果的中间证书
  3. 苹果的根证书

由于此代码仅验证最后两个证书的真实性,并未检查第一个证书是否由第二个证书颁发,因此我可以在那里放置任何我想要的内容。

我认为为了验证链中的第一个证书是否有效,缺少以下内容:

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
}
英文:

Correct me if I'm wrong but the highest score answer doesn't seem correct to me. The certificate chain is not validated and the public key used to validate the message comes from the leaf certificate which is not validated at all.
It looks like I could forge a fake message by putting a certificate chain in the x5c field of the JWSDecodedHeader as such:

  1. My own issued certificate
  2. Apple's intermediate certificate
  3. Apple's root certificate
    Since this code is only validating the authenticity of the last 2 and not checking if the first one is issued by the second one, I can put whatever I want there.

I think that in order to verify that the first certificate in the chain is valid, it's missing this:

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
}

huangapple
  • 本文由 发表于 2021年10月27日 16:48:52
  • 转载请务必保留本文链接:https://go.coder-hub.com/69735525.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定