From 5628ba7997cce76588b9345c97e5084a2af0d89a Mon Sep 17 00:00:00 2001 From: Wei Zang Date: Mon, 15 May 2023 14:33:45 +0800 Subject: [PATCH 1/3] feat(appstore): add summary field to v2 response of app store notification --- appstore/notification_v2.go | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/appstore/notification_v2.go b/appstore/notification_v2.go index ecc8204..67e85e9 100644 --- a/appstore/notification_v2.go +++ b/appstore/notification_v2.go @@ -106,12 +106,26 @@ type ( // SubscriptionNotificationV2DecodedPayload is struct // https://developer.apple.com/documentation/appstoreservernotifications/responsebodyv2decodedpayload SubscriptionNotificationV2DecodedPayload struct { - NotificationType NotificationTypeV2 `json:"notificationType"` - Subtype SubtypeV2 `json:"subtype"` - NotificationUUID string `json:"notificationUUID"` - NotificationVersion string `json:"version"` - SignedDate int64 `json:"signedDate"` - Data SubscriptionNotificationV2Data `json:"data"` + NotificationType NotificationTypeV2 `json:"notificationType"` + Subtype SubtypeV2 `json:"subtype"` + NotificationUUID string `json:"notificationUUID"` + NotificationVersion string `json:"version"` + SignedDate int64 `json:"signedDate"` + Data SubscriptionNotificationV2Data `json:"data,omitempty"` + Summary SubscriptionNotificationV2Summary `json:"summary,omitempty"` + } + + // SubscriptionNotificationV2Summary is struct + // https://developer.apple.com/documentation/appstoreservernotifications/summary + SubscriptionNotificationV2Summary struct { + RequestIdentifier string `json:"requestIdentifier"` + Environment string `json:"environment"` + AppAppleId int64 `json:"appAppleId"` + BundleID string `json:"bundleId"` + ProductID string `json:"productId"` + StorefrontCountryCodes string `json:"storefrontCountryCodes"` + FailedCount int64 `json:"failedCount"` + SucceededCount int64 `json:"succeededCount"` } // SubscriptionNotificationV2Data is struct From 7354ffc93e166187428de0660622220364eee138 Mon Sep 17 00:00:00 2001 From: Wei Zang Date: Mon, 15 May 2023 15:27:27 +0800 Subject: [PATCH 2/3] fix(playstore): fix expected error in test file --- playstore/validator_test.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/playstore/validator_test.go b/playstore/validator_test.go index 317092b..e5e2007 100644 --- a/playstore/validator_test.go +++ b/playstore/validator_test.go @@ -96,7 +96,7 @@ func TestNewDefaultTokenSourceClient(t *testing.T) { func TestAcknowledgeSubscription(t *testing.T) { t.Parallel() // Exception scenario - expected := "googleapi: Error 400: Invalid Value, invalid" + expected := "googleapi: Error 404: No application was found for the given package name., applicationNotFound" client, _ := New(jsonKey) ctx := context.Background() @@ -115,7 +115,7 @@ func TestAcknowledgeSubscription(t *testing.T) { func TestVerifySubscription(t *testing.T) { t.Parallel() // Exception scenario - expected := "googleapi: Error 400: Invalid Value, invalid" + expected := "googleapi: Error 404: No application was found for the given package name., applicationNotFound" client, _ := New(jsonKey) ctx := context.Background() @@ -131,7 +131,7 @@ func TestVerifySubscription(t *testing.T) { func TestVerifySubscriptionV2(t *testing.T) { t.Parallel() // Exception scenario - expected := "googleapi: Error 400: Invalid Value, invalid" + expected := "googleapi: Error 404: No application was found for the given package name., applicationNotFound" client, _ := New(jsonKey) ctx := context.Background() @@ -147,7 +147,7 @@ func TestVerifySubscriptionV2(t *testing.T) { func TestVerifyProduct(t *testing.T) { t.Parallel() // Exception scenario - expected := "googleapi: Error 400: Invalid Value, invalid" + expected := "googleapi: Error 404: No application was found for the given package name., applicationNotFound" client, _ := New(jsonKey) ctx := context.Background() @@ -163,7 +163,7 @@ func TestVerifyProduct(t *testing.T) { func TestAcknowledgeProduct(t *testing.T) { t.Parallel() // Exception scenario - expected := "googleapi: Error 400: Invalid Value, invalid" + expected := "googleapi: Error 404: No application was found for the given package name., applicationNotFound" client, _ := New(jsonKey) ctx := context.Background() @@ -179,7 +179,7 @@ func TestAcknowledgeProduct(t *testing.T) { func TestConsumeProduct(t *testing.T) { t.Parallel() // Exception scenario - expected := "googleapi: Error 400: Invalid Value, invalid" + expected := "googleapi: Error 404: No application was found for the given package name., applicationNotFound" client, _ := New(jsonKey) ctx := context.Background() @@ -212,7 +212,7 @@ func TestCancelSubscription(t *testing.T) { t.Parallel() ctx := context.Background() client, _ := New(jsonKey) - expectedStr := "googleapi: Error 400: Invalid Value, invalid" + expectedStr := "googleapi: Error 404: No application was found for the given package name., applicationNotFound" actual := client.CancelSubscription(ctx, "package", "productID", "purchaseToken") if actual == nil || actual.Error() != expectedStr { From f1f464330c6b7b17214ac53efe722d46cfe341c9 Mon Sep 17 00:00:00 2001 From: Wei Zang Date: Mon, 15 May 2023 17:49:03 +0800 Subject: [PATCH 3/3] feat(appstore): add decode notification v2 body method to appstore --- appstore/cert.go | 129 ++++++++++++++++++++++++++++++++++++++++++ appstore/validator.go | 17 ++++++ 2 files changed, 146 insertions(+) create mode 100644 appstore/cert.go diff --git a/appstore/cert.go b/appstore/cert.go new file mode 100644 index 0000000..a64ade2 --- /dev/null +++ b/appstore/cert.go @@ -0,0 +1,129 @@ +package appstore + +import ( + "crypto/ecdsa" + "crypto/x509" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "strings" +) + +// rootPEM is generated through `openssl x509 -inform der -in AppleRootCA-G3.cer -out apple_root.pem` +const rootPEM = ` +-----BEGIN CERTIFICATE----- +MIICQzCCAcmgAwIBAgIILcX8iNLFS5UwCgYIKoZIzj0EAwMwZzEbMBkGA1UEAwwS +QXBwbGUgUm9vdCBDQSAtIEczMSYwJAYDVQQLDB1BcHBsZSBDZXJ0aWZpY2F0aW9u +IEF1dGhvcml0eTETMBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UEBhMCVVMwHhcN +MTQwNDMwMTgxOTA2WhcNMzkwNDMwMTgxOTA2WjBnMRswGQYDVQQDDBJBcHBsZSBS +b290IENBIC0gRzMxJjAkBgNVBAsMHUFwcGxlIENlcnRpZmljYXRpb24gQXV0aG9y +aXR5MRMwEQYDVQQKDApBcHBsZSBJbmMuMQswCQYDVQQGEwJVUzB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABJjpLz1AcqTtkyJygRMc3RCV8cWjTnHcFBbZDuWmBSp3ZHtf +TjjTuxxEtX/1H7YyYl3J6YRbTzBPEVoA/VhYDKX1DyxNB0cTddqXl5dvMVztK517 +IDvYuVTZXpmkOlEKMaNCMEAwHQYDVR0OBBYEFLuw3qFYM4iapIqZ3r6966/ayySr +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2gA +MGUCMQCD6cHEFl4aXTQY2e3v9GwOAEZLuN+yRhHFD/3meoyhpmvOwgPUnPWTxnS4 +at+qIxUCMG1mihDK1A3UT82NQz60imOlM27jbdoXt2QfyFMm+YhidDkLF1vLUagM +6BgD56KyKA== +-----END CERTIFICATE----- +` + +type Cert struct{} + +// ExtractCertByIndex extracts the certificate from the token string by index. +func (c *Cert) extractCertByIndex(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 +} + +// VerifyCert verifies the certificate chain. +func (c *Cert) verifyCert(rootCert, intermediaCert, leafCert *x509.Certificate) error { + roots := x509.NewCertPool() + ok := roots.AppendCertsFromPEM([]byte(rootPEM)) + if !ok { + return errors.New("failed to parse root certificate") + } + + intermedia := x509.NewCertPool() + intermedia.AddCert(intermediaCert) + + opts := x509.VerifyOptions{ + Roots: roots, + Intermediates: intermedia, + } + _, err := rootCert.Verify(opts) + if err != nil { + return err + } + + _, err = leafCert.Verify(opts) + if err != nil { + return err + } + + return nil +} + +func (c *Cert) ExtractPublicKeyFromToken(token string) (*ecdsa.PublicKey, error) { + rootCertBytes, err := c.extractCertByIndex(token, 2) + if err != nil { + return nil, err + } + rootCert, err := x509.ParseCertificate(rootCertBytes) + if err != nil { + return nil, fmt.Errorf("appstore failed to parse root certificate") + } + + intermediaCertBytes, err := c.extractCertByIndex(token, 1) + if err != nil { + return nil, err + } + intermediaCert, err := x509.ParseCertificate(intermediaCertBytes) + if err != nil { + return nil, fmt.Errorf("appstore failed to parse intermediate certificate") + } + + leafCertBytes, err := c.extractCertByIndex(token, 0) + if err != nil { + return nil, err + } + leafCert, err := x509.ParseCertificate(leafCertBytes) + if err != nil { + return nil, fmt.Errorf("appstore failed to parse leaf certificate") + } + if err = c.verifyCert(rootCert, intermediaCert, leafCert); err != nil { + return nil, err + } + + switch pk := leafCert.PublicKey.(type) { + case *ecdsa.PublicKey: + return pk, nil + default: + return nil, errors.New("appstore public key must be of type ecdsa.PublicKey") + } +} diff --git a/appstore/validator.go b/appstore/validator.go index ce25cb5..02303fc 100644 --- a/appstore/validator.go +++ b/appstore/validator.go @@ -9,6 +9,8 @@ import ( "io/ioutil" "net/http" "time" + + "github.com/golang-jwt/jwt/v4" ) //go:generate mockgen -destination=mocks/appstore.go -package=mocks github.com/awa/go-iap/appstore IAPClient @@ -26,6 +28,7 @@ const ( type IAPClient interface { Verify(ctx context.Context, reqBody IAPRequest, resp interface{}) error VerifyWithStatus(ctx context.Context, reqBody IAPRequest, resp interface{}) (int, error) + ParseNotificationV2(tokenStr string, result *jwt.Token) error } // Client implements IAPClient @@ -187,3 +190,17 @@ func (c *Client) parseResponse(resp *http.Response, result interface{}, ctx cont return r.Status, nil } + +// ParseNotificationV2 parse notification from App Store Server +func (c *Client) ParseNotificationV2(tokenStr string, result *jwt.Token) error { + cert := Cert{} + + result, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) { + return cert.ExtractPublicKeyFromToken(tokenStr) + }) + if err != nil { + return err + } + + return nil +}