Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
fb6aac6
feat(relay): add mechanism to only allow events from trusted relays
Litarnus May 26, 2025
d1dc8a9
docs
Litarnus May 26, 2025
59d4fc1
lint
Litarnus May 27, 2025
e946565
fix
Litarnus May 27, 2025
b4bba9a
fix
Litarnus May 27, 2025
80a5a26
Merge branch 'master' into martinl/trusted-relay-signature
Litarnus May 27, 2025
723cdc2
fix
Litarnus May 27, 2025
cbe3183
fix
Litarnus May 27, 2025
92ed36a
fix
Litarnus May 27, 2025
2dc463b
integration tests
Litarnus May 27, 2025
2308ffa
docs
Litarnus May 28, 2025
96a735e
test
Litarnus May 28, 2025
f711040
test
Litarnus May 28, 2025
c37cef3
Merge branch 'master' into martinl/trusted-relay-signature
Litarnus May 28, 2025
260e4a1
changelog
Litarnus May 28, 2025
756254c
feedback
Litarnus May 28, 2025
9616aec
feedback
Litarnus Jun 2, 2025
c0f0e89
move signature in own module
Litarnus Jun 2, 2025
12f2eb8
move signature in own module
Litarnus Jun 3, 2025
f38a7b0
remove file
Litarnus Jun 3, 2025
794afa9
lint
Litarnus Jun 3, 2025
ea3f75d
wip
Litarnus Jun 4, 2025
47d082b
remove additional headers, use SignatureHeader
Litarnus Jun 4, 2025
caf2431
move into relay-auth
Litarnus Jun 4, 2025
d527526
tests
Litarnus Jun 4, 2025
3cc95d7
tests
Litarnus Jun 4, 2025
d09fa22
rename
Litarnus Jun 4, 2025
ee9c04a
test
Litarnus Jun 4, 2025
4127f80
feedback
Litarnus Jun 10, 2025
48c3125
feedback
Litarnus Jun 12, 2025
5447b28
fix
Litarnus Jun 12, 2025
92b67ba
fix
Litarnus Jun 13, 2025
87921b0
feedback
Litarnus Jun 16, 2025
2a69f65
check against received time
Litarnus Jun 16, 2025
d8d3a1b
cleanup
Litarnus Jun 17, 2025
53ef7db
fix
Litarnus Jun 17, 2025
86e61ef
tests
Litarnus Jun 17, 2025
e116d13
feedback
Litarnus Jun 23, 2025
d3873a7
fix
Litarnus Jun 23, 2025
181c13c
fix & docs
Litarnus Jun 23, 2025
8a4c540
use signature type and add extractor
Litarnus Jun 24, 2025
f146f58
add SignatureRef
Litarnus Jun 24, 2025
3b2302b
cleanup
Litarnus Jun 24, 2025
2eb8ac9
fix
Litarnus Jun 24, 2025
f31af99
cleanup
Litarnus Jun 24, 2025
2447a71
Merge branch 'master' into martinl/trusted-relay-signature
Litarnus Jun 24, 2025
972e8a1
changelog
Litarnus Jun 24, 2025
5dc85c3
feedback
Litarnus Jun 26, 2025
0f61900
formatting
Litarnus Jun 26, 2025
581d140
formatting
Litarnus Jun 26, 2025
355457d
fix integration tests
Litarnus Jun 26, 2025
7c68f1d
remove dependency
Litarnus Jun 26, 2025
04f135a
Merge branch 'master' into martinl/trusted-relay-signature
Litarnus Jun 26, 2025
f061803
create RelayStr from bytes
Litarnus Jun 30, 2025
01aef71
use String again in Signature instead of bytes
Litarnus Jun 30, 2025
d2d544d
explicit lifetimes
Litarnus Jun 30, 2025
84b1bde
move signature
Litarnus Jun 30, 2025
7e9b71c
docs
Litarnus Jun 30, 2025
fab7e9b
do not use processing in tests
Litarnus Jul 2, 2025
e4a7840
Merge branch 'master' into martinl/trusted-relay-signature
Litarnus Jul 2, 2025
20bcd33
changelog
Litarnus Jul 2, 2025
c9f31b4
assert all events that are sent
Litarnus Jul 2, 2025
f55cb9f
rename TrySign to Sign, update docs
Litarnus Jul 2, 2025
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

**Features**:

- Add mechanism to allow ingestion only from trusted relays. ([#4772](https://github.com/getsentry/relay/pull/4772))

**Bug Fixes**:

- Preserve user specified event values in Unreal crash reports. ([#4882](https://github.com/getsentry/relay/pull/4882))
Expand Down
185 changes: 165 additions & 20 deletions relay-auth/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
)]

use std::fmt;
use std::fmt::Display;
use std::str::FromStr;

use chrono::{DateTime, Duration, Utc};
Expand Down Expand Up @@ -196,7 +197,7 @@ impl SecretKey {
/// Signs some data with the secret key and returns the signature.
///
/// This is will sign with the default header.
pub fn sign(&self, data: &[u8]) -> String {
pub fn sign(&self, data: &[u8]) -> Signature {
self.sign_with_header(data, &SignatureHeader::default())
}

Expand All @@ -205,7 +206,7 @@ impl SecretKey {
///
/// The default behavior is to attach the timestamp in the header to the
/// signature so that old signatures on verification can be rejected.
pub fn sign_with_header(&self, data: &[u8], header: &SignatureHeader) -> String {
pub fn sign_with_header(&self, data: &[u8], header: &SignatureHeader) -> Signature {
let mut header =
serde_json::to_vec(&header).expect("attempted to pack non json safe header");
let header_encoded = BASE64URL_NOPAD.encode(&header);
Expand All @@ -215,11 +216,11 @@ impl SecretKey {
let mut sig_encoded = BASE64URL_NOPAD.encode(&sig.to_bytes());
sig_encoded.push('.');
sig_encoded.push_str(&header_encoded);
sig_encoded
Signature(sig_encoded)
}

/// Packs some serializable data into JSON and signs it with the default header.
pub fn pack<S: Serialize>(&self, data: S) -> (Vec<u8>, String) {
pub fn pack<S: Serialize>(&self, data: S) -> (Vec<u8>, Signature) {
self.pack_with_header(data, &SignatureHeader::default())
}

Expand All @@ -228,7 +229,7 @@ impl SecretKey {
&self,
data: S,
header: &SignatureHeader,
) -> (Vec<u8>, String) {
) -> (Vec<u8>, Signature) {
// this can only fail if we deal with badly formed data. In that case we
// consider that a panic. Should not happen.
let json = serde_json::to_vec(&data).expect("attempted to pack non json safe data");
Expand Down Expand Up @@ -302,8 +303,8 @@ pub struct PublicKey {
impl PublicKey {
/// Verifies the signature and returns the embedded signature
/// header.
pub fn verify_meta(&self, data: &[u8], sig: &str) -> Option<SignatureHeader> {
let mut iter = sig.splitn(2, '.');
pub fn verify_meta(&self, data: &[u8], sig: SignatureRef<'_>) -> Option<SignatureHeader> {
let mut iter = sig.0.splitn(2, '.');
let sig_bytes = match iter.next() {
Some(sig_encoded) => BASE64URL_NOPAD.decode(sig_encoded.as_bytes()).ok()?,
None => return None,
Expand All @@ -325,12 +326,17 @@ impl PublicKey {
}

/// Verifies a signature but discards the header.
pub fn verify(&self, data: &[u8], sig: &str) -> bool {
pub fn verify(&self, data: &[u8], sig: SignatureRef<'_>) -> bool {
self.verify_meta(data, sig).is_some()
}

/// Verifies a signature and checks the timestamp.
pub fn verify_timestamp(&self, data: &[u8], sig: &str, max_age: Option<Duration>) -> bool {
pub fn verify_timestamp(
&self,
data: &[u8],
sig: SignatureRef<'_>,
max_age: Option<Duration>,
) -> bool {
self.verify_meta(data, sig)
.map(|header| max_age.is_none() || !header.expired(max_age.unwrap()))
.unwrap_or(false)
Expand All @@ -340,7 +346,7 @@ impl PublicKey {
pub fn unpack_meta<D: DeserializeOwned>(
&self,
data: &[u8],
signature: &str,
signature: SignatureRef<'_>,
) -> Result<(SignatureHeader, D), UnpackError> {
if let Some(header) = self.verify_meta(data, signature) {
serde_json::from_slice(data)
Expand All @@ -358,7 +364,7 @@ impl PublicKey {
pub fn unpack<D: DeserializeOwned>(
&self,
data: &[u8],
signature: &str,
signature: SignatureRef<'_>,
max_age: Option<Duration>,
) -> Result<D, UnpackError> {
let (header, data) = self.unpack_meta(data, signature)?;
Expand Down Expand Up @@ -575,7 +581,7 @@ impl RegisterRequest {
/// the data is returned.
pub fn bootstrap_unpack(
data: &[u8],
signature: &str,
signature: SignatureRef<'_>,
max_age: Option<Duration>,
) -> Result<RegisterRequest, UnpackError> {
let req: RegisterRequest = serde_json::from_slice(data).map_err(UnpackError::BadPayload)?;
Expand Down Expand Up @@ -653,7 +659,7 @@ impl RegisterResponse {
/// Unpacks the register response and validates signatures.
pub fn unpack(
data: &[u8],
signature: &str,
signature: SignatureRef<'_>,
secret: &[u8],
max_age: Option<Duration>,
) -> Result<(Self, RegisterState), UnpackError> {
Expand Down Expand Up @@ -687,6 +693,80 @@ impl RegisterResponse {
}
}

/// A wrapper around a String that represents a signature.
#[derive(Debug, Clone, PartialEq)]
pub struct Signature(pub String);

impl Display for Signature {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}

impl Signature {
/// Verifies the signature against any of the provided public keys.
///
/// Returns `true` if the signature is valid with one of the given
/// public keys and satisfies the timestamp constraints defined by `start_time`
/// and `max_age`.
pub fn verify_any(
&self,
public_key: &[PublicKey],
start_time: DateTime<Utc>,
max_age: Duration,
) -> bool {
public_key
.iter()
.any(|p| self.verify(p, start_time, max_age))
}

/// Verifies the signature using the specified public key.
///
/// The signature is considered valid if it can be verified using the given
/// public key and its embedded timestamp falls within the valid time range,
/// starting from `start_time` and not exceeding `max_age`.
pub fn verify(
&self,
public_key: &PublicKey,
start_time: DateTime<Utc>,
max_age: Duration,
) -> bool {
let Some(header) = public_key.verify_meta(&[], self.as_signature_ref()) else {
return false;
};
let Some(timestamp) = header.timestamp else {
return false;
};
let elapsed = start_time - timestamp;
elapsed >= Duration::zero() && elapsed <= max_age
}

/// Verifies the signature against the given data and public key.
///
/// Returns `true` if the signature is valid for the provided `data`
/// when verified with the given public key.
pub fn verify_bytes(&self, data: &[u8], public_key: &PublicKey) -> bool {
public_key
.verify_meta(data, self.as_signature_ref())
.is_some()
}

/// Returns a borrowed view of the signature as a `SignatureRef`.
///
/// This method provides a lightweight reference wrapper over the internal
/// signature data.
pub fn as_signature_ref(&self) -> SignatureRef<'_> {
SignatureRef(self.0.as_str())
}
}

/// A borrowed reference to a signature string used for validation.
///
/// `SignatureRef` provides a view into the signature data as a string slice,
/// allowing verification to work with borrowed data without unnecessary allocations.
/// This type is typically obtained by borrowing from an owned [`Signature`].
pub struct SignatureRef<'a>(pub &'a str);

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -753,10 +833,10 @@ mod tests {
let data = b"Hello World!";

let sig = sk.sign(data);
assert!(pk.verify(data, &sig));
assert!(pk.verify(data, sig.as_signature_ref()));

let bad_sig = "jgubwSf2wb2wuiRpgt2H9_bdDSMr88hXLp5zVuhbr65EGkSxOfT5ILIWr623twLgLd0bDgHg6xzOaUCX7XvUCw";
assert!(!pk.verify(data, bad_sig));
assert!(!pk.verify(data, SignatureRef(bad_sig)));
}

#[test]
Expand All @@ -774,8 +854,12 @@ mod tests {
let (request_bytes, request_sig) = sk.pack(request);

// attempt to get the data through bootstrap unpacking.
let request =
RegisterRequest::bootstrap_unpack(&request_bytes, &request_sig, Some(max_age)).unwrap();
let request = RegisterRequest::bootstrap_unpack(
&request_bytes,
request_sig.as_signature_ref(),
Some(max_age),
)
.unwrap();
assert_eq!(request.relay_id(), relay_id);
assert_eq!(request.public_key(), &pk);

Expand All @@ -800,7 +884,7 @@ mod tests {
let (response_bytes, response_sig) = sk.pack(response);
let (response, _) = RegisterResponse::unpack(
&response_bytes,
&response_sig,
response_sig.as_signature_ref(),
upstream_secret,
Some(max_age),
)
Expand Down Expand Up @@ -836,8 +920,12 @@ mod tests {
println!("REQUEST_SIG = \"{request_sig}\"");

// attempt to get the data through bootstrap unpacking.
let request =
RegisterRequest::bootstrap_unpack(&request_bytes, &request_sig, Some(max_age)).unwrap();
let request = RegisterRequest::bootstrap_unpack(
&request_bytes,
request_sig.as_signature_ref(),
Some(max_age),
)
.unwrap();

let upstream_secret = b"secret";

Expand Down Expand Up @@ -906,4 +994,61 @@ mod tests {
fn test_relay_version_from_str() {
assert_eq!(RelayVersion::new(20, 7, 0), "20.7.0".parse().unwrap());
}

#[test]
fn test_verify_any() {
let pair1 = generate_key_pair();
let pair2 = generate_key_pair();
let pair3 = generate_key_pair();

let signature = pair3.0.sign(&[]);
assert!(signature.verify_any(
&[pair1.1, pair2.1, pair3.1],
Utc::now(),
Duration::seconds(10)
));
}

#[test]
fn test_verify_max_age() {
let pair = generate_key_pair();
let signature = pair.0.sign(&[]);
let start_time = Utc::now();
// The signature is valid in general
assert!(signature.verify(&pair.1, start_time, Duration::seconds(10)));
// Signature is no longer valid because too much time elapsed
assert!(!signature.verify(
&pair.1,
start_time - Duration::seconds(1),
Duration::milliseconds(500)
))
}

#[test]
fn test_verify_any_max_age() {
let start_time = Utc::now();
let pair1 = generate_key_pair();
let pair2 = generate_key_pair();
let pair3 = generate_key_pair();

let header = SignatureHeader {
timestamp: Some(start_time),
};
let signature = pair3.0.sign_with_header(&[], &header);

let public_keys = &[pair1.1, pair2.1, pair3.1];

// Signature still valid after 1 second
assert!(signature.verify_any(
public_keys,
start_time + Duration::seconds(1),
Duration::seconds(2)
));
// Signature is no longer valid because too much time elapsed
assert!(!signature.verify_any(
public_keys,
start_time + Duration::seconds(3),
Duration::seconds(2)
))
}
}
18 changes: 11 additions & 7 deletions relay-cabi/src/auth.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use chrono::Duration;
use relay_auth::{
PublicKey, RegisterRequest, RegisterResponse, RelayId, RelayVersion, SecretKey,
PublicKey, RegisterRequest, RegisterResponse, RelayId, RelayVersion, SecretKey, SignatureRef,
generate_key_pair, generate_relay_id,
};
use serde::Serialize;
Expand Down Expand Up @@ -60,7 +60,8 @@ pub unsafe extern "C" fn relay_publickey_verify(
sig: *const RelayStr,
) -> bool {
let pk = spk as *const PublicKey;
unsafe { (*pk).verify((*data).as_bytes(), (*sig).as_str()) }
let signature = SignatureRef(unsafe { (*sig).as_str() });
unsafe { (*pk).verify((*data).as_bytes(), signature) }
}

/// Verifies a signature
Expand All @@ -74,7 +75,8 @@ pub unsafe extern "C" fn relay_publickey_verify_timestamp(
) -> bool {
let pk = spk as *const PublicKey;
let max_age = Some(Duration::seconds(i64::from(max_age)));
unsafe { (*pk).verify_timestamp((*data).as_bytes(), (*sig).as_str(), max_age) }
let signature = SignatureRef(unsafe { (*sig).as_str() });
unsafe { (*pk).verify_timestamp((*data).as_bytes(), signature, max_age) }
}

/// Parses a secret key from a string.
Expand Down Expand Up @@ -111,7 +113,8 @@ pub unsafe extern "C" fn relay_secretkey_sign(
data: *const RelayBuf,
) -> RelayStr {
let pk = spk as *const SecretKey;
unsafe { RelayStr::from_string((*pk).sign((*data).as_bytes())) }
let signature = unsafe { (*pk).sign((*data).as_bytes()) };
RelayStr::from_string(signature.0)
}

/// Generates a secret, public key pair.
Expand Down Expand Up @@ -146,10 +149,10 @@ pub unsafe extern "C" fn relay_create_register_challenge(
0 => None,
m => Some(Duration::seconds(i64::from(m))),
};
let signature = SignatureRef(unsafe { (*signature).as_str() });

let challenge = unsafe {
let req =
RegisterRequest::bootstrap_unpack((*data).as_bytes(), (*signature).as_str(), max_age)?;
let req = RegisterRequest::bootstrap_unpack((*data).as_bytes(), signature, max_age)?;

req.into_challenge((*secret).as_str().as_bytes())
};
Expand Down Expand Up @@ -179,9 +182,10 @@ pub unsafe extern "C" fn relay_validate_register_response(
m => Some(Duration::seconds(i64::from(m))),
};

let signature = SignatureRef(unsafe { (*signature).as_str() });
let (response, state) = RegisterResponse::unpack(
unsafe { (*data).as_bytes() },
unsafe { (*signature).as_str() },
signature,
unsafe { (*secret).as_str().as_bytes() },
max_age,
)?;
Expand Down
Loading
Loading