Skip to content

Commit 431069f

Browse files
jevansnycChristianPavilonis
authored andcommitted
Enhanced request signing with domain verification (v1.1)
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
1 parent b2291fd commit 431069f

3 files changed

Lines changed: 163 additions & 8 deletions

File tree

crates/common/src/integrations/prebid.rs

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ use crate::openrtb::{
2626
Banner, Device, Format, Geo, Imp, ImpExt, OpenRtbRequest, PrebidExt, PrebidImpExt, Regs,
2727
RegsExt, RequestExt, Site, TrustedServerExt, User, UserExt,
2828
};
29-
use crate::request_signing::RequestSigner;
29+
use crate::request_signing::{RequestSigner, SigningParams, SIGNING_VERSION};
3030
use crate::settings::{IntegrationConfig, Settings};
3131

3232
const PREBID_INTEGRATION_ID: &str = "prebid";
@@ -466,7 +466,7 @@ impl PrebidAuctionProvider {
466466
&self,
467467
request: &AuctionRequest,
468468
context: &AuctionContext<'_>,
469-
signer: Option<(&RequestSigner, String)>,
469+
signer: Option<(&RequestSigner, String, &SigningParams)>,
470470
) -> OpenRtbRequest {
471471
let imps: Vec<Imp> = request
472472
.slots
@@ -553,19 +553,28 @@ impl PrebidAuctionProvider {
553553

554554
// Build ext object
555555
let request_info = RequestInfo::from_request(context.request);
556-
let (signature, kid) = signer
557-
.map(|(s, sig)| (Some(sig), Some(s.kid.clone())))
558-
.unwrap_or((None, None));
556+
let (version, signature, kid, ts) = signer
557+
.map(|(s, sig, params)| {
558+
(
559+
Some(SIGNING_VERSION.to_string()),
560+
Some(sig),
561+
Some(s.kid.clone()),
562+
Some(params.timestamp),
563+
)
564+
})
565+
.unwrap_or((None, None, None, None));
559566

560567
let ext = Some(RequestExt {
561568
prebid: Some(PrebidExt {
562569
debug: if self.config.debug { Some(true) } else { None },
563570
}),
564571
trusted_server: Some(TrustedServerExt {
572+
version,
565573
signature,
566574
kid,
567575
request_host: Some(request_info.host),
568576
request_scheme: Some(request_info.scheme),
577+
ts,
569578
}),
570579
});
571580

@@ -689,9 +698,15 @@ impl AuctionProvider for PrebidAuctionProvider {
689698
let signer_with_signature =
690699
if let Some(request_signing_config) = &context.settings.request_signing {
691700
if request_signing_config.enabled {
701+
let request_info = RequestInfo::from_request(context.request);
692702
let signer = RequestSigner::from_config()?;
693-
let signature = signer.sign(request.id.as_bytes())?;
694-
Some((signer, signature))
703+
let params = SigningParams::new(
704+
request.id.clone(),
705+
request_info.host,
706+
request_info.scheme,
707+
);
708+
let signature = signer.sign_request(&params)?;
709+
Some((signer, signature, params))
695710
} else {
696711
None
697712
}
@@ -705,7 +720,7 @@ impl AuctionProvider for PrebidAuctionProvider {
705720
context,
706721
signer_with_signature
707722
.as_ref()
708-
.map(|(s, sig)| (s, sig.clone())),
723+
.map(|(s, sig, params)| (s, sig.clone(), params)),
709724
);
710725

711726
// Log the outgoing OpenRTB request for debugging

crates/common/src/openrtb.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ pub struct PrebidExt {
113113

114114
#[derive(Debug, Serialize, Default)]
115115
pub struct TrustedServerExt {
116+
/// Version of the signing protocol (e.g., "1.1")
117+
#[serde(skip_serializing_if = "Option::is_none")]
118+
pub version: Option<String>,
116119
#[serde(skip_serializing_if = "Option::is_none")]
117120
pub signature: Option<String>,
118121
#[serde(skip_serializing_if = "Option::is_none")]
@@ -121,6 +124,9 @@ pub struct TrustedServerExt {
121124
pub request_host: Option<String>,
122125
#[serde(skip_serializing_if = "Option::is_none")]
123126
pub request_scheme: Option<String>,
127+
/// Unix timestamp for replay protection
128+
#[serde(skip_serializing_if = "Option::is_none")]
129+
pub ts: Option<u64>,
124130
}
125131

126132
#[derive(Debug, Serialize)]

crates/common/src/request_signing/signing.rs

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,45 @@ pub struct RequestSigner {
4545
pub kid: String,
4646
}
4747

48+
/// Current version of the signing protocol
49+
pub const SIGNING_VERSION: &str = "1.1";
50+
51+
/// Parameters for enhanced request signing
52+
#[derive(Debug, Clone)]
53+
pub struct SigningParams {
54+
pub request_id: String,
55+
pub request_host: String,
56+
pub request_scheme: String,
57+
pub timestamp: u64,
58+
}
59+
60+
impl SigningParams {
61+
/// Creates a new `SigningParams` with the current timestamp
62+
#[must_use]
63+
pub fn new(request_id: String, request_host: String, request_scheme: String) -> Self {
64+
Self {
65+
request_id,
66+
request_host,
67+
request_scheme,
68+
timestamp: std::time::SystemTime::now()
69+
.duration_since(std::time::UNIX_EPOCH)
70+
.map(|d| d.as_secs())
71+
.unwrap_or(0),
72+
}
73+
}
74+
75+
/// Builds the canonical payload string for signing.
76+
///
77+
/// Format: `kid:request_host:request_scheme:id:ts`
78+
#[must_use]
79+
pub fn build_payload(&self, kid: &str) -> String {
80+
format!(
81+
"{}:{}:{}:{}:{}",
82+
kid, self.request_host, self.request_scheme, self.request_id, self.timestamp
83+
)
84+
}
85+
}
86+
4887
impl RequestSigner {
4988
/// Creates a `RequestSigner` from the current key ID stored in config.
5089
///
@@ -82,6 +121,21 @@ impl RequestSigner {
82121

83122
Ok(general_purpose::URL_SAFE_NO_PAD.encode(signature_bytes))
84123
}
124+
125+
/// Signs a request using the enhanced v1.1 signing protocol.
126+
///
127+
/// The signed payload format is: `kid:request_host:request_scheme:id:ts`
128+
///
129+
/// # Errors
130+
///
131+
/// Returns an error if signing fails.
132+
pub fn sign_request(
133+
&self,
134+
params: &SigningParams,
135+
) -> Result<String, Report<TrustedServerError>> {
136+
let payload = params.build_payload(&self.kid);
137+
self.sign(payload.as_bytes())
138+
}
85139
}
86140

87141
/// Verifies a signature using the public key associated with the given key ID.
@@ -234,4 +288,84 @@ mod tests {
234288
let result = verify_signature(payload, malformed_signature, &signer.kid);
235289
assert!(result.is_err(), "Should error for malformed signature");
236290
}
291+
292+
#[test]
293+
fn test_signing_params_build_payload() {
294+
let params = SigningParams {
295+
request_id: "req-123".to_string(),
296+
request_host: "example.com".to_string(),
297+
request_scheme: "https".to_string(),
298+
timestamp: 1706900000,
299+
};
300+
301+
let payload = params.build_payload("kid-abc");
302+
assert_eq!(payload, "kid-abc:example.com:https:req-123:1706900000");
303+
}
304+
305+
#[test]
306+
fn test_signing_params_new_creates_timestamp() {
307+
let params = SigningParams::new(
308+
"req-123".to_string(),
309+
"example.com".to_string(),
310+
"https".to_string(),
311+
);
312+
313+
assert_eq!(params.request_id, "req-123");
314+
assert_eq!(params.request_host, "example.com");
315+
assert_eq!(params.request_scheme, "https");
316+
// Timestamp should be recent (within last minute)
317+
let now = std::time::SystemTime::now()
318+
.duration_since(std::time::UNIX_EPOCH)
319+
.unwrap()
320+
.as_secs();
321+
assert!(params.timestamp <= now);
322+
assert!(params.timestamp >= now - 60);
323+
}
324+
325+
#[test]
326+
fn test_sign_request_enhanced() {
327+
let signer = RequestSigner::from_config().unwrap();
328+
let params = SigningParams::new(
329+
"auction-123".to_string(),
330+
"publisher.com".to_string(),
331+
"https".to_string(),
332+
);
333+
334+
let signature = signer.sign_request(&params).unwrap();
335+
assert!(!signature.is_empty());
336+
337+
// Verify the signature is valid by reconstructing the payload
338+
let payload = params.build_payload(&signer.kid);
339+
let result = verify_signature(payload.as_bytes(), &signature, &signer.kid).unwrap();
340+
assert!(result, "Enhanced signature should be valid");
341+
}
342+
343+
#[test]
344+
fn test_sign_request_different_params_different_signature() {
345+
let signer = RequestSigner::from_config().unwrap();
346+
347+
let params1 = SigningParams {
348+
request_id: "req-1".to_string(),
349+
request_host: "host1.com".to_string(),
350+
request_scheme: "https".to_string(),
351+
timestamp: 1706900000,
352+
};
353+
354+
let params2 = SigningParams {
355+
request_id: "req-1".to_string(),
356+
request_host: "host2.com".to_string(), // Different host
357+
request_scheme: "https".to_string(),
358+
timestamp: 1706900000,
359+
};
360+
361+
let sig1 = signer.sign_request(&params1).unwrap();
362+
let sig2 = signer.sign_request(&params2).unwrap();
363+
364+
assert_ne!(sig1, sig2, "Different hosts should produce different signatures");
365+
}
366+
367+
#[test]
368+
fn test_signing_version_constant() {
369+
assert_eq!(SIGNING_VERSION, "1.1");
370+
}
237371
}

0 commit comments

Comments
 (0)