diff --git a/.github/workflows/docker-test.yml b/.github/workflows/docker-test.yml index 3bfb674..1af6add 100644 --- a/.github/workflows/docker-test.yml +++ b/.github/workflows/docker-test.yml @@ -3,13 +3,13 @@ name: Docker Script Testing on: pull_request: paths: - - 'run-docker.sh' + - 'scripts/run-docker.sh' - 'Dockerfile' - '.github/workflows/docker-test.yml' push: branches: [ main ] paths: - - 'run-docker.sh' + - 'scripts/run-docker.sh' - 'Dockerfile' workflow_dispatch: @@ -30,12 +30,12 @@ jobs: - name: Make scripts executable run: | - chmod +x run-docker.sh - chmod +x cleanup.sh + chmod +x scripts/run-docker.sh + chmod +x scripts/cleanup.sh - name: Test Docker script - Build run: | - ./run-docker.sh build + ./scripts/run-docker.sh build - name: Verify Docker image exists run: | @@ -47,27 +47,27 @@ jobs: - name: Test Docker script - Help run: | - ./run-docker.sh --help + ./scripts/run-docker.sh --help - name: Test Docker script - Simulation mode run: | - ./run-docker.sh -m simulation -s BTC-USD & + ./scripts/run-docker.sh -m simulation -s BTC-USD & sleep 10 - name: Test Docker script - Logs run: | - timeout 10s ./run-docker.sh logs || true + timeout 10s ./scripts/run-docker.sh logs || true - name: Test Docker script - Stop run: | - ./run-docker.sh stop || true + ./scripts/run-docker.sh stop || true - name: Test Docker script - Custom parameters run: | - ./run-docker.sh -s ETH-USD -v & + ./scripts/run-docker.sh -s ETH-USD -v & sleep 5 - ./run-docker.sh stop || true + ./scripts/run-docker.sh stop || true - name: Clean up Docker resources run: | - ./run-docker.sh clean || true + ./scripts/run-docker.sh clean || true diff --git a/.github/workflows/pr-title-lint.yml b/.github/workflows/pr-title-lint.yml index 7e7f2f4..c1e0903 100644 --- a/.github/workflows/pr-title-lint.yml +++ b/.github/workflows/pr-title-lint.yml @@ -36,5 +36,5 @@ jobs: didn't match the configured pattern. Please ensure that the subject doesn't start with an uppercase character. # Validate that the PR title length is not too long - headerPattern: '^(\w+)(?:\([\w\-\.]+\))?: .{1,100}$' + headerPattern: '^(\w+)(?:\(([\w\-\.]+)\))?: (.{1,100})$' headerPatternCorrespondence: type, scope, subject diff --git a/CMakeLists.txt b/CMakeLists.txt index efe5163..03b5d13 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -409,6 +409,18 @@ if(BUILD_TESTS) GTest::gtest Threads::Threads) add_test(NAME BacktestEngineTests COMMAND backtest_engine_tests) + # Certificate Pinner tests - Tests certificate pinning and validation + add_executable(certificate_pinner_tests tests/unit/CertificatePinnerTest.cpp) + target_link_libraries( + certificate_pinner_tests + core + GTest::gtest_main + GTest::gtest + Threads::Threads + OpenSSL::SSL + OpenSSL::Crypto) + add_test(NAME CertificatePinnerTests COMMAND certificate_pinner_tests) + # FIX Protocol tests - Tests factory patterns, configuration, and core FIX # integration add_executable(fix_basic_test tests/fix_basic_test.cpp) diff --git a/README.md b/README.md index f5e04d9..f503c76 100644 --- a/README.md +++ b/README.md @@ -104,20 +104,20 @@ git clone https://github.com/chizy7/PinnacleMM.git cd PinnacleMM # One-command setup and run -./run-native.sh # Simulation mode (auto-builds if needed) -./run-native.sh -m live -v # Live trading with verbose logs -./run-native.sh --enable-ml # ML-enhanced simulation mode -./run-native.sh --enable-visualization # With real-time dashboard -./run-native.sh --setup-credentials # Configure API keys +scripts/run-native.sh # Simulation mode (auto-builds if needed) +scripts/run-native.sh -m live -v # Live trading with verbose logs +scripts/run-native.sh --enable-ml # ML-enhanced simulation mode +scripts/run-native.sh --enable-visualization # With real-time dashboard +scripts/run-native.sh --setup-credentials # Configure API keys ``` #### **Docker Execution** (Recommended for Production) ```bash # Build and run in one command -./run-docker.sh # Simulation mode -./run-docker.sh -m live -v # Live trading mode -./run-docker.sh build # Build Docker image -./run-docker.sh logs # View container logs +scripts/run-docker.sh # Simulation mode +scripts/run-docker.sh -m live -v # Live trading mode +scripts/run-docker.sh build # Build Docker image +scripts/run-docker.sh logs # View container logs ``` ### Manual Building from Source @@ -128,7 +128,7 @@ git clone https://github.com/chizy7/PinnacleMM.git cd PinnacleMM # Build with native script (recommended) -./run-native.sh build +scripts/run-native.sh build # Or build manually mkdir build && cd build @@ -140,13 +140,13 @@ make -j$(sysctl -n hw.ncpu) # macOS ### Script Features Comparison > **Note**: I will update later on after completing phase 4 and 5, cleaning up the code and getting PinnacleMM ready for optimization and production deployment. -| Feature | Native Script (`./run-native.sh`) | Docker Script (`./run-docker.sh`) | +| Feature | Native Script (`scripts/run-native.sh`) | Docker Script (`scripts/run-docker.sh`) | |---------|-----------------------------------|-----------------------------------| | **Simulation Mode** | Perfect | Perfect | | **Live Trading** | Real WebSocket data | ⚠️ WebSocket config issue | | **Auto-Build** | Builds if needed | Auto Docker build | -| **Test Runner** | `./run-native.sh test` | ❌ Not included | -| **Benchmarks** | `./run-native.sh benchmark` | ❌ Not included | +| **Test Runner** | `scripts/run-native.sh test` | ❌ Not included | +| **Benchmarks** | `scripts/run-native.sh benchmark` | ❌ Not included | | **Credential Setup** | Interactive setup | Volume mounting | | **Dependency Check** | cmake, make, g++ | Docker only | | **Best For** | Development & Live Trading | Production & Simulation | @@ -156,8 +156,8 @@ make -j$(sysctl -n hw.ncpu) # macOS #### Simulation Mode ```bash # Using scripts (recommended) -./run-native.sh # Native execution -./run-docker.sh # Docker execution +scripts/run-native.sh # Native execution +scripts/run-docker.sh # Docker execution # Manual execution cd build && ./pinnaclemm --mode simulation --symbol BTC-USD @@ -187,11 +187,11 @@ cd build && ./pinnaclemm --mode simulation --enable-ml --enable-visualization -- #### Live Exchange Mode ```bash # Setup credentials first -./run-native.sh --setup-credentials +scripts/run-native.sh --setup-credentials # Live trading with scripts -./run-native.sh -m live -v # Native (recommended for live) -./run-docker.sh -m live -v # Docker +scripts/run-native.sh -m live -v # Native (recommended for live) +scripts/run-docker.sh -m live -v # Docker # Manual execution cd build && ./pinnaclemm --mode live --exchange coinbase --symbol BTC-USD --verbose @@ -292,7 +292,7 @@ PinnacleMM securely stores and manages exchange API credentials: 1. **Run credential setup**: ```bash -./run-native.sh --setup-credentials +scripts/run-native.sh --setup-credentials # or manually: ./pinnaclemm --setup-credentials ``` @@ -306,7 +306,7 @@ PinnacleMM securely stores and manages exchange API credentials: 4. **Verify setup**: ```bash -./run-native.sh -m live -v +scripts/run-native.sh -m live -v # or manually: ./pinnaclemm --mode live --exchange coinbase --symbol BTC-USD --verbose ``` @@ -329,31 +329,31 @@ For more detailed instructions, see the [Getting Started Guide](docs/user_guide/ ## Script Documentation -### Native Script (`./run-native.sh`) +### Native Script (`scripts/run-native.sh`) **Available Commands:** ```bash # Execution modes -./run-native.sh # Simulation mode (default) -./run-native.sh -m live -v # Live mode with verbose logging -./run-native.sh -s ETH-USD # Custom trading symbol -./run-native.sh -e coinbase # Specify exchange +scripts/run-native.sh # Simulation mode (default) +scripts/run-native.sh -m live -v # Live mode with verbose logging +scripts/run-native.sh -s ETH-USD # Custom trading symbol +scripts/run-native.sh -e coinbase # Specify exchange # Build commands -./run-native.sh build # Build project -./run-native.sh clean # Clean build directory -./run-native.sh rebuild # Clean and rebuild +scripts/run-native.sh build # Build project +scripts/run-native.sh clean # Clean build directory +scripts/run-native.sh rebuild # Clean and rebuild # Testing and benchmarks -./run-native.sh test # Run all tests -./run-native.sh benchmark # Run performance benchmarks +scripts/run-native.sh test # Run all tests +scripts/run-native.sh benchmark # Run performance benchmarks # Setup -./run-native.sh --setup-credentials # Configure API credentials (secure input) -./run-native.sh --help # Show help +scripts/run-native.sh --setup-credentials # Configure API credentials (secure input) +scripts/run-native.sh --help # Show help # Cleanup -./cleanup.sh # Interactive cleanup utility +scripts/cleanup.sh # Interactive cleanup utility ``` **Features:** @@ -363,21 +363,21 @@ For more detailed instructions, see the [Getting Started Guide](docs/user_guide/ - **Test runner**: Comprehensive test suite execution - **Live trading**: Real WebSocket connections to exchanges -### Docker Script (`./run-docker.sh`) +### Docker Script (`scripts/run-docker.sh`) **Available Commands:** ```bash # Execution modes -./run-docker.sh # Simulation mode (detached) -./run-docker.sh -m live -v # Live mode (interactive) -./run-docker.sh -s ETH-USD # Custom trading symbol +scripts/run-docker.sh # Simulation mode (detached) +scripts/run-docker.sh -m live -v # Live mode (interactive) +scripts/run-docker.sh -s ETH-USD # Custom trading symbol # Container management -./run-docker.sh build # Build Docker image -./run-docker.sh logs # View container logs -./run-docker.sh stop # Stop and remove containers -./run-docker.sh clean # Remove containers and image -./run-docker.sh --help # Show help +scripts/run-docker.sh build # Build Docker image +scripts/run-docker.sh logs # View container logs +scripts/run-docker.sh stop # Stop and remove containers +scripts/run-docker.sh clean # Remove containers and image +scripts/run-docker.sh --help # Show help ``` **Features:** @@ -392,12 +392,12 @@ For more detailed instructions, see the [Getting Started Guide](docs/user_guide/ ### Using Docker Script (Recommended) ```bash # Quick start -./run-docker.sh # Simulation mode -./run-docker.sh -m live -v # Live trading +scripts/run-docker.sh # Simulation mode +scripts/run-docker.sh -m live -v # Live trading # Container management -./run-docker.sh logs # Monitor logs -./run-docker.sh stop # Stop trading +scripts/run-docker.sh logs # Monitor logs +scripts/run-docker.sh stop # Stop trading ``` ### Using Pre-built Images (GitHub Container Registry) @@ -540,6 +540,7 @@ open build/test_dashboard.html - [Persistence System](docs/architecture/persistence.md) - [Recovery Guide](docs/user_guide/recovery.md) - [Security & API Key Management](docs/security/credentials.md) +- [Certificate Pinning Guide](docs/security/CERTIFICATE_PINNING.md) ## Technology Stack diff --git a/core/utils/CertificatePinner.cpp b/core/utils/CertificatePinner.cpp index ba630f7..28bce09 100644 --- a/core/utils/CertificatePinner.cpp +++ b/core/utils/CertificatePinner.cpp @@ -164,23 +164,26 @@ std::string CertificatePinner::getCertificateFingerprint(X509* cert) { } void CertificatePinner::initializeDefaultPins() { - // Coinbase Pro pins (note to self:just an example - these should be updated - // with real pins) + // Coinbase certificate pins (extracted on 2025-10-13) + // Certificate valid until: Dec 22 02:31:45 2025 GMT + // Issuer: Google Trust Services (WE1) + + // Primary WebSocket endpoint for Coinbase Pro/Advanced Trade addPin("ws-feed.exchange.coinbase.com", - "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", false); + "mpzb4t3w5gAFZJGODlP0+FJa+wjD/bOQszdCDs6BTmU=", true); - // Kraken pins - addPin("ws.kraken.com", "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=", false); + // Coinbase Prime WebSocket endpoint + addPin("ws-feed.prime.coinbase.com", + "ERzVGmVjfqDVEe2YEp5l1B7zaXEJoSYinwL9InU8Pis=", true); - // Binance pins - addPin("stream.binance.com", - "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC=", false); + // Advanced Trade WebSocket endpoint + addPin("advanced-trade-ws.coinbase.com", + "Is81uMxmmDbwnPDQSpN+FgZ5nfv2XenZ8Ql8zE4Vbzs=", true); - // TODO and note to remind myself to update these pins - // Self Note: In production, these should be real certificate pins - // obtained by connecting to the services and extracting public key hashes - spdlog::info("Initialized default certificate pins (example pins - update " - "for production)"); + // Note: Certificate pinning is now enabled with real pins + // Pins should be updated before certificate expiry (Dec 2025) + // To extract new pins, run: scripts/extract_cert_pin.sh + spdlog::info("Initialized certificate pins for Coinbase (pinning enabled)"); } bool CertificatePinner::extractPublicKeyHash( diff --git a/docs/security/CERTIFICATE_PINNING.md b/docs/security/CERTIFICATE_PINNING.md new file mode 100644 index 0000000..8b183c5 --- /dev/null +++ b/docs/security/CERTIFICATE_PINNING.md @@ -0,0 +1,290 @@ +# Certificate Pinning Guide + +## Overview + +Certificate pinning enhances security by validating that the server's SSL/TLS certificate matches a known "pin" (the SHA256 hash of the certificate's public key). This prevents man-in-the-middle (MITM) attacks, even if an attacker has a valid certificate from a compromised Certificate Authority. + +## Current Configuration + +### Coinbase Endpoints (Enabled & Enforced) + +| Endpoint | Pin | Expiry | +|----------|-----|--------| +| `ws-feed.exchange.coinbase.com` | `mpzb4t3w5gAFZJGODlP0+FJa+wjD/bOQszdCDs6BTmU=` | Dec 22, 2025 | +| `ws-feed.prime.coinbase.com` | `ERzVGmVjfqDVEe2YEp5l1B7zaXEJoSYinwL9InU8Pis=` | Jan 6, 2026 | +| `advanced-trade-ws.coinbase.com` | `Is81uMxmmDbwnPDQSpN+FgZ5nfv2XenZ8Ql8zE4Vbzs=` | Dec 22, 2025 | + +**Status:** Pinning enabled and enforced +**Issuer:** Google Trust Services (WE1) +**Last Updated:** October 13, 2025 + +## How It Works + +1. **Connection Attempt:** When connecting to a WebSocket endpoint (e.g., Coinbase) +2. **Certificate Received:** Server presents its SSL/TLS certificate +3. **Pin Extraction:** Extract SHA256 hash of the certificate's public key +4. **Pin Validation:** Compare against configured pins +5. **Decision:** + - Match -> Connection allowed + - Mismatch -> Connection rejected (if enforcement enabled) + +## Extracting Certificate Pins + +### Using the Script (Recommended) + +```bash +# Run the extraction script +./scripts/extract_cert_pin.sh + +# Output shows pins for all configured endpoints +``` + +### Manual Extraction + +```bash +# Connect and extract certificate pin +echo | openssl s_client -connect ws-feed.exchange.coinbase.com:443 \ + -servername ws-feed.exchange.coinbase.com 2>/dev/null \ + | openssl x509 -pubkey -noout \ + | openssl pkey -pubin -outform DER \ + | openssl dgst -sha256 -binary \ + | openssl base64 +``` + +## Updating Certificate Pins + +### When to Update + +- **Before Expiry:** Update pins 1-2 weeks before certificate expiry +- **Certificate Rotation:** When exchange rotates certificates +- **Security Incident:** Immediately if certificate compromise suspected + +### Update Process + +1. **Extract New Pins:** + ```bash + ./scripts/extract_cert_pin.sh + ``` + +2. **Update Source Code:** + - Edit: `core/utils/CertificatePinner.cpp` + - Function: `CertificatePinner::initializeDefaultPins()` + - Lines: 166-187 + +3. **Update Pins:** + ```cpp + addPin("ws-feed.exchange.coinbase.com", + "NEW_PIN_HERE_BASE64_ENCODED=", true); + ``` + +4. **Test:** + ```bash + # Rebuild + cd build && cmake --build . --target certificate_pinner_tests + + # Run tests + ./certificate_pinner_tests + ``` + +5. **Verify:** + - All tests pass + - Real certificate validation succeeds + - Check logs for "Certificate pin matched" + +## Configuration + +### In Code (Default) + +Pins are configured in `core/utils/CertificatePinner.cpp`: + +```cpp +void CertificatePinner::initializeDefaultPins() { + addPin("ws-feed.exchange.coinbase.com", + "mpzb4t3w5gAFZJGODlP0+FJa+wjD/bOQszdCDs6BTmU=", true); + // ... more pins +} +``` + +### Via Configuration File (Optional) + +Create `config/certificate_pins.json`: + +```json +{ + "enabled": true, + "certificate_pins": { + "ws-feed.exchange.coinbase.com": { + "enforce": true, + "pins": [ + "mpzb4t3w5gAFZJGODlP0+FJa+wjD/bOQszdCDs6BTmU=", + "BACKUP_PIN_HERE_IF_NEEDED=" + ] + } + } +} +``` + +Load at runtime: +```cpp +pinner->loadPinsFromFile("config/certificate_pins.json"); +``` + +## Testing + +### Run Certificate Pinning Tests + +```bash +# Build tests +cd build && cmake --build . --target certificate_pinner_tests + +# Run tests +./certificate_pinner_tests +``` + +### Expected Output + +``` +[==========] Running 6 tests from 1 test suite. +... +=== Testing Real Coinbase Certificate Pinning === +Actual certificate pin: mpzb4t3w5gAFZJGODlP0+FJa+wjD/bOQszdCDs6BTmU= +Certificate Subject: /CN=exchange.coinbase.com +Certificate Issuer: /C=US/O=Google Trust Services/CN=WE1 +=== Certificate Pinning Test PASSED === +... +[ PASSED ] 6 tests. +``` + +## Troubleshooting + +### Connection Fails with "Certificate pin verification failed" + +**Cause:** Certificate changed (rotation or MITM attack) + +**Solution:** +1. Extract current certificate pin +2. Compare with configured pin +3. If legitimate rotation, update pin +4. If unexpected, investigate security incident + +### Test Fails: "Could not connect to Coinbase" + +**Cause:** Network issue or endpoint unavailable + +**Solution:** +- Check internet connection +- Verify endpoint is accessible: `curl -I https://ws-feed.exchange.coinbase.com` +- Firewall/proxy may be blocking connection + +### Warning: "Certificate pin mismatch (not enforced)" + +**Cause:** Pin mismatch but enforcement disabled + +**Solution:** +- This is informational when `enforce: false` +- Update pin or enable enforcement for production + +## Best Practices + +### Multiple Pins per Endpoint + +Include backup pins for certificate rotation: + +```cpp +// Current certificate +addPin("ws-feed.exchange.coinbase.com", + "mpzb4t3w5gAFZJGODlP0+FJa+wjD/bOQszdCDs6BTmU=", true); + +// Backup pin (for rotation) +addPin("ws-feed.exchange.coinbase.com", + "BACKUP_PIN_BASE64=", true); +``` + +### Pin Expiry Monitoring + +Set calendar reminders: +- **90 days before expiry:** Extract backup pins +- **30 days before expiry:** Test with backup pins +- **7 days before expiry:** Deploy updated pins + +### Development vs Production + +```cpp +// Development: Log warnings but don't enforce +addPin("ws-feed.exchange.coinbase.com", "pin==", false); + +// Production: Enforce pinning +addPin("ws-feed.exchange.coinbase.com", "pin==", true); +``` + +## Security Considerations + +### Benefits + +- Prevents MITM attacks +- Protects against compromised CAs +- Detects certificate changes immediately + +### Risks + +- **Availability Risk:** Outdated pins block legitimate connections +- **Maintenance:** Requires pin updates before cert expiry +- **Monitoring:** Must track certificate expiry dates + +### Recommendations + +1. **Always enable pinning in production** +2. **Monitor certificate expiry dates** +3. **Include backup pins for rotation** +4. **Test pin updates in staging first** +5. **Document pin update procedures** + +## API Reference + +### Adding Pins + +```cpp +// Add single pin +pinner->addPin(hostname, sha256Pin, enforce); + +// Example +pinner->addPin("ws-feed.exchange.coinbase.com", + "mpzb4t3w5gAFZJGODlP0+FJa+wjD/bOQszdCDs6BTmU=", + true); +``` + +### Verifying Certificates + +```cpp +// Verify certificate against pins +bool valid = pinner->verifyCertificate(hostname, x509_cert); + +if (!valid) { + // Connection rejected - potential security issue +} +``` + +### Enable/Disable Pinning + +```cpp +// Disable pinning (not recommended for production) +pinner->setEnabled(false); + +// Check status +bool enabled = pinner->isEnabled(); +``` + +## References + +- **Implementation:** `core/utils/CertificatePinner.cpp` +- **Header:** `core/utils/CertificatePinner.h` +- **Tests:** `tests/unit/CertificatePinnerTest.cpp` +- **Extraction Script:** `scripts/extract_cert_pin.sh` + +## Support + +For questions or issues: +1. Check test output: `./certificate_pinner_tests` +2. Review logs for pin verification errors +3. Re-extract pins if certificates rotated +4. See [Credentials Guide](./credentials.md) for API key security practices diff --git a/docs/user_guide/getting_started.md b/docs/user_guide/getting_started.md index 9d2ea4a..faed114 100644 --- a/docs/user_guide/getting_started.md +++ b/docs/user_guide/getting_started.md @@ -83,7 +83,7 @@ The simplest way to start is with the simulation mode (no API keys needed): ```bash # Using the native script (recommended) -./run-native.sh --setup-credentials +scripts/run-native.sh --setup-credentials # Or directly ./pinnaclemm --setup-credentials @@ -207,7 +207,7 @@ docker run -d --name pinnaclemm pinnaclemm --symbol ETH-USD --verbose ### Runtime Issues -- **"Failed to load secure config"**: Run `./run-native.sh --setup-credentials` first +- **"Failed to load secure config"**: Run `scripts/run-native.sh --setup-credentials` first - **"Authentication failure"**: Check your API credentials and master password - **"Key derivation failed"**: Your config file may be corrupted; delete and recreate credentials - **WebSocket connection issues**: Verify internet connection and exchange endpoints diff --git a/cleanup.sh b/scripts/cleanup.sh similarity index 72% rename from cleanup.sh rename to scripts/cleanup.sh index ec1fc62..16a1f28 100755 --- a/cleanup.sh +++ b/scripts/cleanup.sh @@ -1,4 +1,5 @@ #!/bin/bash +set -euo pipefail # PinnacleMM Cleanup Script @@ -15,7 +16,7 @@ read -r -p "Choose option (1-6): " choice case $choice in 1) echo "Cleaning build files..." - cd build && make clean + (cd build && make clean) echo "Build files cleaned" ;; 2) @@ -29,12 +30,20 @@ case $choice in echo "Data/state cleaned" ;; 4) + echo "WARNING: This will delete the entire build directory!" + read -r -p "Are you sure? (yes/NO): " confirm + if [[ "$confirm" != "yes" ]]; then + echo "Nuclear cleanup cancelled" + exit 0 + fi echo "Nuclear cleanup - removing everything..." rm -rf build/* echo "Rebuilding..." - mkdir -p build && cd build || exit - cmake .. - make -j4 + mkdir -p build + if ! (cd build && cmake .. && make -j4); then + echo "ERROR: Rebuild failed. Check cmake and make output above." + exit 1 + fi echo "Complete rebuild finished" ;; 5) diff --git a/scripts/extract_cert_pin.sh b/scripts/extract_cert_pin.sh new file mode 100755 index 0000000..6140de5 --- /dev/null +++ b/scripts/extract_cert_pin.sh @@ -0,0 +1,66 @@ +#!/bin/bash +# Script to extract certificate pins from Coinbase WebSocket endpoints +# This extracts the SHA256 hash of the Subject Public Key Info (SPKI) in base64 format + +set -e + +echo "==================================================" +echo "Certificate Pin Extractor for Coinbase" +echo "==================================================" +echo "" + +# Coinbase WebSocket endpoints +ENDPOINTS=( + "ws-feed.exchange.coinbase.com:443" + "ws-feed.prime.coinbase.com:443" + "advanced-trade-ws.coinbase.com:443" +) + +for ENDPOINT in "${ENDPOINTS[@]}"; do + HOST=$(echo "$ENDPOINT" | cut -d: -f1) + PORT=$(echo "$ENDPOINT" | cut -d: -f2) + + echo "Extracting pin for: $HOST:$PORT" + echo "----------------------------------------" + + # Get the certificate chain with timeout + # Use gtimeout on macOS (brew install coreutils) or timeout on Linux + if command -v timeout >/dev/null 2>&1; then + TIMEOUT_CMD="timeout 10s" + elif command -v gtimeout >/dev/null 2>&1; then + TIMEOUT_CMD="gtimeout 10s" + else + TIMEOUT_CMD="" + fi + + if [ -n "$TIMEOUT_CMD" ]; then + CERT_CHAIN=$($TIMEOUT_CMD openssl s_client -connect "$ENDPOINT" -servername "$HOST" /dev/null | openssl x509 -outform PEM) + else + # Fallback without timeout + CERT_CHAIN=$(openssl s_client -connect "$ENDPOINT" -servername "$HOST" /dev/null | openssl x509 -outform PEM) + fi + + if [ -z "$CERT_CHAIN" ]; then + echo "Failed to retrieve certificate for $HOST" + echo "" + continue + fi + + # Extract the public key and compute SHA256 hash in base64 + PIN=$(echo "$CERT_CHAIN" | openssl x509 -pubkey -noout | openssl pkey -pubin -outform DER | openssl dgst -sha256 -binary | openssl base64) + + echo "Certificate Pin (SHA256/Base64): $PIN" + + # Also show certificate details + echo "" + echo "Certificate Details:" + echo "$CERT_CHAIN" | openssl x509 -noout -subject -issuer -dates + echo "" + echo "==================================================" + echo "" +done + +echo "Script completed!" +echo "" +echo "To use these pins, update core/utils/CertificatePinner.cpp" +echo "Replace the placeholder pins with the extracted values above." diff --git a/run-docker.sh b/scripts/run-docker.sh similarity index 94% rename from run-docker.sh rename to scripts/run-docker.sh index 5506730..733d138 100755 --- a/run-docker.sh +++ b/scripts/run-docker.sh @@ -152,13 +152,13 @@ print_info "Stopping any existing PinnacleMM containers..." docker stop pinnaclemm pinnaclemm-live 2>/dev/null || true docker rm pinnaclemm pinnaclemm-live 2>/dev/null || true -# Prepare container name and arguments +# Prepare container name and arguments as array CONTAINER_NAME="pinnaclemm" -DOCKER_ARGS="--mode $MODE --symbol $SYMBOL" +DOCKER_ARGS=("--mode" "$MODE" "--symbol" "$SYMBOL") if [[ "$MODE" == "live" ]]; then CONTAINER_NAME="pinnaclemm-live" - DOCKER_ARGS="$DOCKER_ARGS --exchange $EXCHANGE" + DOCKER_ARGS+=("--exchange" "$EXCHANGE") # Check if config directory exists for live mode if [[ ! -d "config" ]]; then @@ -169,14 +169,14 @@ if [[ "$MODE" == "live" ]]; then # Check if credentials are configured if [[ ! -f "config/secure_config.json" ]]; then print_warning "No API credentials found. You'll need to set them up first." - print_info "Run: ./run-native.sh --setup-credentials" + print_info "Run: scripts/run-native.sh --setup-credentials" print_info "Or run the native binary to configure credentials first." fi fi # Add verbose flag if specified if [[ -n "$VERBOSE" ]]; then - DOCKER_ARGS="$DOCKER_ARGS $VERBOSE" + DOCKER_ARGS+=("$VERBOSE") fi print_info "Starting PinnacleMM in Docker..." @@ -192,12 +192,12 @@ if [[ "$MODE" == "live" ]]; then print_info "Running in live mode (interactive for password input)..." docker run -it --name "$CONTAINER_NAME" \ -v "$(pwd)/config:/app/config" \ - pinnaclemm "$DOCKER_ARGS" + pinnaclemm "${DOCKER_ARGS[@]}" else # Simulation mode - detached print_info "Running in simulation mode (detached)..." docker run -d --name "$CONTAINER_NAME" \ - pinnaclemm "$DOCKER_ARGS" + pinnaclemm "${DOCKER_ARGS[@]}" print_success "Container started successfully!" print_info "Container ID: $(docker ps -q -f name=$CONTAINER_NAME)" diff --git a/run-native.sh b/scripts/run-native.sh similarity index 98% rename from run-native.sh rename to scripts/run-native.sh index 94d7861..3ca3958 100755 --- a/run-native.sh +++ b/scripts/run-native.sh @@ -293,11 +293,11 @@ if [[ ! -f "$BUILD_DIR/pinnaclemm" ]]; then build_project fi -# Prepare arguments -ARGS="--mode $MODE --symbol $SYMBOL" +# Prepare arguments as array +ARGS=("--mode" "$MODE" "--symbol" "$SYMBOL") if [[ "$MODE" == "live" ]]; then - ARGS="$ARGS --exchange $EXCHANGE" + ARGS+=("--exchange" "$EXCHANGE") # Check if credentials are configured if [[ ! -f "config/secure_config.json" ]]; then @@ -318,7 +318,7 @@ fi # Add verbose flag if specified if [[ -n "$VERBOSE" ]]; then - ARGS="$ARGS $VERBOSE" + ARGS+=("$VERBOSE") fi print_info "Starting PinnacleMM..." @@ -330,4 +330,4 @@ fi # Run the application cd "$BUILD_DIR" -./pinnaclemm "$ARGS" +./pinnaclemm "${ARGS[@]}" diff --git a/tests/unit/CertificatePinnerTest.cpp b/tests/unit/CertificatePinnerTest.cpp new file mode 100644 index 0000000..9570997 --- /dev/null +++ b/tests/unit/CertificatePinnerTest.cpp @@ -0,0 +1,214 @@ +#include "../../core/utils/CertificatePinner.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace pinnacle::utils; + +class CertificatePinnerTest : public ::testing::Test { +protected: + void SetUp() override { pinner = std::make_shared(); } + + void TearDown() override { pinner.reset(); } + + std::shared_ptr pinner; + + // Helper to connect and get certificate from a real endpoint + X509* fetchCertificate(const std::string& hostname, int port = 443) { + SSL_CTX* ctx = SSL_CTX_new(TLS_client_method()); + if (!ctx) { + return nullptr; + } + + SSL* ssl = SSL_new(ctx); + if (!ssl) { + SSL_CTX_free(ctx); + return nullptr; + } + + // Create socket + int sock = socket(AF_INET, SOCK_STREAM, 0); + if (sock < 0) { + SSL_free(ssl); + SSL_CTX_free(ctx); + return nullptr; + } + + // Set timeout + struct timeval timeout; + timeout.tv_sec = 5; + timeout.tv_usec = 0; + if (setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) < + 0 || + setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)) < + 0) { + // Continue anyway - timeouts are best-effort + // But log that timeout setting failed + } + + // Resolve hostname + struct hostent* host = gethostbyname(hostname.c_str()); + if (!host) { + close(sock); + SSL_free(ssl); + SSL_CTX_free(ctx); + return nullptr; + } + + struct sockaddr_in addr; + addr.sin_family = AF_INET; + addr.sin_port = htons(port); + addr.sin_addr = *reinterpret_cast(host->h_addr); + + // Connect + if (connect(sock, reinterpret_cast(&addr), sizeof(addr)) < + 0) { + close(sock); + SSL_free(ssl); + SSL_CTX_free(ctx); + return nullptr; + } + + // SSL handshake + SSL_set_fd(ssl, sock); + SSL_set_tlsext_host_name(ssl, hostname.c_str()); + + if (SSL_connect(ssl) != 1) { + close(sock); + SSL_free(ssl); + SSL_CTX_free(ctx); + return nullptr; + } + + // Get certificate (this creates a copy, so we own it) + X509* cert = SSL_get_peer_certificate(ssl); + + // Cleanup + SSL_shutdown(ssl); + close(sock); + SSL_free(ssl); + SSL_CTX_free(ctx); + + return cert; + } +}; + +TEST_F(CertificatePinnerTest, InitializeDefaultPins) { + // Should have default pins configured + EXPECT_TRUE(pinner->isEnabled()); +} + +TEST_F(CertificatePinnerTest, AddAndVerifyPin) { + // Add a test pin + pinner->addPin("test.example.com", "testpin123==", true); + + // This will pass because we don't have a real certificate to test against + // Just verify the pin was added + SUCCEED(); +} + +TEST_F(CertificatePinnerTest, CoinbaseRealCertificateValidation) { + std::cout << "\n=== Testing Real Coinbase Certificate Pinning ===\n" + << std::endl; + + // Try to fetch the real certificate from Coinbase + X509* cert = fetchCertificate("ws-feed.exchange.coinbase.com", 443); + + if (!cert) { + GTEST_SKIP() << "Could not connect to Coinbase (network issue or endpoint " + "unavailable)"; + return; + } + + // Verify the certificate against our pinned values + bool verified = + pinner->verifyCertificate("ws-feed.exchange.coinbase.com", cert); + + // Get the actual fingerprint for debugging + std::string actualPin = CertificatePinner::getCertificateFingerprint(cert); + std::cout << "Actual certificate pin: " << actualPin << std::endl; + + // Verify certificate details + char* subjectName = + X509_NAME_oneline(X509_get_subject_name(cert), nullptr, 0); + char* issuerName = X509_NAME_oneline(X509_get_issuer_name(cert), nullptr, 0); + + std::cout << "Certificate Subject: " << (subjectName ? subjectName : "N/A") + << std::endl; + std::cout << "Certificate Issuer: " << (issuerName ? issuerName : "N/A") + << std::endl; + + if (subjectName) + OPENSSL_free(subjectName); + if (issuerName) + OPENSSL_free(issuerName); + + // Clean up + X509_free(cert); + + // The verification should succeed with our configured pins + EXPECT_TRUE(verified) + << "Certificate pinning failed for ws-feed.exchange.coinbase.com"; + std::cout << "\n=== Certificate Pinning Test PASSED ===\n" << std::endl; +} + +TEST_F(CertificatePinnerTest, InvalidCertificateShouldFail) { + std::cout << "\n=== Testing Invalid Certificate Rejection ===\n" << std::endl; + + // Create a pinner with a specific pin for a test domain + auto testPinner = std::make_shared(); + testPinner->addPin("test.invalid.domain", + "wrongpin12345678901234567890123=", true); + + // Try to fetch a real certificate from Coinbase + X509* cert = fetchCertificate("ws-feed.exchange.coinbase.com", 443); + + if (!cert) { + GTEST_SKIP() << "Could not connect to Coinbase for negative test"; + return; + } + + // This should fail because we're testing with a wrong pin + bool verified = testPinner->verifyCertificate("test.invalid.domain", cert); + + X509_free(cert); + + // Should fail because the pin doesn't match + EXPECT_FALSE(verified) << "Invalid certificate was incorrectly accepted"; + std::cout << "\n=== Invalid Certificate Rejection Test PASSED ===\n" + << std::endl; +} + +TEST_F(CertificatePinnerTest, DisabledPinnerAllowsAll) { + pinner->setEnabled(false); + + // When disabled, all certificates should pass + EXPECT_FALSE(pinner->isEnabled()); +} + +TEST_F(CertificatePinnerTest, FingerprintExtraction) { + // Try to extract fingerprint from a real certificate + X509* cert = fetchCertificate("ws-feed.exchange.coinbase.com", 443); + + if (!cert) { + GTEST_SKIP() << "Could not connect to Coinbase for fingerprint test"; + return; + } + + std::string fingerprint = CertificatePinner::getCertificateFingerprint(cert); + + std::cout << "\nExtracted fingerprint: " << fingerprint << std::endl; + + X509_free(cert); + + // Should have extracted a non-empty fingerprint + EXPECT_FALSE(fingerprint.empty()) + << "Failed to extract certificate fingerprint"; +}