Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 40 additions & 10 deletions crates/common/src/integrations/prebid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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(),
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

request_scheme.clone(),
);
let signature = signer.sign_request(&params)?;

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
});
}
}
Expand Down Expand Up @@ -719,7 +732,7 @@ impl PrebidAuctionProvider {
&self,
request: &AuctionRequest,
context: &AuctionContext<'_>,
signer: Option<(&RequestSigner, String)>,
signer: Option<(&RequestSigner, String, &SigningParams)>,
) -> OpenRtbRequest {
let imps: Vec<Imp> = request
.slots
Expand Down Expand Up @@ -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 {
Expand All @@ -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,
}),
});

Expand Down Expand Up @@ -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(&params)?;
Some((signer, signature, params))
} else {
None
}
Expand All @@ -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
Expand Down
6 changes: 6 additions & 0 deletions crates/common/src/openrtb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub signature: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
Expand All @@ -119,6 +122,9 @@ pub struct TrustedServerExt {
pub request_host: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub request_scheme: Option<String>,
/// Unix timestamp for replay protection
#[serde(skip_serializing_if = "Option::is_none")]
pub ts: Option<u64>,
}

#[derive(Debug, Serialize)]
Expand Down
134 changes: 134 additions & 0 deletions crates/common/src/request_signing/signing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
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.

.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.
///
Expand Down Expand Up @@ -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<String, Report<TrustedServerError>> {
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.
Expand Down Expand Up @@ -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(&params).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(&params1).unwrap();
let sig2 = signer.sign_request(&params2).unwrap();

assert_ne!(sig1, sig2, "Different hosts should produce different signatures");
}

#[test]
fn test_signing_version_constant() {
assert_eq!(SIGNING_VERSION, "1.1");
}
}
Loading