Skip to content

Commit 6f40146

Browse files
authored
Merge pull request #200 from richzw/feature/notification
feat(appstore): add decode notification v2 body method to appstore
2 parents aa730a7 + f1f4643 commit 6f40146

File tree

2 files changed

+146
-0
lines changed

2 files changed

+146
-0
lines changed

appstore/cert.go

+129
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package appstore
2+
3+
import (
4+
"crypto/ecdsa"
5+
"crypto/x509"
6+
"encoding/base64"
7+
"encoding/json"
8+
"errors"
9+
"fmt"
10+
"strings"
11+
)
12+
13+
// rootPEM is generated through `openssl x509 -inform der -in AppleRootCA-G3.cer -out apple_root.pem`
14+
const rootPEM = `
15+
-----BEGIN CERTIFICATE-----
16+
MIICQzCCAcmgAwIBAgIILcX8iNLFS5UwCgYIKoZIzj0EAwMwZzEbMBkGA1UEAwwS
17+
QXBwbGUgUm9vdCBDQSAtIEczMSYwJAYDVQQLDB1BcHBsZSBDZXJ0aWZpY2F0aW9u
18+
IEF1dGhvcml0eTETMBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UEBhMCVVMwHhcN
19+
MTQwNDMwMTgxOTA2WhcNMzkwNDMwMTgxOTA2WjBnMRswGQYDVQQDDBJBcHBsZSBS
20+
b290IENBIC0gRzMxJjAkBgNVBAsMHUFwcGxlIENlcnRpZmljYXRpb24gQXV0aG9y
21+
aXR5MRMwEQYDVQQKDApBcHBsZSBJbmMuMQswCQYDVQQGEwJVUzB2MBAGByqGSM49
22+
AgEGBSuBBAAiA2IABJjpLz1AcqTtkyJygRMc3RCV8cWjTnHcFBbZDuWmBSp3ZHtf
23+
TjjTuxxEtX/1H7YyYl3J6YRbTzBPEVoA/VhYDKX1DyxNB0cTddqXl5dvMVztK517
24+
IDvYuVTZXpmkOlEKMaNCMEAwHQYDVR0OBBYEFLuw3qFYM4iapIqZ3r6966/ayySr
25+
MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2gA
26+
MGUCMQCD6cHEFl4aXTQY2e3v9GwOAEZLuN+yRhHFD/3meoyhpmvOwgPUnPWTxnS4
27+
at+qIxUCMG1mihDK1A3UT82NQz60imOlM27jbdoXt2QfyFMm+YhidDkLF1vLUagM
28+
6BgD56KyKA==
29+
-----END CERTIFICATE-----
30+
`
31+
32+
type Cert struct{}
33+
34+
// ExtractCertByIndex extracts the certificate from the token string by index.
35+
func (c *Cert) extractCertByIndex(tokenStr string, index int) ([]byte, error) {
36+
if index > 2 {
37+
return nil, errors.New("invalid index")
38+
}
39+
40+
tokenArr := strings.Split(tokenStr, ".")
41+
headerByte, err := base64.RawStdEncoding.DecodeString(tokenArr[0])
42+
if err != nil {
43+
return nil, err
44+
}
45+
46+
type Header struct {
47+
Alg string `json:"alg"`
48+
X5c []string `json:"x5c"`
49+
}
50+
var header Header
51+
err = json.Unmarshal(headerByte, &header)
52+
if err != nil {
53+
return nil, err
54+
}
55+
56+
certByte, err := base64.StdEncoding.DecodeString(header.X5c[index])
57+
if err != nil {
58+
return nil, err
59+
}
60+
61+
return certByte, nil
62+
}
63+
64+
// VerifyCert verifies the certificate chain.
65+
func (c *Cert) verifyCert(rootCert, intermediaCert, leafCert *x509.Certificate) error {
66+
roots := x509.NewCertPool()
67+
ok := roots.AppendCertsFromPEM([]byte(rootPEM))
68+
if !ok {
69+
return errors.New("failed to parse root certificate")
70+
}
71+
72+
intermedia := x509.NewCertPool()
73+
intermedia.AddCert(intermediaCert)
74+
75+
opts := x509.VerifyOptions{
76+
Roots: roots,
77+
Intermediates: intermedia,
78+
}
79+
_, err := rootCert.Verify(opts)
80+
if err != nil {
81+
return err
82+
}
83+
84+
_, err = leafCert.Verify(opts)
85+
if err != nil {
86+
return err
87+
}
88+
89+
return nil
90+
}
91+
92+
func (c *Cert) ExtractPublicKeyFromToken(token string) (*ecdsa.PublicKey, error) {
93+
rootCertBytes, err := c.extractCertByIndex(token, 2)
94+
if err != nil {
95+
return nil, err
96+
}
97+
rootCert, err := x509.ParseCertificate(rootCertBytes)
98+
if err != nil {
99+
return nil, fmt.Errorf("appstore failed to parse root certificate")
100+
}
101+
102+
intermediaCertBytes, err := c.extractCertByIndex(token, 1)
103+
if err != nil {
104+
return nil, err
105+
}
106+
intermediaCert, err := x509.ParseCertificate(intermediaCertBytes)
107+
if err != nil {
108+
return nil, fmt.Errorf("appstore failed to parse intermediate certificate")
109+
}
110+
111+
leafCertBytes, err := c.extractCertByIndex(token, 0)
112+
if err != nil {
113+
return nil, err
114+
}
115+
leafCert, err := x509.ParseCertificate(leafCertBytes)
116+
if err != nil {
117+
return nil, fmt.Errorf("appstore failed to parse leaf certificate")
118+
}
119+
if err = c.verifyCert(rootCert, intermediaCert, leafCert); err != nil {
120+
return nil, err
121+
}
122+
123+
switch pk := leafCert.PublicKey.(type) {
124+
case *ecdsa.PublicKey:
125+
return pk, nil
126+
default:
127+
return nil, errors.New("appstore public key must be of type ecdsa.PublicKey")
128+
}
129+
}

appstore/validator.go

+17
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
"io/ioutil"
1010
"net/http"
1111
"time"
12+
13+
"github.com/golang-jwt/jwt/v4"
1214
)
1315

1416
//go:generate mockgen -destination=mocks/appstore.go -package=mocks github.com/awa/go-iap/appstore IAPClient
@@ -26,6 +28,7 @@ const (
2628
type IAPClient interface {
2729
Verify(ctx context.Context, reqBody IAPRequest, resp interface{}) error
2830
VerifyWithStatus(ctx context.Context, reqBody IAPRequest, resp interface{}) (int, error)
31+
ParseNotificationV2(tokenStr string, result *jwt.Token) error
2932
}
3033

3134
// Client implements IAPClient
@@ -187,3 +190,17 @@ func (c *Client) parseResponse(resp *http.Response, result interface{}, ctx cont
187190

188191
return r.Status, nil
189192
}
193+
194+
// ParseNotificationV2 parse notification from App Store Server
195+
func (c *Client) ParseNotificationV2(tokenStr string, result *jwt.Token) error {
196+
cert := Cert{}
197+
198+
result, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
199+
return cert.ExtractPublicKeyFromToken(tokenStr)
200+
})
201+
if err != nil {
202+
return err
203+
}
204+
205+
return nil
206+
}

0 commit comments

Comments
 (0)