Skip to content

Commit a471317

Browse files
authored
feat(relay): add mechanism to only allow events from trusted relays (#4772)
This PR adds a signature to requests between relays which is used to verify if the request comes from a trusted relay. It leverages the signature mechanism used by `relay-auth` which already includes a timestamp which can be checked for a `max_age`. The signature itself does not contain any data since we are not interested in data integrity and we just want to see if we can successfully verify the signature by any of the public keys that are added by the user. Envelopes coming from internal relays are not checked because they are already in our infrastructure and are considered trusted. The `max_age` for envelopes from external relays is set to 5 Minutes currently. ref RELAY-17
1 parent 9106270 commit a471317

File tree

17 files changed

+857
-60
lines changed

17 files changed

+857
-60
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
**Features**:
6+
7+
- Add mechanism to allow ingestion only from trusted relays. ([#4772](https://github.com/getsentry/relay/pull/4772))
8+
59
**Bug Fixes**:
610

711
- Preserve user specified event values in Unreal crash reports. ([#4882](https://github.com/getsentry/relay/pull/4882))

relay-auth/src/lib.rs

Lines changed: 165 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
)]
2424

2525
use std::fmt;
26+
use std::fmt::Display;
2627
use std::str::FromStr;
2728

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

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

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

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

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

332333
/// Verifies a signature and checks the timestamp.
333-
pub fn verify_timestamp(&self, data: &[u8], sig: &str, max_age: Option<Duration>) -> bool {
334+
pub fn verify_timestamp(
335+
&self,
336+
data: &[u8],
337+
sig: SignatureRef<'_>,
338+
max_age: Option<Duration>,
339+
) -> bool {
334340
self.verify_meta(data, sig)
335341
.map(|header| max_age.is_none() || !header.expired(max_age.unwrap()))
336342
.unwrap_or(false)
@@ -340,7 +346,7 @@ impl PublicKey {
340346
pub fn unpack_meta<D: DeserializeOwned>(
341347
&self,
342348
data: &[u8],
343-
signature: &str,
349+
signature: SignatureRef<'_>,
344350
) -> Result<(SignatureHeader, D), UnpackError> {
345351
if let Some(header) = self.verify_meta(data, signature) {
346352
serde_json::from_slice(data)
@@ -358,7 +364,7 @@ impl PublicKey {
358364
pub fn unpack<D: DeserializeOwned>(
359365
&self,
360366
data: &[u8],
361-
signature: &str,
367+
signature: SignatureRef<'_>,
362368
max_age: Option<Duration>,
363369
) -> Result<D, UnpackError> {
364370
let (header, data) = self.unpack_meta(data, signature)?;
@@ -575,7 +581,7 @@ impl RegisterRequest {
575581
/// the data is returned.
576582
pub fn bootstrap_unpack(
577583
data: &[u8],
578-
signature: &str,
584+
signature: SignatureRef<'_>,
579585
max_age: Option<Duration>,
580586
) -> Result<RegisterRequest, UnpackError> {
581587
let req: RegisterRequest = serde_json::from_slice(data).map_err(UnpackError::BadPayload)?;
@@ -653,7 +659,7 @@ impl RegisterResponse {
653659
/// Unpacks the register response and validates signatures.
654660
pub fn unpack(
655661
data: &[u8],
656-
signature: &str,
662+
signature: SignatureRef<'_>,
657663
secret: &[u8],
658664
max_age: Option<Duration>,
659665
) -> Result<(Self, RegisterState), UnpackError> {
@@ -687,6 +693,80 @@ impl RegisterResponse {
687693
}
688694
}
689695

696+
/// A wrapper around a String that represents a signature.
697+
#[derive(Debug, Clone, PartialEq)]
698+
pub struct Signature(pub String);
699+
700+
impl Display for Signature {
701+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
702+
write!(f, "{}", self.0)
703+
}
704+
}
705+
706+
impl Signature {
707+
/// Verifies the signature against any of the provided public keys.
708+
///
709+
/// Returns `true` if the signature is valid with one of the given
710+
/// public keys and satisfies the timestamp constraints defined by `start_time`
711+
/// and `max_age`.
712+
pub fn verify_any(
713+
&self,
714+
public_key: &[PublicKey],
715+
start_time: DateTime<Utc>,
716+
max_age: Duration,
717+
) -> bool {
718+
public_key
719+
.iter()
720+
.any(|p| self.verify(p, start_time, max_age))
721+
}
722+
723+
/// Verifies the signature using the specified public key.
724+
///
725+
/// The signature is considered valid if it can be verified using the given
726+
/// public key and its embedded timestamp falls within the valid time range,
727+
/// starting from `start_time` and not exceeding `max_age`.
728+
pub fn verify(
729+
&self,
730+
public_key: &PublicKey,
731+
start_time: DateTime<Utc>,
732+
max_age: Duration,
733+
) -> bool {
734+
let Some(header) = public_key.verify_meta(&[], self.as_signature_ref()) else {
735+
return false;
736+
};
737+
let Some(timestamp) = header.timestamp else {
738+
return false;
739+
};
740+
let elapsed = start_time - timestamp;
741+
elapsed >= Duration::zero() && elapsed <= max_age
742+
}
743+
744+
/// Verifies the signature against the given data and public key.
745+
///
746+
/// Returns `true` if the signature is valid for the provided `data`
747+
/// when verified with the given public key.
748+
pub fn verify_bytes(&self, data: &[u8], public_key: &PublicKey) -> bool {
749+
public_key
750+
.verify_meta(data, self.as_signature_ref())
751+
.is_some()
752+
}
753+
754+
/// Returns a borrowed view of the signature as a `SignatureRef`.
755+
///
756+
/// This method provides a lightweight reference wrapper over the internal
757+
/// signature data.
758+
pub fn as_signature_ref(&self) -> SignatureRef<'_> {
759+
SignatureRef(self.0.as_str())
760+
}
761+
}
762+
763+
/// A borrowed reference to a signature string used for validation.
764+
///
765+
/// `SignatureRef` provides a view into the signature data as a string slice,
766+
/// allowing verification to work with borrowed data without unnecessary allocations.
767+
/// This type is typically obtained by borrowing from an owned [`Signature`].
768+
pub struct SignatureRef<'a>(pub &'a str);
769+
690770
#[cfg(test)]
691771
mod tests {
692772
use super::*;
@@ -753,10 +833,10 @@ mod tests {
753833
let data = b"Hello World!";
754834

755835
let sig = sk.sign(data);
756-
assert!(pk.verify(data, &sig));
836+
assert!(pk.verify(data, sig.as_signature_ref()));
757837

758838
let bad_sig = "jgubwSf2wb2wuiRpgt2H9_bdDSMr88hXLp5zVuhbr65EGkSxOfT5ILIWr623twLgLd0bDgHg6xzOaUCX7XvUCw";
759-
assert!(!pk.verify(data, bad_sig));
839+
assert!(!pk.verify(data, SignatureRef(bad_sig)));
760840
}
761841

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

776856
// attempt to get the data through bootstrap unpacking.
777-
let request =
778-
RegisterRequest::bootstrap_unpack(&request_bytes, &request_sig, Some(max_age)).unwrap();
857+
let request = RegisterRequest::bootstrap_unpack(
858+
&request_bytes,
859+
request_sig.as_signature_ref(),
860+
Some(max_age),
861+
)
862+
.unwrap();
779863
assert_eq!(request.relay_id(), relay_id);
780864
assert_eq!(request.public_key(), &pk);
781865

@@ -800,7 +884,7 @@ mod tests {
800884
let (response_bytes, response_sig) = sk.pack(response);
801885
let (response, _) = RegisterResponse::unpack(
802886
&response_bytes,
803-
&response_sig,
887+
response_sig.as_signature_ref(),
804888
upstream_secret,
805889
Some(max_age),
806890
)
@@ -836,8 +920,12 @@ mod tests {
836920
println!("REQUEST_SIG = \"{request_sig}\"");
837921

838922
// attempt to get the data through bootstrap unpacking.
839-
let request =
840-
RegisterRequest::bootstrap_unpack(&request_bytes, &request_sig, Some(max_age)).unwrap();
923+
let request = RegisterRequest::bootstrap_unpack(
924+
&request_bytes,
925+
request_sig.as_signature_ref(),
926+
Some(max_age),
927+
)
928+
.unwrap();
841929

842930
let upstream_secret = b"secret";
843931

@@ -906,4 +994,61 @@ mod tests {
906994
fn test_relay_version_from_str() {
907995
assert_eq!(RelayVersion::new(20, 7, 0), "20.7.0".parse().unwrap());
908996
}
997+
998+
#[test]
999+
fn test_verify_any() {
1000+
let pair1 = generate_key_pair();
1001+
let pair2 = generate_key_pair();
1002+
let pair3 = generate_key_pair();
1003+
1004+
let signature = pair3.0.sign(&[]);
1005+
assert!(signature.verify_any(
1006+
&[pair1.1, pair2.1, pair3.1],
1007+
Utc::now(),
1008+
Duration::seconds(10)
1009+
));
1010+
}
1011+
1012+
#[test]
1013+
fn test_verify_max_age() {
1014+
let pair = generate_key_pair();
1015+
let signature = pair.0.sign(&[]);
1016+
let start_time = Utc::now();
1017+
// The signature is valid in general
1018+
assert!(signature.verify(&pair.1, start_time, Duration::seconds(10)));
1019+
// Signature is no longer valid because too much time elapsed
1020+
assert!(!signature.verify(
1021+
&pair.1,
1022+
start_time - Duration::seconds(1),
1023+
Duration::milliseconds(500)
1024+
))
1025+
}
1026+
1027+
#[test]
1028+
fn test_verify_any_max_age() {
1029+
let start_time = Utc::now();
1030+
let pair1 = generate_key_pair();
1031+
let pair2 = generate_key_pair();
1032+
let pair3 = generate_key_pair();
1033+
1034+
let header = SignatureHeader {
1035+
timestamp: Some(start_time),
1036+
};
1037+
let signature = pair3.0.sign_with_header(&[], &header);
1038+
1039+
let public_keys = &[pair1.1, pair2.1, pair3.1];
1040+
1041+
// Signature still valid after 1 second
1042+
assert!(signature.verify_any(
1043+
public_keys,
1044+
start_time + Duration::seconds(1),
1045+
Duration::seconds(2)
1046+
));
1047+
// Signature is no longer valid because too much time elapsed
1048+
assert!(!signature.verify_any(
1049+
public_keys,
1050+
start_time + Duration::seconds(3),
1051+
Duration::seconds(2)
1052+
))
1053+
}
9091054
}

relay-cabi/src/auth.rs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use chrono::Duration;
22
use relay_auth::{
3-
PublicKey, RegisterRequest, RegisterResponse, RelayId, RelayVersion, SecretKey,
3+
PublicKey, RegisterRequest, RegisterResponse, RelayId, RelayVersion, SecretKey, SignatureRef,
44
generate_key_pair, generate_relay_id,
55
};
66
use serde::Serialize;
@@ -60,7 +60,8 @@ pub unsafe extern "C" fn relay_publickey_verify(
6060
sig: *const RelayStr,
6161
) -> bool {
6262
let pk = spk as *const PublicKey;
63-
unsafe { (*pk).verify((*data).as_bytes(), (*sig).as_str()) }
63+
let signature = SignatureRef(unsafe { (*sig).as_str() });
64+
unsafe { (*pk).verify((*data).as_bytes(), signature) }
6465
}
6566

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

8082
/// Parses a secret key from a string.
@@ -111,7 +113,8 @@ pub unsafe extern "C" fn relay_secretkey_sign(
111113
data: *const RelayBuf,
112114
) -> RelayStr {
113115
let pk = spk as *const SecretKey;
114-
unsafe { RelayStr::from_string((*pk).sign((*data).as_bytes())) }
116+
let signature = unsafe { (*pk).sign((*data).as_bytes()) };
117+
RelayStr::from_string(signature.0)
115118
}
116119

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

150154
let challenge = unsafe {
151-
let req =
152-
RegisterRequest::bootstrap_unpack((*data).as_bytes(), (*signature).as_str(), max_age)?;
155+
let req = RegisterRequest::bootstrap_unpack((*data).as_bytes(), signature, max_age)?;
153156

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

185+
let signature = SignatureRef(unsafe { (*signature).as_str() });
182186
let (response, state) = RegisterResponse::unpack(
183187
unsafe { (*data).as_bytes() },
184-
unsafe { (*signature).as_str() },
188+
signature,
185189
unsafe { (*secret).as_str().as_bytes() },
186190
max_age,
187191
)?;

0 commit comments

Comments
 (0)