Skip to content

Enhanced request signing with domain verification (v1.1)#220

Open
jevansnyc wants to merge 1 commit intomainfrom
feature/enhanced-request-signing
Open

Enhanced request signing with domain verification (v1.1)#220
jevansnyc wants to merge 1 commit intomainfrom
feature/enhanced-request-signing

Conversation

@jevansnyc
Copy link
Collaborator

Summary

  • Implements cryptographic signing of OpenRTB requests that includes publisher domain verification and replay protection
  • The signed payload format is: kid:request_host:request_scheme:id:ts
  • Adds version ("1.1") and ts (Unix timestamp) fields to ext.trusted_server

This prevents request tampering and domain spoofing by ensuring the signature is cryptographically bound to the originating publisher domain.

Changes

  • openrtb.rs: Add version and ts fields to TrustedServerExt
  • signing.rs: Add SigningParams struct, SIGNING_VERSION constant, and sign_request() method
  • prebid.rs: Update both PrebidAuctionProvider and enhance_openrtb_request to use enhanced signing

Output Format

{
  "ext": {
    "trusted_server": {
      "version": "1.1",
      "kid": "ts-2026-01-A",
      "request_host": "publisher.com",
      "request_scheme": "https",
      "ts": 1738527600,
      "signature": "base64-encoded-ed25519-signature"
    }
  }
}

Test plan

  • Verify build passes (cargo build --release)
  • Verify clippy passes (cargo clippy)
  • Deploy to test environment and verify signature verification works on Mocktioneer
  • Test with different hosts to confirm signatures differ

Implements cryptographic signing of OpenRTB requests that includes publisher
domain verification and replay protection. The signed payload now includes:
- Key ID (kid)
- Request host
- Request scheme
- Request ID
- Unix timestamp

This prevents request tampering and domain spoofing by ensuring the signature
is bound to the originating publisher domain.

Changes:
- Add version and ts fields to TrustedServerExt
- Add SigningParams struct and sign_request() method
- Update PrebidAuctionProvider to use enhanced signing
- Add comprehensive tests for payload construction and signing
Copy link
Collaborator

@ChristianPavilonis ChristianPavilonis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pretty good, a couple nit picks.

Opening an issue on mocktioneer to verify new signature payload: stackpop/mocktioneer#30

deployed test site with this (verification broken until mocktioneer updated) but the TS side is working.

request_scheme,
timestamp: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should use milliseconds to stay in line with the rest of the openrtb spec.

let signature = signer.sign(id.as_bytes())?;
let params = SigningParams::new(
id.to_string(),
request_host.clone(),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't have to clone here if you use params.request_host and params.request_scheme on lines 544-545

@jevansnyc
Copy link
Collaborator Author

jevansnyc commented Feb 5, 2026

Enhanced Request Signing (v1.1) - Architecture Diagram

Request Signing Flow

sequenceDiagram
    participant Client as Publisher<br/>Website
    participant TS as Trusted Server
    participant Signer as Request<br/>Signer
    participant SSP as Prebid SSP/<br/>Ad Exchange

    Client->>TS: Ad Request
    Note over TS: Extract request metadata

    TS->>TS: Parse request_host<br/>(e.g., "publisher.com")
    TS->>TS: Parse request_scheme<br/>(e.g., "https")
    TS->>TS: Generate request_id<br/>(e.g., "auction-123")

    TS->>Signer: Create SigningParams
    Note over Signer: SigningParams {<br/>  request_id: "auction-123"<br/>  request_host: "publisher.com"<br/>  request_scheme: "https"<br/>  timestamp: 1738527600<br/>}

    Signer->>Signer: Build canonical payload
    Note over Signer: Format:<br/>"kid:host:scheme:id:ts"<br/><br/>Example:<br/>"ts-2026-01-A:publisher.com:https:auction-123:1738527600"

    Signer->>Signer: Sign with Ed25519 private key
    Signer->>Signer: Base64url encode signature
    Signer-->>TS: Return signature

    TS->>TS: Build OpenRTB request with ext.trusted_server
    Note over TS: {<br/>  "version": "1.1",<br/>  "kid": "ts-2026-01-A",<br/>  "request_host": "publisher.com",<br/>  "request_scheme": "https",<br/>  "ts": 1738527600,<br/>  "signature": "base64..."<br/>}

    TS->>SSP: POST /openrtb2/auction
    SSP->>SSP: Verify signature
    Note over SSP: 1. Reconstruct payload<br/>2. Fetch public key by kid<br/>3. Verify Ed25519 signature<br/>4. Check timestamp freshness<br/>5. Validate domain binding

    alt Valid Signature
        SSP-->>TS: 200 OK with bids
        TS-->>Client: Return ad
    else Invalid Signature
        SSP-->>TS: 403 Forbidden
        Note over SSP: Reject if:<br/>• Signature invalid<br/>• Timestamp too old<br/>• Domain mismatch<br/>• Missing fields
        TS-->>Client: Error response
    end
Loading

Security Properties

flowchart TB
    subgraph Input["Request Inputs"]
        A1[Request ID]
        A2[Publisher Host]
        A3[Request Scheme]
        A4[Timestamp]
        A5[Key ID]
    end

    subgraph Signing["Canonical Payload Construction"]
        B1["Build payload string:<br/>kid:host:scheme:id:ts"]
    end

    subgraph Crypto["Cryptographic Signing"]
        C1[Ed25519 Private Key]
        C2[Sign payload]
        C3[Base64url encode]
    end

    subgraph Output["Signed Request Extension"]
        D1[version: '1.1']
        D2[kid]
        D3[request_host]
        D4[request_scheme]
        D5[ts]
        D6[signature]
    end

    subgraph Protection["Security Guarantees"]
        E1[Domain Binding]
        E2[Replay Protection]
        E3[Tampering Detection]
        E4[Request Authenticity]
    end

    Input --> Signing
    Signing --> Crypto
    C1 --> C2
    C2 --> C3
    Crypto --> Output

    Output --> Protection

Loading

Attack Prevention

flowchart LR
    subgraph Threats["Attack Vectors"]
        T1[Domain Spoofing]
        T2[Request Replay]
        T3[Payload Tampering]
        T4[MITM Attacks]
    end

    subgraph v10["v1.0 Signing<br/>(Only ID)"]
        V1[❌ Vulnerable to<br/>domain spoofing]
        V2[❌ No replay<br/>protection]
        V3[✅ Tampering<br/>detection]
    end

    subgraph v11["v1.1 Enhanced Signing<br/>(ID + Host + Scheme + Timestamp)"]
        N1[✅ Domain binding<br/>prevents spoofing]
        N2[✅ Timestamp prevents<br/>replay attacks]
        N3[✅ Enhanced tampering<br/>detection]
        N4[✅ Scheme validation<br/>prevents downgrade]
    end

    T1 --> V1
    T1 --> N1
    T2 --> V2
    T2 --> N2
    T3 --> V3
    T3 --> N3
    T4 --> N4

    
Loading

Payload Format Evolution

Version 1.0 (Legacy)

Signed payload: request_id
Example:        "auction-123"

Attack scenario: Attacker can replay signature for different domains

Version 1.1 (Enhanced)

Signed payload: kid:request_host:request_scheme:request_id:ts
Example:        "ts-2026-01-A:publisher.com:https:auction-123:1738527600"

Security:       Cryptographically binds signature to specific publisher domain,
                scheme, and timestamp - preventing cross-domain replay and MITM

Verification Process (SSP/Exchange Side)

flowchart TD
    Start([Receive OpenRTB Request]) --> Extract[Extract ext.trusted_server]
    Extract --> CheckVersion{version == '1.1'?}

    CheckVersion -->|No| Legacy[Use legacy verification]
    CheckVersion -->|Yes| CheckFields{All fields present?}

    CheckFields -->|Missing| Reject1[❌ Reject: Missing fields]
    CheckFields -->|Present| BuildPayload[Reconstruct payload:<br/>kid:host:scheme:id:ts]

    BuildPayload --> FetchKey[Fetch Ed25519 public key<br/>using kid]
    FetchKey --> KeyFound{Key exists?}

    KeyFound -->|No| Reject2[❌ Reject: Unknown key]
    KeyFound -->|Yes| VerifySig[Verify Ed25519 signature]

    VerifySig --> SigValid{Signature valid?}
    SigValid -->|No| Reject3[❌ Reject: Invalid signature]
    SigValid -->|Yes| CheckTime[Check timestamp freshness]

    CheckTime --> TimeValid{ts within window?<br/>e.g., ±5 minutes}
    TimeValid -->|No| Reject4[❌ Reject: Timestamp too old/new]
    TimeValid -->|Yes| CheckDomain{request_host matches<br/>expected publisher?}

    CheckDomain -->|No| Reject5[❌ Reject: Domain mismatch]
    CheckDomain -->|Yes| CheckScheme{request_scheme == 'https'?}

    CheckScheme -->|No| Reject6[❌ Reject: Invalid scheme]
    CheckScheme -->|Yes| Accept[✅ Accept Request]

    Reject1 --> End([Return 403])
    Reject2 --> End
    Reject3 --> End
    Reject4 --> End
    Reject5 --> End
    Reject6 --> End
    Accept --> Process[Process auction]

   
Loading

Key Benefits

  1. Domain Binding: Signature is cryptographically bound to the originating publisher domain, preventing domain spoofing attacks
  2. Replay Protection: Timestamp (ts) field prevents replay attacks by allowing SSPs to reject stale requests
  3. Scheme Validation: Including the request scheme prevents downgrade attacks from HTTPS to HTTP
  4. Backward Compatible: Version field allows SSPs to handle both v1.0 and v1.1 signatures
  5. Tamper-Proof: Any modification to request_id, host, scheme, or timestamp invalidates the signature

Implementation Notes

  • Timestamp is Unix epoch seconds (u64)
  • Signature is base64url-encoded (no padding) Ed25519 signature
  • Canonical payload format is strictly ordered: kid:request_host:request_scheme:request_id:ts
  • SSPs should reject timestamps outside acceptable window (recommended: ±5 minutes)

@aram356 aram356 assigned aram356 and jevansnyc and unassigned aram356 Feb 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants