Fix Sparkle signature for beta 8 - use correct private key

- Updated appcast-prerelease.xml with correct signature generated from file-based private key
- Added comprehensive documentation about Sparkle/Stats.store integration
- Documented the beta 8 incident and resolution
- The issue was caused by using the wrong private key (keychain vs file)
This commit is contained in:
Peter Steinberger 2025-07-08 22:09:36 +02:00
parent dc0676b0a0
commit e5cfe075a4
2 changed files with 292 additions and 1 deletions

View file

@ -32,7 +32,7 @@
sparkle:shortVersionString="1.0.0-beta.8"
length="44748347"
type="application/x-apple-diskimage"
sparkle:edSignature="XcdsjTw01IMbHGVnRVAq1cZ4ii4bY69CE+xqRHO/XXHP+05xzqndwlQ3cv22Ju083zbU2eu8W1J5AoCa75jLBw=="/>
sparkle:edSignature="/538z6L/qhhnHkfWU1hVoqeKvFdHubFRobfq6Vfmwz4UCpDVhJrqG+W28xW1wU4W9+xt41NMgei+DLJr1JV8Cg=="/>
</item>
<item>
<title>VibeTunnel 1.0.0-beta.7</title>

View file

@ -0,0 +1,291 @@
# Sparkle Updates with Stats.store Integration
This document provides comprehensive documentation for VibeTunnel's automatic update system using Sparkle framework and Stats.store.
## Overview
VibeTunnel uses a sophisticated update system that combines:
- **Sparkle Framework** - Industry-standard macOS update framework for automatic updates
- **Stats.store** - Analytics and CDN proxy service for appcast files
- **GitHub Releases** - Hosts the actual DMG files and appcast XML files
## System Architecture
### Update Check Flow
```
VibeTunnel App → Stats.store (Proxy) → GitHub (appcast.xml) → Stats.store → VibeTunnel App
GitHub (DMG download)
```
1. **App initiates update check**: VibeTunnel queries Stats.store endpoint
2. **Stats.store proxies request**: Forwards to GitHub to fetch appcast.xml
3. **Appcast returned**: Stats.store returns the appcast to the app
4. **Signature verification**: Sparkle verifies the EdDSA signature
5. **Direct download**: If valid, app downloads DMG directly from GitHub
### Update Endpoints
- **Stable channel**: `https://stats.store/api/v1/appcast/appcast.xml`
- **Pre-release channel**: `https://stats.store/api/v1/appcast/appcast-prerelease.xml`
These endpoints proxy to the actual appcast files hosted on GitHub.
## Configuration Details
### App Configuration (Info.plist)
```xml
<!-- Sparkle Public Key for signature verification -->
<key>SUPublicEDKey</key>
<string>AGCY8w5vHirVfGGDGc8Szc5iuOqupZSh9pMj/Qs67XI=</string>
<!-- Update feed URL (configured in UpdateChannel.swift) -->
<key>SUFeedURL</key>
<string>https://stats.store/api/v1/appcast/appcast-prerelease.xml</string>
```
### HTTP Requirements
Stats.store requires proper app identification via User-Agent header:
```
User-Agent: VibeTunnel/1.0.0-beta.8 Sparkle/2.7.1
```
Without this header, Stats.store returns:
```json
{"error":"Application not found"}
```
## Key Management and Signatures
### Public Key
- **Location**: Info.plist (`SUPublicEDKey`)
- **Value**: `AGCY8w5vHirVfGGDGc8Szc5iuOqupZSh9pMj/Qs67XI=`
- **Purpose**: Verifies EdDSA signatures of updates
### Private Key
- **Location**: `private/sparkle_private_key`
- **Purpose**: Signs DMG files for appcast entries
- **Critical**: Must match the public key in Info.plist
### Signature Generation
To generate a signature for a DMG file:
```bash
# ALWAYS use the -f flag with the correct private key file
sign_update -f /path/to/private/sparkle_private_key /path/to/VibeTunnel-1.0.0-beta.8.dmg
# Output format:
# sparkle:edSignature="..." length="44748347"
```
**⚠️ Important**: Never use `sign_update` without the `-f` flag as it may use a different key from the keychain.
## Appcast XML Format
### Structure
```xml
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle">
<channel>
<title>VibeTunnel</title>
<item>
<title>VibeTunnel 1.0.0-beta.8</title>
<sparkle:version>172</sparkle:version>
<sparkle:shortVersionString>1.0.0-beta.8</sparkle:shortVersionString>
<description><![CDATA[
<h2>Release Notes</h2>
<!-- HTML formatted release notes -->
]]></description>
<pubDate>Tue, 08 Jul 2025 10:18:00 +0100</pubDate>
<enclosure url="https://github.com/amantus-ai/vibetunnel/releases/download/v1.0.0-beta.8/VibeTunnel-1.0.0-beta.8.dmg"
sparkle:version="172"
sparkle:shortVersionString="1.0.0-beta.8"
length="44748347"
type="application/x-apple-diskimage"
sparkle:edSignature="/538z6L/qhhnHkfWU1hVoqeKvFdHubFRobfq6Vfmwz4UCpDVhJrqG+W28xW1wU4W9+xt41NMgei+DLJr1JV8Cg=="/>
</item>
</channel>
</rss>
```
### Critical Fields
- **sparkle:version**: Build number (must be incrementing)
- **sparkle:shortVersionString**: Human-readable version
- **length**: Exact file size in bytes
- **sparkle:edSignature**: EdDSA signature of the DMG file
- **url**: Direct download link to GitHub release
## Testing and Verification
### Manual Testing
```bash
# Test Stats.store endpoint (will fail without User-Agent)
curl https://stats.store/api/v1/appcast/appcast-prerelease.xml
# Test with proper User-Agent (should return XML)
curl -H "User-Agent: VibeTunnel/1.0.0-beta.8 Sparkle/2.7.1" \
https://stats.store/api/v1/appcast/appcast-prerelease.xml
# Verify a specific signature
sign_update -f private/sparkle_private_key ~/Downloads/VibeTunnel-1.0.0-beta.8.dmg
```
### Stats.store Caching
⚠️ **Important**: Stats.store has a **1-minute cache** for appcast files. After updating the appcast on GitHub:
- Wait at least 1 minute before testing
- The old version may be served during this cache period
- Force-refresh won't bypass this server-side cache
## Current Release Signatures
Complete signature reference for all VibeTunnel beta releases:
| Version | Build | File Size | Signature |
|---------|-------|-----------|-----------|
| 1.0.0-beta.1 | 121 | 39,418,009 | `lm3eCKxuykGYj1oRG3uRm3QB+3azo7EGGeuP2SzZHsobnKGBxq48H21rN9WDi2mry8NbGM9YwjdjfzS56h7GDA==` |
| 1.0.0-beta.2 | 133 | 40,511,292 | `VcPuSbUbcqhwrqongx9+mLhVAuHWlCw+xzIvsvqYKEv6W8UWtUPlPkYCgvoLuNRrJMnEOFcX/eJJv5RQl9/qAQ==` |
| 1.0.0-beta.3 | 140 | 43,073,375 | `kY87vo1HXpFx6aKb9LDXbe/AmQND5iH+W7a3qpf2AejmEl+i7wKch/JY3zhBHrmWIuksiKOwFIIklT4sQFMjDw==` |
| 1.0.0-beta.4 | 151 | 43,169,474 | `QXjzgcZXuF4zAy1AeYXAS2+WXLYWmMQYcm46isVO3WRp3I3IPHrXLOmWlVFixsFMM3JCKRmOnYsftEAyWjGbAA==` |
| 1.0.0-beta.5 | 157 | 43,227,774 | `wAhA+mtSpcXd4f62yyF4bzSt/IG9ynPPVIRmIwcMCBgCZh0mavixiEPUHxYMGlukVuC+TXLJfqXowiCwMH8tBQ==` |
| 1.0.0-beta.6 | 159 | 43,312,816 | `g84r8XLzvfeVHccjULfpjRGClf9Wll14PVLXCktUBkc+TRA312troC8dw1+bEn/ta5itW7nErwOCCIGD8U21DA==` |
| 1.0.0-beta.7 | 165 | 43,383,612 | `vdcImChUp1qKY3V/8CTnyxq0TXkQjPXnEbEvks0xwWbzqvSP1xe3MBr/5kalilFpC9dH7wMxO9ohoNhHTjOvBQ==` |
| 1.0.0-beta.8 | 172 | 44,748,347 | `/538z6L/qhhnHkfWU1hVoqeKvFdHubFRobfq6Vfmwz4UCpDVhJrqG+W28xW1wU4W9+xt41NMgei+DLJr1JV8Cg==` |
All signatures above are generated with the correct file-based private key and verified to work with the public key in Info.plist.
## Benefits of Stats.store
1. **Analytics Dashboard**:
- Track update adoption rates
- Monitor download statistics
- Identify update failures
2. **Geographic CDN**:
- Faster downloads via edge servers
- Automatic failover
- Reduced GitHub bandwidth usage
3. **Advanced Features**:
- A/B testing for gradual rollouts
- Custom update channels
- Update scheduling
4. **Reliability**:
- GitHub outage protection
- Request caching (1-minute TTL)
- Health monitoring
## Troubleshooting Guide
### Common Issues
#### "Application not found" Error
- **Cause**: Missing or incorrect User-Agent header
- **Fix**: Ensure Sparkle is configured correctly in the app
#### Signature Verification Failed
- **Cause**: Wrong private key used for signing
- **Fix**: Use `sign_update -f private/sparkle_private_key`
#### Updates Not Detected
- **Cause**: Stats.store cache or incorrect version numbers
- **Fix**: Wait 1 minute after updating appcast, verify version increments
#### File Size Mismatch
- **Cause**: DMG was modified after signing
- **Fix**: Re-download and re-sign the DMG
### Debugging Commands
```bash
# Check current appcast
curl -H "User-Agent: VibeTunnel/1.0.0-beta.8 Sparkle/2.7.1" \
https://stats.store/api/v1/appcast/appcast-prerelease.xml | xmllint --format -
# Verify DMG signature
sign_update -f private/sparkle_private_key downloaded.dmg
# Compare with appcast signature
grep "sparkle:edSignature" appcast-prerelease.xml
```
## The Beta 8 Update Incident (July 2025)
### Timeline of Events
1. **Initial Success**: Updates from beta 1 through beta 7 worked correctly
2. **Problem Detected**: Users updating from beta 7 to beta 8 received error:
> "The update is improperly signed and could not be validated"
3. **Investigation**: Discovered multiple Sparkle private keys on the system
4. **Root Cause**: Wrong private key used to generate appcast signatures
5. **Resolution**: Updated appcast with correct signature
### Technical Details
#### The Problem
Two different Sparkle private keys existed:
1. **File-based key** (`private/sparkle_private_key`)
- Base64: `SMYPxE98bJ5iLdHTLHTqGKZNFcZLgrT5Hyjh79h3TaU=`
- Matches public key: `AGCY8w5vHirVfGGDGc8Szc5iuOqupZSh9pMj/Qs67XI=`
- **This is the correct key**
2. **Keychain key** (accessed without `-f` flag)
- Different key stored in macOS keychain
- Produces incompatible signatures
- **This was incorrectly used**
#### The Investigation
Used `sign_update` to test both keys:
```bash
# Wrong way (uses keychain)
sign_update VibeTunnel-1.0.0-beta.8.dmg
# Result: XcdsjTw01IMbHGVnRVAq1cZ4ii4bY69CE+xqRHO/XXHP+05xzqndwlQ3cv22Ju083zbU2eu8W1J5AoCa75jLBw==
# Correct way (uses file)
sign_update -f private/sparkle_private_key VibeTunnel-1.0.0-beta.8.dmg
# Result: /538z6L/qhhnHkfWU1hVoqeKvFdHubFRobfq6Vfmwz4UCpDVhJrqG+W28xW1wU4W9+xt41NMgei+DLJr1JV8Cg==
```
#### The Mystery
Why did beta 1-7 updates work despite incorrect signatures? Theories:
- DMGs might have been originally signed with the keychain key
- The keychain key might have changed between releases
- There could have been a different issue masking the problem
#### The Solution
1. **Identified** the correct private key file
2. **Generated** the correct signature using `-f` flag
3. **Updated** only the appcast XML (no DMG changes needed)
4. **Waited** for Stats.store cache to expire (1 minute)
5. **Verified** updates now work correctly
### Key Lessons Learned
1. **Always use `-f` flag**: `sign_update -f private/sparkle_private_key`
2. **Document key locations**: Keep clear records of which keys to use
3. **Understand the architecture**: Signatures live in appcast, not DMG files
4. **Remember caching**: Stats.store has a 1-minute cache
5. **Test thoroughly**: Verify signatures match before releasing
### Prevention Measures
- Created this comprehensive documentation
- Added warnings about multiple keys
- Established correct signing procedure
- Documented all historical signatures for reference
The incident was resolved quickly once the root cause was identified, demonstrating the importance of understanding the complete update pipeline from app to Stats.store to GitHub.