diff --git a/crates/common/src/integrations/prebid.rs b/crates/common/src/integrations/prebid.rs index 7c982587..2366c066 100644 --- a/crates/common/src/integrations/prebid.rs +++ b/crates/common/src/integrations/prebid.rs @@ -25,7 +25,7 @@ use crate::openrtb::{ Banner, Device, Format, Geo, Imp, ImpExt, OpenRtbRequest, PrebidExt, PrebidImpExt, Regs, RegsExt, RequestExt, Site, TrustedServerExt, User, UserExt, }; -use crate::request_signing::RequestSigner; +use crate::request_signing::{RequestSigner, SigningParams, SIGNING_VERSION}; use crate::settings::{IntegrationConfig, Settings}; const PREBID_INTEGRATION_ID: &str = "prebid"; @@ -394,7 +394,7 @@ impl PrebidAuctionProvider { &self, request: &AuctionRequest, context: &AuctionContext<'_>, - signer: Option<(&RequestSigner, String)>, + signer: Option<(&RequestSigner, String, &SigningParams)>, ) -> OpenRtbRequest { let imps: Vec = request .slots @@ -475,9 +475,16 @@ impl PrebidAuctionProvider { // Build ext object let request_info = RequestInfo::from_request(context.request); - let (signature, kid) = signer - .map(|(s, sig)| (Some(sig), Some(s.kid.clone()))) - .unwrap_or((None, None)); + let (version, signature, kid, ts) = signer + .map(|(s, sig, params)| { + ( + Some(SIGNING_VERSION.to_string()), + Some(sig), + Some(s.kid.clone()), + Some(params.timestamp), + ) + }) + .unwrap_or((None, None, None, None)); let ext = Some(RequestExt { prebid: if self.config.debug { @@ -486,10 +493,12 @@ impl PrebidAuctionProvider { None }, trusted_server: Some(TrustedServerExt { + version, signature, kid, request_host: Some(request_info.host), request_scheme: Some(request_info.scheme), + ts, }), }); @@ -610,18 +619,22 @@ impl AuctionProvider for PrebidAuctionProvider { log::info!("Prebid: requesting bids for {} slots", request.slots.len()); // Create signer and compute signature if request signing is enabled - let signer_with_signature = - if let Some(request_signing_config) = &context.settings.request_signing { - if request_signing_config.enabled { - let signer = RequestSigner::from_config()?; - let signature = signer.sign(request.id.as_bytes())?; - Some((signer, signature)) - } else { - None - } + let signer_with_signature = if let Some(request_signing_config) = + &context.settings.request_signing + { + if request_signing_config.enabled { + let request_info = RequestInfo::from_request(context.request); + let signer = RequestSigner::from_config()?; + let params = + SigningParams::new(request.id.clone(), request_info.host, request_info.scheme); + let signature = signer.sign_request(¶ms)?; + Some((signer, signature, params)) } else { None - }; + } + } else { + None + }; // Convert to OpenRTB with all enrichments let openrtb = self.to_openrtb( @@ -629,7 +642,7 @@ impl AuctionProvider for PrebidAuctionProvider { context, signer_with_signature .as_ref() - .map(|(s, sig)| (s, sig.clone())), + .map(|(s, sig, params)| (s, sig.clone(), params)), ); // Create HTTP request diff --git a/crates/common/src/openrtb.rs b/crates/common/src/openrtb.rs index 3b405209..fd9a84cd 100644 --- a/crates/common/src/openrtb.rs +++ b/crates/common/src/openrtb.rs @@ -113,6 +113,9 @@ pub struct PrebidExt { #[derive(Debug, Serialize, Default)] pub struct TrustedServerExt { + /// Version of the signing protocol (e.g., "1.1") + #[serde(skip_serializing_if = "Option::is_none")] + pub version: Option, #[serde(skip_serializing_if = "Option::is_none")] pub signature: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -121,6 +124,9 @@ pub struct TrustedServerExt { pub request_host: Option, #[serde(skip_serializing_if = "Option::is_none")] pub request_scheme: Option, + /// Unix timestamp in milliseconds for replay protection + #[serde(skip_serializing_if = "Option::is_none")] + pub ts: Option, } #[derive(Debug, Serialize)] diff --git a/crates/common/src/request_signing/signing.rs b/crates/common/src/request_signing/signing.rs index 1961c780..ac38acbc 100644 --- a/crates/common/src/request_signing/signing.rs +++ b/crates/common/src/request_signing/signing.rs @@ -45,6 +45,45 @@ pub struct RequestSigner { pub kid: String, } +/// Current version of the signing protocol +pub const SIGNING_VERSION: &str = "1.1"; + +/// Parameters for enhanced request signing +#[derive(Debug, Clone)] +pub struct SigningParams { + pub request_id: String, + pub request_host: String, + pub request_scheme: String, + pub timestamp: u64, +} + +impl SigningParams { + /// Creates a new `SigningParams` with the current timestamp in milliseconds + #[must_use] + pub fn new(request_id: String, request_host: String, request_scheme: String) -> Self { + Self { + request_id, + request_host, + request_scheme, + timestamp: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_millis() as u64) + .unwrap_or(0), + } + } + + /// Builds the canonical payload string for signing. + /// + /// Format: `kid:request_host:request_scheme:id:ts` + #[must_use] + pub fn build_payload(&self, kid: &str) -> String { + format!( + "{}:{}:{}:{}:{}", + kid, self.request_host, self.request_scheme, self.request_id, self.timestamp + ) + } +} + impl RequestSigner { /// Creates a `RequestSigner` from the current key ID stored in config. /// @@ -82,6 +121,21 @@ impl RequestSigner { Ok(general_purpose::URL_SAFE_NO_PAD.encode(signature_bytes)) } + + /// Signs a request using the enhanced v1.1 signing protocol. + /// + /// The signed payload format is: `kid:request_host:request_scheme:id:ts` + /// + /// # Errors + /// + /// Returns an error if signing fails. + pub fn sign_request( + &self, + params: &SigningParams, + ) -> Result> { + let payload = params.build_payload(&self.kid); + self.sign(payload.as_bytes()) + } } /// Verifies a signature using the public key associated with the given key ID. @@ -234,4 +288,87 @@ mod tests { let result = verify_signature(payload, malformed_signature, &signer.kid); assert!(result.is_err(), "Should error for malformed signature"); } + + #[test] + fn test_signing_params_build_payload() { + let params = SigningParams { + request_id: "req-123".to_string(), + request_host: "example.com".to_string(), + request_scheme: "https".to_string(), + timestamp: 1706900000, + }; + + let payload = params.build_payload("kid-abc"); + assert_eq!(payload, "kid-abc:example.com:https:req-123:1706900000"); + } + + #[test] + fn test_signing_params_new_creates_timestamp() { + let params = SigningParams::new( + "req-123".to_string(), + "example.com".to_string(), + "https".to_string(), + ); + + assert_eq!(params.request_id, "req-123"); + assert_eq!(params.request_host, "example.com"); + assert_eq!(params.request_scheme, "https"); + // Timestamp should be recent (within last minute), in milliseconds + let now_ms = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() as u64; + assert!(params.timestamp <= now_ms); + assert!(params.timestamp >= now_ms - 60_000); + } + + #[test] + fn test_sign_request_enhanced() { + let signer = RequestSigner::from_config().unwrap(); + let params = SigningParams::new( + "auction-123".to_string(), + "publisher.com".to_string(), + "https".to_string(), + ); + + let signature = signer.sign_request(¶ms).unwrap(); + assert!(!signature.is_empty()); + + // Verify the signature is valid by reconstructing the payload + let payload = params.build_payload(&signer.kid); + let result = verify_signature(payload.as_bytes(), &signature, &signer.kid).unwrap(); + assert!(result, "Enhanced signature should be valid"); + } + + #[test] + fn test_sign_request_different_params_different_signature() { + let signer = RequestSigner::from_config().unwrap(); + + let params1 = SigningParams { + request_id: "req-1".to_string(), + request_host: "host1.com".to_string(), + request_scheme: "https".to_string(), + timestamp: 1706900000, + }; + + let params2 = SigningParams { + request_id: "req-1".to_string(), + request_host: "host2.com".to_string(), // Different host + request_scheme: "https".to_string(), + timestamp: 1706900000, + }; + + let sig1 = signer.sign_request(¶ms1).unwrap(); + let sig2 = signer.sign_request(¶ms2).unwrap(); + + assert_ne!( + sig1, sig2, + "Different hosts should produce different signatures" + ); + } + + #[test] + fn test_signing_version_constant() { + assert_eq!(SIGNING_VERSION, "1.1"); + } }