package payment import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/sha256" "crypto/x509" "crypto/x509/pkix" "encoding/asn1" "encoding/base64" "encoding/hex" "encoding/json" "math/big" "testing" "time" ) func TestValidateAppleJWSChain(t *testing.T) { chain := testAppleCertificateChain(t) jws := testAppleJWS(t, "ES256", chain, time.Now()) if err := validateAppleJWSChainWithRoots(jws, testAppleTrustedRoots(t, chain)); err != nil { t.Fatalf("validateAppleJWSChain() error = %v", err) } } func TestValidateAppleJWSChainRejectsUnrelatedLeaf(t *testing.T) { chain := testAppleCertificateChain(t) attackerChain := testAppleCertificateChain(t) chain[0] = attackerChain[0] if err := validateAppleJWSChainWithRoots(testAppleJWS(t, "ES256", chain, time.Now()), testAppleTrustedRoots(t, chain)); err == nil { t.Fatal("validateAppleJWSChain() accepted a leaf outside the declared chain") } } func TestValidateAppleJWSChainRejectsInvalidSignature(t *testing.T) { chain := testAppleCertificateChain(t) jws := testAppleJWS(t, "ES256", chain, time.Now()) header, payload, _, err := splitAppleJWS(jws) if err != nil { t.Fatal(err) } tampered := header + "." + payload + "." + base64.RawURLEncoding.EncodeToString(make([]byte, 64)) if err = validateAppleJWSChainWithRoots(tampered, testAppleTrustedRoots(t, chain)); err == nil { t.Fatal("validateAppleJWSChain accepted an invalid ES256 signature") } } func TestValidateAppleJWSChainRejectsUnexpectedAlgorithmAndShape(t *testing.T) { chain := testAppleCertificateChain(t) if err := validateAppleJWSChainWithRoots(testAppleJWS(t, "ES384", chain, time.Now()), testAppleTrustedRoots(t, chain)); err == nil { t.Fatal("validateAppleJWSChain() accepted a non-ES256 algorithm") } if err := validateAppleJWSChainWithRoots(testAppleJWS(t, "ES256", chain[:2], time.Now()), nil); err == nil { t.Fatal("validateAppleJWSChain() accepted an incomplete x5c chain") } } func TestValidateAppleJWSChainRejectsAdditionalCertificates(t *testing.T) { chain := testAppleCertificateChain(t) chain = append(chain, chain[2]) if err := validateAppleJWSChainWithRoots(testAppleJWS(t, "ES256", chain, time.Now()), testAppleTrustedRoots(t, chain[:3])); err == nil { t.Fatal("validateAppleJWSChain() accepted additional x5c certificates") } } func TestValidateAppleJWSChainRejectsNonAppleSigningCertificates(t *testing.T) { chain := testAppleCertificateChainWithoutAppleExtensions(t) if err := validateAppleJWSChainWithRoots(testAppleJWS(t, "ES256", chain, time.Now()), testAppleTrustedRoots(t, chain)); err == nil { t.Fatal("validateAppleJWSChain() accepted certificates without Apple signing extensions") } } func TestValidateAppleJWSChainUsesSignedDateForHistoricalPayload(t *testing.T) { signedAt := time.Now().AddDate(-2, 0, 0) chain := testAppleCertificateChainAt(t, signedAt, true) if err := validateAppleJWSChainWithRoots(testAppleJWS(t, "ES256", chain, signedAt), testAppleTrustedRoots(t, chain)); err != nil { t.Fatalf("validateAppleJWSChain() rejected historical payload: %v", err) } } func TestValidateAppleJWSChainUsesNestedSignedDateForNotification(t *testing.T) { signedAt := time.Now().AddDate(-2, 0, 0) chain := testAppleCertificateChainAt(t, signedAt, true) inner := testAppleJWS(t, "ES256", chain, signedAt) header, err := json.Marshal(map[string]any{"alg": "ES256", "x5c": chain}) if err != nil { t.Fatal(err) } payload, err := json.Marshal(map[string]any{ "notificationType": "DID_RENEW", "data": map[string]any{"signedTransactionInfo": inner}, }) if err != nil { t.Fatal(err) } outer := testAppleSignedJWS(t, header, payload, chain) if err := validateAppleJWSChainWithRoots(outer, testAppleTrustedRoots(t, chain)); err != nil { t.Fatalf("validateAppleJWSChain() rejected historical notification: %v", err) } } func TestValidateAppleJWSChainRejectsExpiredChainWithoutSignedDate(t *testing.T) { signedAt := time.Now().AddDate(-2, 0, 0) chain := testAppleCertificateChainAt(t, signedAt, true) if err := validateAppleJWSChainWithRoots(testAppleJWSWithoutDate(t, chain), testAppleTrustedRoots(t, chain)); err == nil { t.Fatal("validateAppleJWSChain() accepted an expired chain without signedDate") } } func TestValidateAppleJWSChainRejectsBrokenChainWithoutSignedDate(t *testing.T) { chain := testAppleCertificateChain(t) attackerChain := testAppleCertificateChain(t) chain[0] = attackerChain[0] if err := validateAppleJWSChainWithRoots(testAppleJWSWithoutDate(t, chain), testAppleTrustedRoots(t, chain)); err == nil { t.Fatal("validateAppleJWSChain() accepted a broken chain without signedDate") } } func testAppleCertificateChain(t *testing.T) []string { return testAppleCertificateChainWithAppleExtensions(t, true) } func testAppleCertificateChainWithoutAppleExtensions(t *testing.T) []string { return testAppleCertificateChainWithAppleExtensions(t, false) } func testAppleCertificateChainWithAppleExtensions(t *testing.T, includeAppleExtensions bool) []string { return testAppleCertificateChainAt(t, time.Now(), includeAppleExtensions) } func testAppleCertificateChainAt(t *testing.T, validAt time.Time, includeAppleExtensions bool) []string { t.Helper() rootKey := testAppleECDSAKey(t) rootTemplate := &x509.Certificate{ SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "test Apple root"}, NotBefore: validAt.Add(-time.Hour), NotAfter: validAt.Add(time.Hour), IsCA: true, BasicConstraintsValid: true, KeyUsage: x509.KeyUsageCertSign, } rootDER := testAppleCertificate(t, rootTemplate, rootTemplate, &rootKey.PublicKey, rootKey) root, err := x509.ParseCertificate(rootDER) if err != nil { t.Fatal(err) } intermediateKey := testAppleECDSAKey(t) intermediateTemplate := &x509.Certificate{ SerialNumber: big.NewInt(2), Subject: pkix.Name{CommonName: "test Apple intermediate"}, NotBefore: validAt.Add(-time.Hour), NotAfter: validAt.Add(time.Hour), IsCA: true, BasicConstraintsValid: true, KeyUsage: x509.KeyUsageCertSign, } if includeAppleExtensions { intermediateTemplate.ExtraExtensions = []pkix.Extension{{ Id: asn1.ObjectIdentifier{1, 2, 840, 113635, 100, 6, 2, 1}, Value: []byte{0x05, 0x00}, }} } intermediateDER := testAppleCertificate(t, intermediateTemplate, root, &intermediateKey.PublicKey, rootKey) intermediate, err := x509.ParseCertificate(intermediateDER) if err != nil { t.Fatal(err) } leafKey := testAppleECDSAKey(t) leafTemplate := &x509.Certificate{ SerialNumber: big.NewInt(3), Subject: pkix.Name{CommonName: "test Apple signing leaf"}, NotBefore: validAt.Add(-time.Hour), NotAfter: validAt.Add(time.Hour), BasicConstraintsValid: true, KeyUsage: x509.KeyUsageDigitalSignature, } if includeAppleExtensions { leafTemplate.ExtraExtensions = []pkix.Extension{{ Id: asn1.ObjectIdentifier{1, 2, 840, 113635, 100, 6, 11, 1}, Value: []byte{0x05, 0x00}, }} } leafDER := testAppleCertificate(t, leafTemplate, intermediate, &leafKey.PublicKey, intermediateKey) chain := []string{ base64.StdEncoding.EncodeToString(leafDER), base64.StdEncoding.EncodeToString(intermediateDER), base64.StdEncoding.EncodeToString(rootDER), } testAppleLeafKeys[chain[0]] = leafKey return chain } func testAppleECDSAKey(t *testing.T) *ecdsa.PrivateKey { t.Helper() key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { t.Fatal(err) } return key } func testAppleCertificate(t *testing.T, template, parent *x509.Certificate, publicKey any, signer any) []byte { t.Helper() der, err := x509.CreateCertificate(rand.Reader, template, parent, publicKey, signer) if err != nil { t.Fatal(err) } return der } func testAppleJWS(t *testing.T, algorithm string, chain []string, signedAt time.Time) string { t.Helper() header, err := json.Marshal(map[string]any{"alg": algorithm, "x5c": chain}) if err != nil { t.Fatal(err) } payload, err := json.Marshal(map[string]any{"signedDate": signedAt.UnixMilli()}) if err != nil { t.Fatal(err) } return testAppleSignedJWS(t, header, payload, chain) } func testAppleJWSWithoutDate(t *testing.T, chain []string) string { t.Helper() header, err := json.Marshal(map[string]any{"alg": "ES256", "x5c": chain}) if err != nil { t.Fatal(err) } payload, err := json.Marshal(map[string]any{"bundleId": "com.example"}) if err != nil { t.Fatal(err) } return testAppleSignedJWS(t, header, payload, chain) } var testAppleLeafKeys = map[string]*ecdsa.PrivateKey{} func testAppleSignedJWS(t *testing.T, header, payload []byte, chain []string) string { t.Helper() headerSegment := base64.RawURLEncoding.EncodeToString(header) payloadSegment := base64.RawURLEncoding.EncodeToString(payload) key := testAppleLeafKeys[chain[0]] if key == nil { t.Fatal("missing test Apple leaf key") } digest := sha256.Sum256([]byte(headerSegment + "." + payloadSegment)) r, s, err := ecdsa.Sign(rand.Reader, key, digest[:]) if err != nil { t.Fatal(err) } signature := make([]byte, 64) r.FillBytes(signature[:32]) s.FillBytes(signature[32:]) return headerSegment + "." + payloadSegment + "." + base64.RawURLEncoding.EncodeToString(signature) } func testAppleTrustedRoots(t *testing.T, chain []string) map[string]struct{} { t.Helper() rootDER, err := base64.StdEncoding.DecodeString(chain[2]) if err != nil { t.Fatal(err) } fingerprint := sha256.Sum256(rootDER) return map[string]struct{}{hex.EncodeToString(fingerprint[:]): {}} }