diff --git a/crates/common/src/integrations/prebid.rs b/crates/common/src/integrations/prebid.rs index 7fa6589..6aca0c1 100644 --- a/crates/common/src/integrations/prebid.rs +++ b/crates/common/src/integrations/prebid.rs @@ -27,7 +27,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}; use crate::synthetic::{generate_synthetic_id, get_or_generate_synthetic_id}; @@ -526,11 +526,24 @@ fn enhance_openrtb_request( let id = request["id"] .as_str() .expect("should have string id when is_string checked"); + let request_host = get_request_host(req); + let request_scheme = get_request_scheme(req); + let signer = RequestSigner::from_config()?; - let signature = signer.sign(id.as_bytes())?; + let params = SigningParams::new( + id.to_string(), + request_host.clone(), + request_scheme.clone(), + ); + let signature = signer.sign_request(¶ms)?; + request["ext"]["trusted_server"] = json!({ + "version": SIGNING_VERSION, "signature": signature, - "kid": signer.kid + "kid": signer.kid, + "request_host": request_host, + "request_scheme": request_scheme, + "ts": params.timestamp }); } } @@ -719,7 +732,7 @@ impl PrebidAuctionProvider { &self, request: &AuctionRequest, context: &AuctionContext<'_>, - signer: Option<(&RequestSigner, String)>, + signer: Option<(&RequestSigner, String, &SigningParams)>, ) -> OpenRtbRequest { let imps: Vec = request .slots @@ -803,9 +816,16 @@ impl PrebidAuctionProvider { let request_host = get_request_host(context.request); let request_scheme = get_request_scheme(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 { @@ -814,10 +834,12 @@ impl PrebidAuctionProvider { None }, trusted_server: Some(TrustedServerExt { + version, signature, kid, request_host: Some(request_host), request_scheme: Some(request_scheme), + ts, }), }); @@ -938,12 +960,20 @@ 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 request_host = get_request_host(context.request); + let request_scheme = get_request_scheme(context.request); + 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)) + let params = SigningParams::new( + request.id.clone(), + request_host, + request_scheme, + ); + let signature = signer.sign_request(¶ms)?; + Some((signer, signature, params)) } else { None } @@ -957,7 +987,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 b87afc2..2647d66 100644 --- a/crates/common/src/openrtb.rs +++ b/crates/common/src/openrtb.rs @@ -111,6 +111,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")] @@ -119,6 +122,9 @@ pub struct TrustedServerExt { pub request_host: Option, #[serde(skip_serializing_if = "Option::is_none")] pub request_scheme: Option, + /// Unix timestamp 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 6013ff0..55eb2bf 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 + #[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_secs()) + .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. @@ -227,4 +281,84 @@ 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) + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + assert!(params.timestamp <= now); + assert!(params.timestamp >= now - 60); + } + + #[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"); + } }