AppStore服务器通知设置[接收App Store服务器通知版本2]

发布于 2025-02-06 23:17:31 字数 1619 浏览 3 评论 0原文

我正在尝试使用AppStore通知来设置服务器。 因此,当用户退还其应用内购买时,我可以收到通知。 https://developer.apple.com/documentation/appstoreservernotifications/receiving_app_store_server_notifications <- guide我现在正在寻找的行。

The version 2 response body, responseBodyV2, contains a signedPayload that’s cryptographically signed by the App Store in JSON Web Signature (JWS) format. The JWS format increases security and enables you to decode and validate the signature on your server. The notification data contains transaction and subscription renewal information that the App Store signs in JWS. The App Store Server API and the StoreKit In-App Purchase API use the same JWS-signed format for transaction and subscription status information. For more information about JWS, see the IETF RFC 7515 specification.

根据文章,似乎我必须在我与App Store Connect共享的URL中持有签名的Pay代码。 “ noreferrer”> https://github.github.com/atpons/atpons/atpons/52799af568cb768cb7d124cb7d124c.24c.1b7d4c.1b7d4c.1 af4c &lt ;-思考代码看起来像这样
所以我的问题是,

我需要制作一些新的私钥并与服务器开发人员共享吗? 看起来我们从这里存储了钥匙 https://wwwww.apple.com/certificateauthority/ 并在我们要求时使用它? 我如何收到通知? 我应该期待的 预期响应json结构 这种通知将出现在我与App Store Connect共享的URL上。 感谢您阅读我的问题!

I am trying to set up my server with appstore notification.
So that I can get notification when users refund their in-app-purchase.
https://developer.apple.com/documentation/appstoreservernotifications/receiving_app_store_server_notifications <- guide line that I am looking now.

The version 2 response body, responseBodyV2, contains a signedPayload that’s cryptographically signed by the App Store in JSON Web Signature (JWS) format. The JWS format increases security and enables you to decode and validate the signature on your server. The notification data contains transaction and subscription renewal information that the App Store signs in JWS. The App Store Server API and the StoreKit In-App Purchase API use the same JWS-signed format for transaction and subscription status information. For more information about JWS, see the IETF RFC 7515 specification.

according to article, seem like I have to hold a signedpayload code inside a url that I am shared with my App Store Connect.
https://gist.github.com/atpons/5279af568cb7d1b101247c02f0a022af
<- thinking code would be look like this
So my question is,

Do I need to make some new private key and share with server developers ?
look like we store the key from here https://www.apple.com/certificateauthority/
and use it whenever we request?
how do I get notification?
should I just expect that
expected response json structure
this kind of notification will come to url that I am shared with my App Store Connect.
Thank you for reading my question!

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

云裳 2025-02-13 23:17:31

我按照步骤进行操作:

  1. 获取Apple root证书:wget https://wwww.apple。 com/periferateAuthority/applerootca -g3.cer
  2. 创建PEM文件:openssl x509 -inform der -in ./applerootca-g3.cer -out -out ./apple_root.pem.pem
  3. run代码:
import { X509Certificate } from 'crypto'
import fs from 'fs'
import jwt from 'jsonwebtoken'

// parameter
const signedPayloadFile = 'path to signedPayload file, ex: /home/vannguyen/signedPayload.txt'
const appleRootPemFile = 'path to pem file in step 2, ex: /home/vannguyen/apple_root.pem'
// end

const signedPayload = fs.readFileSync(signedPayloadFile).toString()

const decodeToken = (token, segment) => {
  const tokenDecodablePart = token.split('.')[segment]
  const decoded = Buffer.from(tokenDecodablePart, 'base64').toString()
  return decoded
}

const { alg, x5c } = JSON.parse(decodeToken(signedPayload, 0))

const x5cCertificates = x5c.map(
  (header) => new X509Certificate(Buffer.from(header, 'base64'))
)
const appleRootCertificate = new X509Certificate(
  fs.readFileSync(appleRootPemFile)
)

const checkIssued = appleRootCertificate.checkIssued(
  x5cCertificates[x5cCertificates.length - 1]
)
if (!checkIssued) {
  throw new Error('Invalid token')
}

x5cCertificates.push(appleRootCertificate)

const verifierStatuses = x5cCertificates.map((x590, index) => {
  if (index >= x5cCertificates.length - 1) return true
  return x590.verify(x5cCertificates[index + 1].publicKey)
})
if (verifierStatuses.includes(false)) {
  throw new Error('Invalid token')
}
const { publicKey } = x5cCertificates[0]
const payload = JSON.parse(decodeToken(signedPayload, 1))

const transactionInfo = jwt.verify(
  payload.data.signedTransactionInfo,
  publicKey,
  {
    algorithms: alg
  }
)

console.log('transactionInfo: ', transactionInfo)

const transactionRenewalInfo = jwt.verify(
  payload.data.signedRenewalInfo,
  publicKey,
  {
    algorithms: alg
  }
)

console.log('transactionRenewalInfo: ', transactionRenewalInfo)

I'm follow by steps:

  1. Get Apple Root Certificates: wget https://www.apple.com/certificateauthority/AppleRootCA-G3.cer
  2. Create pem file: openssl x509 -inform der -in ./AppleRootCA-G3.cer -out ./apple_root.pem
  3. Run code:

import { X509Certificate } from 'crypto'
import fs from 'fs'
import jwt from 'jsonwebtoken'

// parameter
const signedPayloadFile = 'path to signedPayload file, ex: /home/vannguyen/signedPayload.txt'
const appleRootPemFile = 'path to pem file in step 2, ex: /home/vannguyen/apple_root.pem'
// end

const signedPayload = fs.readFileSync(signedPayloadFile).toString()

const decodeToken = (token, segment) => {
  const tokenDecodablePart = token.split('.')[segment]
  const decoded = Buffer.from(tokenDecodablePart, 'base64').toString()
  return decoded
}

const { alg, x5c } = JSON.parse(decodeToken(signedPayload, 0))

const x5cCertificates = x5c.map(
  (header) => new X509Certificate(Buffer.from(header, 'base64'))
)
const appleRootCertificate = new X509Certificate(
  fs.readFileSync(appleRootPemFile)
)

const checkIssued = appleRootCertificate.checkIssued(
  x5cCertificates[x5cCertificates.length - 1]
)
if (!checkIssued) {
  throw new Error('Invalid token')
}

x5cCertificates.push(appleRootCertificate)

const verifierStatuses = x5cCertificates.map((x590, index) => {
  if (index >= x5cCertificates.length - 1) return true
  return x590.verify(x5cCertificates[index + 1].publicKey)
})
if (verifierStatuses.includes(false)) {
  throw new Error('Invalid token')
}
const { publicKey } = x5cCertificates[0]
const payload = JSON.parse(decodeToken(signedPayload, 1))

const transactionInfo = jwt.verify(
  payload.data.signedTransactionInfo,
  publicKey,
  {
    algorithms: alg
  }
)

console.log('transactionInfo: ', transactionInfo)

const transactionRenewalInfo = jwt.verify(
  payload.data.signedRenewalInfo,
  publicKey,
  {
    algorithms: alg
  }
)

console.log('transactionRenewalInfo: ', transactionRenewalInfo)

肥爪爪 2025-02-13 23:17:31

好了,我可能会花一天的时间在上面,但是我发现并在一堆Java和Ruby Code片段之间撰写了一个工作的Nodejs代码。希望别人可以从中受益。

async processAppleNotification(signedPayload: string) {
  // Here we start with importing Apple's precious Root Certificate
  // With help of NodeJS crypto's X509Certificate constructor which only works after Node15.X but to make sure I'm using Node16.17.0
  const appleRootCertificate = new X509Certificate(
    fs.readFileSync(
      path.join(__dirname, '../../../src/assets/AppleRootCAG3.cer'),
    ),
  );

  // Decode payload unsafely, we need to get header base64 values
  let decodedPayload = await this.appleJWTService.decode(signedPayload, {
    complete: true,
  });
  if (typeof decodedPayload === 'string') {
    // Just to make sure, if decode has return string
    decodedPayload = JSON.parse(decodedPayload);
  }

  const decodedHeaders = decodedPayload['header'];
  const x5cHeaders = decodedHeaders['x5c'];
  // Map all the x5c header array values, and get them as Base64 Decoded Buffer and create X509 Cert.
  const decodedX5CHeaders: X509Certificate[] = x5cHeaders.map((_header) => {
    return new X509Certificate(Buffer.from(_header, 'base64'));
  });

  // We already know the last certificate which we receive in x5c header is AppleRootCertificate
  if (!appleRootCertificate.checkIssued(
      decodedX5CHeaders[decodedX5CHeaders.length - 1],
    )) {
    throw new UnauthorizedException();
  }
  decodedX5CHeaders.push(appleRootCertificate);

  // Let's verify all the chain together, if there is any corrupted certificate
  const verificationStatuses = [];
  decodedX5CHeaders.forEach((_header, index) => {
    if (index >= decodedX5CHeaders.length - 1) {
      return;
    }
    verificationStatuses.push(
      // Verify function returns boolean
      _header.verify(decodedX5CHeaders[index + 1].publicKey),
    );
  });

  // Check the status array, if everything is okey
  if (verificationStatuses.includes(false)) {
    throw new UnauthorizedException();
  }

  // This part is a bit Critical one, I couldn't find another way to convert X509 public key to string so here we are
  // Library name is 'jwk-to-pem', for TS please use with import * as JWKPM import statement
  const publicKeyToPEM = JWKtoPem(
    decodedX5CHeaders[0].publicKey.export({
      format: 'jwk'
    }),
  );
  const verifiedPayload = await this.appleJWTService.verify(signedPayload, {
    algorithms: [decodedHeaders.alg],
    publicKey: publicKeyToPEM,
  });
  // Here we go, all validated and have the actual payload as validated
  console.log(verifiedPayload);
}

Well now, I might spend a day on it but I figured out and composed a working NodeJS code finally between a bunch of Java and Ruby code snippets. Hope someone else can get benefit from it.

async processAppleNotification(signedPayload: string) {
  // Here we start with importing Apple's precious Root Certificate
  // With help of NodeJS crypto's X509Certificate constructor which only works after Node15.X but to make sure I'm using Node16.17.0
  const appleRootCertificate = new X509Certificate(
    fs.readFileSync(
      path.join(__dirname, '../../../src/assets/AppleRootCAG3.cer'),
    ),
  );

  // Decode payload unsafely, we need to get header base64 values
  let decodedPayload = await this.appleJWTService.decode(signedPayload, {
    complete: true,
  });
  if (typeof decodedPayload === 'string') {
    // Just to make sure, if decode has return string
    decodedPayload = JSON.parse(decodedPayload);
  }

  const decodedHeaders = decodedPayload['header'];
  const x5cHeaders = decodedHeaders['x5c'];
  // Map all the x5c header array values, and get them as Base64 Decoded Buffer and create X509 Cert.
  const decodedX5CHeaders: X509Certificate[] = x5cHeaders.map((_header) => {
    return new X509Certificate(Buffer.from(_header, 'base64'));
  });

  // We already know the last certificate which we receive in x5c header is AppleRootCertificate
  if (!appleRootCertificate.checkIssued(
      decodedX5CHeaders[decodedX5CHeaders.length - 1],
    )) {
    throw new UnauthorizedException();
  }
  decodedX5CHeaders.push(appleRootCertificate);

  // Let's verify all the chain together, if there is any corrupted certificate
  const verificationStatuses = [];
  decodedX5CHeaders.forEach((_header, index) => {
    if (index >= decodedX5CHeaders.length - 1) {
      return;
    }
    verificationStatuses.push(
      // Verify function returns boolean
      _header.verify(decodedX5CHeaders[index + 1].publicKey),
    );
  });

  // Check the status array, if everything is okey
  if (verificationStatuses.includes(false)) {
    throw new UnauthorizedException();
  }

  // This part is a bit Critical one, I couldn't find another way to convert X509 public key to string so here we are
  // Library name is 'jwk-to-pem', for TS please use with import * as JWKPM import statement
  const publicKeyToPEM = JWKtoPem(
    decodedX5CHeaders[0].publicKey.export({
      format: 'jwk'
    }),
  );
  const verifiedPayload = await this.appleJWTService.verify(signedPayload, {
    algorithms: [decodedHeaders.alg],
    publicKey: publicKeyToPEM,
  });
  // Here we go, all validated and have the actual payload as validated
  console.log(verifiedPayload);
}

憧憬巴黎街头的黎明 2025-02-13 23:17:31

以下是解析App Store Server通知的步骤V2

  • 从JWS代币
  • 验证标头中使用App Store键
  • 验证标头从令牌中提取公共密钥从令牌到解析有效载荷数据
  • 准备结构的结构以绑定通知
  • 解除有效载荷并将其绑定到结构

和示例代码

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

有关更多详细信息

payload := &NotificationPayload{}
cert := Cert{}

_, err = jwt.ParseWithClaims(tokenStr, payload, func(token *jwt.Token) (interface{}, error) {
        return cert.ExtractPublicKeyFromToken(tokenStr)
})


,请参阅 https://github.com/richzw/richzw/appstore

Here are the steps to parse App Store Server Notifications V2

  • Extract header from JWS token
  • Verify header with an app store key
  • Extract the public key from the token to parse payload data
  • Prepare structures to bind notification
  • Parse payload and bind it to structures

And the sample codes with Golang

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

Usage Sample

payload := &NotificationPayload{}
cert := Cert{}

_, err = jwt.ParseWithClaims(tokenStr, payload, func(token *jwt.Token) (interface{}, error) {
        return cert.ExtractPublicKeyFromToken(tokenStr)
})


For more details, please refer to https://github.com/richzw/appstore

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文