Enhanced request signing with domain verification (v1.1)#220
Enhanced request signing with domain verification (v1.1)#220
Conversation
There was a problem hiding this comment.
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.
Enhanced Request Signing (v1.1) - Architecture DiagramRequest Signing FlowsequenceDiagram
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
Security Propertiesflowchart 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
Attack Preventionflowchart 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
Payload Format EvolutionVersion 1.0 (Legacy)Version 1.1 (Enhanced)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]
Key Benefits
Implementation Notes
|
|
@ChristianPavilonis To finish and resolve conflict |
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
ada15db to
e210989
Compare
| /// | ||
| /// Format: `kid:request_host:request_scheme:id:ts` | ||
| #[must_use] | ||
| pub fn build_payload(&self, kid: &str) -> String { |
There was a problem hiding this comment.
🔧 build_payload formats kid:request_host:request_scheme:id:ts with : as separator, but none of the inputs are validated. request_host comes from extract_request_host() which reads raw header values — X-Forwarded-Host, Forwarded, or Host. A crafted Host: evil.com:https:forged-id:9999 header would produce a payload identical to a legitimate one with different fields, enabling signature confusion.
Options:
Use a json serialize with a proper struct
| } | ||
|
|
||
| #[test] | ||
| fn test_signing_version_constant() { |
| /// Format: `kid:request_host:request_scheme:id:ts` | ||
| #[must_use] | ||
| pub fn build_payload(&self, kid: &str) -> String { | ||
| format!( |
There was a problem hiding this comment.
🔧 Add version to signature
Summary
kid:request_host:request_scheme:id:tsversion("1.1") andts(Unix timestamp) fields toext.trusted_serverThis prevents request tampering and domain spoofing by ensuring the signature is cryptographically bound to the originating publisher domain.
Changes
versionandtsfields toTrustedServerExtSigningParamsstruct,SIGNING_VERSIONconstant, andsign_request()methodPrebidAuctionProviderandenhance_openrtb_requestto use enhanced signingOutput 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" } } }Closes #274
Closes #216
Test plan
cargo build --release)cargo clippy)