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

4

我正在尝试使用应用商店通知来设置我的服务器,这样当用户退款其应用内购买时,我可以收到通知。 https://developer.apple.com/documentation/appstoreservernotifications/receiving_app_store_server_notifications <- 这是我现在查看的指南。

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中保存一个已签名的payload代码。https://gist.github.com/atpons/5279af568cb7d1b101247c02f0a022af <- 想象代码看起来会像这样。
所以我的问题是,
我需要制作一些新的私钥并与服务器开发人员共享吗? 看起来我们从这里存储密钥 https://www.apple.com/certificateauthority/ 并在请求时使用它? 我该如何收到通知? 我应该期望这种类型的通知期望响应JSON结构将发送到与我分享的App Store Connect的URL。 感谢您阅读我的问题!
3个回答

3

我按照以下步骤进行:

  1. 获取苹果根证书:wget https://www.apple.com/certificateauthority/AppleRootCA-G3.cer
  2. 创建pem文件:openssl x509 -inform der -in ./AppleRootCA-G3.cer -out ./apple_root.pem
  3. 运行代码:

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)


我有一个问题。为什么忽略JWT的第三部分?我们不应该验证整个JWT的签名吗? - undefined

1

现在,我可能会花一天的时间来研究它,但是最终我在一堆Java和Ruby代码片段之间找到并编写了一个可用的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);
}


4
太好了,但是你能添加所有使用的导入吗,比如X509Certificate,appleJWTService。苹果喜欢把事情搞得超级复杂。他们本可以在各种语言中发布参考实现。 - Nathan B

0

以下是解析App Store服务器通知V2的步骤:

  • 从JWS令牌中提取头部
  • 使用应用商店密钥验证头部
  • 从令牌中提取公钥以解析有效载荷数据
  • 准备结构以绑定通知
  • 解析有效载荷并将其绑定到结构中

还有使用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")
    }
}

用法示例

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

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


更多详情请参考https://github.com/richzw/appstore


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接