英文:
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 语言进行验证。
我们尝试过的方法:
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
。
- 另一种验证方法是使用 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
- 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
.
- 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
> "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
英文:
> 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字段中放置证书链来伪造一个假消息,如下所示:
- 我自己颁发的证书
- 苹果的中间证书
- 苹果的根证书
由于此代码仅验证最后两个证书的真实性,并未检查第一个证书是否由第二个证书颁发,因此我可以在那里放置任何我想要的内容。
我认为为了验证链中的第一个证书是否有效,缺少以下内容:
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:
- My own issued certificate
- Apple's intermediate certificate
- 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
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论