211 lines
7.3 KiB
Go
211 lines
7.3 KiB
Go
package payment
|
|
|
|
import (
|
|
"crypto/x509"
|
|
"encoding/asn1"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
maxAppleJWSHeaderSize = 256 << 10
|
|
maxAppleJWSPayloadSize = 1 << 20
|
|
maxAppleJWSTimeDepth = 2
|
|
)
|
|
|
|
var (
|
|
appleJWSLeafExtensionOID = asn1.ObjectIdentifier{1, 2, 840, 113635, 100, 6, 11, 1}
|
|
appleJWSIntermediateExtensionOID = asn1.ObjectIdentifier{1, 2, 840, 113635, 100, 6, 2, 1}
|
|
)
|
|
|
|
// validateAppleJWSChain binds the leaf certificate used by GoPay for JWS
|
|
// signature verification to the intermediate and root certificates that
|
|
// GoPay validates against its embedded Apple trust anchor.
|
|
func validateAppleJWSChain(signedPayload string) error {
|
|
headerSegment, payloadSegment, signatureSegment, err := splitAppleJWS(signedPayload)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
verificationTime, hasVerificationTime, err := appleJWSVerificationTime(payloadSegment)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if signatureSegment == "" {
|
|
return errors.New("Apple JWS 格式无效")
|
|
}
|
|
if len(headerSegment) > base64.RawURLEncoding.EncodedLen(maxAppleJWSHeaderSize) {
|
|
return errors.New("Apple JWS header 过大")
|
|
}
|
|
headerBytes, err := base64.RawURLEncoding.DecodeString(headerSegment)
|
|
if err != nil {
|
|
return fmt.Errorf("解析 Apple JWS header: %w", err)
|
|
}
|
|
if len(headerBytes) > maxAppleJWSHeaderSize {
|
|
return errors.New("Apple JWS header 过大")
|
|
}
|
|
var header struct {
|
|
Algorithm string `json:"alg"`
|
|
Chain []string `json:"x5c"`
|
|
}
|
|
if err = json.Unmarshal(headerBytes, &header); err != nil {
|
|
return fmt.Errorf("解析 Apple JWS header: %w", err)
|
|
}
|
|
if header.Algorithm != "ES256" {
|
|
return errors.New("Apple JWS 算法必须是 ES256")
|
|
}
|
|
if len(header.Chain) != 3 {
|
|
return errors.New("Apple JWS x5c 必须包含叶子、中间和根证书")
|
|
}
|
|
|
|
certificates := make([]*x509.Certificate, 0, len(header.Chain))
|
|
for index, encoded := range header.Chain {
|
|
der, decodeErr := base64.StdEncoding.DecodeString(encoded)
|
|
if decodeErr != nil {
|
|
return fmt.Errorf("解析 Apple JWS x5c[%d]: %w", index, decodeErr)
|
|
}
|
|
certificate, parseErr := x509.ParseCertificate(der)
|
|
if parseErr != nil {
|
|
return fmt.Errorf("解析 Apple JWS x5c[%d] 证书: %w", index, parseErr)
|
|
}
|
|
certificates = append(certificates, certificate)
|
|
}
|
|
if err = certificates[0].CheckSignatureFrom(certificates[1]); err != nil {
|
|
return fmt.Errorf("Apple JWS 叶子证书不属于声明的证书链: %w", err)
|
|
}
|
|
if err = certificates[1].CheckSignatureFrom(certificates[2]); err != nil {
|
|
return fmt.Errorf("Apple JWS 中间证书不属于声明的证书链: %w", err)
|
|
}
|
|
if err = certificates[2].CheckSignatureFrom(certificates[2]); err != nil {
|
|
return fmt.Errorf("Apple JWS 根证书不是自签名证书: %w", err)
|
|
}
|
|
if !certificates[1].IsCA || !certificates[1].BasicConstraintsValid {
|
|
return errors.New("Apple JWS 中间证书不是有效 CA")
|
|
}
|
|
if !certificates[2].IsCA || !certificates[2].BasicConstraintsValid {
|
|
return errors.New("Apple JWS 根证书不是有效 CA")
|
|
}
|
|
if certificates[1].KeyUsage != 0 && certificates[1].KeyUsage&x509.KeyUsageCertSign == 0 {
|
|
return errors.New("Apple JWS 中间证书不允许签发证书")
|
|
}
|
|
if certificates[2].KeyUsage != 0 && certificates[2].KeyUsage&x509.KeyUsageCertSign == 0 {
|
|
return errors.New("Apple JWS 根证书不允许签发证书")
|
|
}
|
|
if !hasAppleCertificateExtension(certificates[0], appleJWSLeafExtensionOID) {
|
|
return errors.New("Apple JWS 叶子证书缺少 App Store 签名扩展")
|
|
}
|
|
if !hasAppleCertificateExtension(certificates[1], appleJWSIntermediateExtensionOID) {
|
|
return errors.New("Apple JWS 中间证书缺少 Apple 签名扩展")
|
|
}
|
|
|
|
if hasVerificationTime {
|
|
roots := x509.NewCertPool()
|
|
roots.AddCert(certificates[2])
|
|
intermediates := x509.NewCertPool()
|
|
intermediates.AddCert(certificates[1])
|
|
if _, err = certificates[0].Verify(x509.VerifyOptions{
|
|
Roots: roots,
|
|
Intermediates: intermediates,
|
|
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
|
|
CurrentTime: verificationTime,
|
|
}); err != nil {
|
|
return fmt.Errorf("验证 Apple JWS 证书链: %w", err)
|
|
}
|
|
}
|
|
if certificates[0].IsCA {
|
|
return errors.New("Apple JWS 叶子证书不能是 CA")
|
|
}
|
|
if certificates[0].KeyUsage != 0 && certificates[0].KeyUsage&x509.KeyUsageDigitalSignature == 0 {
|
|
return errors.New("Apple JWS 叶子证书不允许数字签名")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func splitAppleJWS(signedPayload string) (header, payload, signature string, err error) {
|
|
header, remainder, ok := strings.Cut(strings.TrimSpace(signedPayload), ".")
|
|
if !ok || header == "" {
|
|
return "", "", "", errors.New("Apple JWS 格式无效")
|
|
}
|
|
payload, signature, ok = strings.Cut(remainder, ".")
|
|
if !ok || payload == "" || signature == "" || strings.Contains(signature, ".") {
|
|
return "", "", "", errors.New("Apple JWS 格式无效")
|
|
}
|
|
return header, payload, signature, nil
|
|
}
|
|
|
|
func appleJWSVerificationTime(payloadSegment string) (time.Time, bool, error) {
|
|
return appleJWSVerificationTimeAtDepth(payloadSegment, 0)
|
|
}
|
|
|
|
// appleJWSVerificationTimeAtDepth extracts a signing time from the current
|
|
// payload, then follows the nested signed transaction/renewal JWS used by
|
|
// App Store Server Notifications. The outer notification often has no date of
|
|
// its own, while its nested transaction does.
|
|
func appleJWSVerificationTimeAtDepth(payloadSegment string, depth int) (time.Time, bool, error) {
|
|
if len(payloadSegment) > base64.RawURLEncoding.EncodedLen(maxAppleJWSPayloadSize) {
|
|
return time.Time{}, false, errors.New("Apple JWS payload 过大")
|
|
}
|
|
payloadBytes, err := base64.RawURLEncoding.DecodeString(payloadSegment)
|
|
if err != nil {
|
|
return time.Time{}, false, fmt.Errorf("解析 Apple JWS payload: %w", err)
|
|
}
|
|
if len(payloadBytes) > maxAppleJWSPayloadSize {
|
|
return time.Time{}, false, errors.New("Apple JWS payload 过大")
|
|
}
|
|
var payload struct {
|
|
SignedDate json.Number `json:"signedDate"`
|
|
ReceiptCreationDate json.Number `json:"receiptCreationDate"`
|
|
Data struct {
|
|
SignedTransactionInfo string `json:"signedTransactionInfo"`
|
|
SignedRenewalInfo string `json:"signedRenewalInfo"`
|
|
} `json:"data"`
|
|
}
|
|
if err = json.Unmarshal(payloadBytes, &payload); err != nil {
|
|
return time.Time{}, false, fmt.Errorf("解析 Apple JWS payload: %w", err)
|
|
}
|
|
date := payload.SignedDate
|
|
if date == "" {
|
|
date = payload.ReceiptCreationDate
|
|
}
|
|
if date != "" {
|
|
milliseconds, parseErr := date.Int64()
|
|
if parseErr != nil || milliseconds <= 0 {
|
|
return time.Time{}, false, errors.New("Apple JWS signedDate 无效")
|
|
}
|
|
return time.UnixMilli(milliseconds), true, nil
|
|
}
|
|
if depth >= maxAppleJWSTimeDepth {
|
|
return time.Time{}, false, nil
|
|
}
|
|
for _, nested := range []string{payload.Data.SignedTransactionInfo, payload.Data.SignedRenewalInfo} {
|
|
if strings.TrimSpace(nested) == "" {
|
|
continue
|
|
}
|
|
_, nestedPayload, _, splitErr := splitAppleJWS(nested)
|
|
if splitErr != nil {
|
|
continue
|
|
}
|
|
if nestedTime, found, nestedErr := appleJWSVerificationTimeAtDepth(nestedPayload, depth+1); nestedErr != nil {
|
|
return time.Time{}, false, nestedErr
|
|
} else if found {
|
|
return nestedTime, true, nil
|
|
}
|
|
}
|
|
return time.Time{}, false, nil
|
|
}
|
|
|
|
func hasAppleCertificateExtension(certificate *x509.Certificate, oid asn1.ObjectIdentifier) bool {
|
|
if certificate == nil {
|
|
return false
|
|
}
|
|
for _, extension := range certificate.Extensions {
|
|
if extension.Id.Equal(oid) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|