package apple import ( "bytes" "crypto/ecdsa" "crypto/rand" "crypto/sha256" "crypto/x509" "encoding/base64" "encoding/json" "encoding/pem" "fmt" "net/http" "time" ) type ServerAPIConfig struct { KeyID string IssuerID string PrivateKey string Sandbox bool BundleID string } func buildAPIToken(cfg ServerAPIConfig) (string, error) { header := map[string]interface{}{ "alg": "ES256", "kid": cfg.KeyID, "typ": "JWT", } now := time.Now().Unix() payload := map[string]interface{}{ "iss": cfg.IssuerID, "iat": now, "exp": now + 1800, "aud": "appstoreconnect-v1", } if cfg.BundleID != "" { payload["bid"] = cfg.BundleID } hb, _ := json.Marshal(header) pb, _ := json.Marshal(payload) enc := func(b []byte) string { return base64.RawURLEncoding.EncodeToString(b) } unsigned := fmt.Sprintf("%s.%s", enc(hb), enc(pb)) block, _ := pem.Decode([]byte(cfg.PrivateKey)) if block == nil { return "", fmt.Errorf("invalid private key") } keyAny, err := x509.ParsePKCS8PrivateKey(block.Bytes) if err != nil { return "", err } priv, ok := keyAny.(*ecdsa.PrivateKey) if !ok { return "", fmt.Errorf("private key is not ECDSA") } // Correctly calculate R and S for ES256 (P-256 + SHA-256) digest := sha256Sum([]byte(unsigned)) r, s, err := ecdsa.Sign(rand.Reader, priv, digest) if err != nil { return "", err } // Concatenate R and S (each 32 bytes for P-256) curveBits := priv.Curve.Params().BitSize keyBytes := curveBits / 8 if curveBits%8 > 0 { keyBytes += 1 } rBytes := r.Bytes() rBytesPadded := make([]byte, keyBytes) copy(rBytesPadded[keyBytes-len(rBytes):], rBytes) sBytes := s.Bytes() sBytesPadded := make([]byte, keyBytes) copy(sBytesPadded[keyBytes-len(sBytes):], sBytes) sig := append(rBytesPadded, sBytesPadded...) return unsigned + "." + base64.RawURLEncoding.EncodeToString(sig), nil } func sha256Sum(b []byte) []byte { h := sha256.New() h.Write(b) return h.Sum(nil) } func GetTransactionInfo(cfg ServerAPIConfig, transactionId string) (string, error) { token, err := buildAPIToken(cfg) if err != nil { return "", err } try := func(host string) (string, int, string, error) { url := fmt.Sprintf("%s/inApps/v1/transactions/%s", host, transactionId) req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { return "", 0, "", err } defer resp.Body.Close() buf := new(bytes.Buffer) _, _ = buf.ReadFrom(resp.Body) if resp.StatusCode != 200 { return "", resp.StatusCode, buf.String(), fmt.Errorf("apple api error: %d", resp.StatusCode) } var body struct { SignedTransactionInfo string `json:"signedTransactionInfo"` } if err := json.Unmarshal(buf.Bytes(), &body); err != nil { return "", resp.StatusCode, buf.String(), err } return body.SignedTransactionInfo, resp.StatusCode, buf.String(), nil } primary := "https://api.storekit.itunes.apple.com" secondary := "https://api.storekit-sandbox.itunes.apple.com" if cfg.Sandbox { primary, secondary = secondary, primary } jws, code, body, err := try(primary) if err == nil && jws != "" { return jws, nil } // Fallback to the other environment if primary failed (common when env mismatches) jws2, code2, body2, err2 := try(secondary) if err2 == nil && jws2 != "" { return jws2, nil } return "", fmt.Errorf("apple api error, primary[%d:%s], secondary[%d:%s]", code, body, code2, body2) }