mirror of
https://github.com/block/buzz.git
synced 2026-11-05 00:41:28 +00:00
feat(push): add gated development App Attest bypass
Co-authored-by: Tom Brow <tomb@block.xyz> Signed-off-by: Tom Brow <tomb@block.xyz>
This commit is contained in:
committed by
npub12wpjffj7q5qjsky5jvk4ldwlxmse5xll3d8gytk4wqd0c5y7jvwspg37n6
parent
0e3389a037
commit
5d236db269
@@ -7,6 +7,10 @@ rust-version.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
dev-app-attest-bypass = []
|
||||
|
||||
[lib]
|
||||
name = "buzz_push_gateway"
|
||||
path = "src/lib.rs"
|
||||
|
||||
@@ -183,6 +183,7 @@ impl PushTransport for ApnsTransport {
|
||||
profile: AppProfile,
|
||||
endpoint: &str,
|
||||
) -> DeliveryOutcome {
|
||||
crate::metrics::record_apns_send_attempt();
|
||||
let response = self.send_response(attempt, profile, endpoint).await;
|
||||
let response = match response {
|
||||
Ok(response) => response,
|
||||
|
||||
82
crates/buzz-push-gateway/src/app_attest_policy.rs
Normal file
82
crates/buzz-push-gateway/src/app_attest_policy.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
//! Selects the production Apple verifier or the feature-gated development stub.
|
||||
|
||||
use crate::app_attest::{
|
||||
AppAttestError, AppAttestVerifier, VerifiedAssertion, VerifiedAttestation,
|
||||
};
|
||||
|
||||
#[cfg_attr(
|
||||
not(feature = "dev-app-attest-bypass"),
|
||||
doc = r#"
|
||||
The development policy is structurally unavailable in default builds:
|
||||
|
||||
```compile_fail
|
||||
use buzz_push_gateway::app_attest_policy::AppAttestPolicy;
|
||||
|
||||
let _ = AppAttestPolicy::Development;
|
||||
```
|
||||
"#
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
pub enum AppAttestPolicy {
|
||||
Apple(AppAttestVerifier),
|
||||
#[cfg(feature = "dev-app-attest-bypass")]
|
||||
Development,
|
||||
}
|
||||
|
||||
impl AppAttestPolicy {
|
||||
pub fn apple(verifier: AppAttestVerifier) -> Self {
|
||||
Self::Apple(verifier)
|
||||
}
|
||||
|
||||
#[cfg(feature = "dev-app-attest-bypass")]
|
||||
pub fn development() -> Self {
|
||||
Self::Development
|
||||
}
|
||||
|
||||
pub fn verify_attestation(
|
||||
&self,
|
||||
attestation_b64: &str,
|
||||
key_id_b64: &str,
|
||||
client_data: &[u8],
|
||||
) -> Result<VerifiedAttestation, AppAttestError> {
|
||||
match self {
|
||||
Self::Apple(verifier) => {
|
||||
verifier.verify_attestation(attestation_b64, key_id_b64, client_data)
|
||||
}
|
||||
#[cfg(feature = "dev-app-attest-bypass")]
|
||||
Self::Development => {
|
||||
crate::dev_app_attest::verify_attestation(attestation_b64, key_id_b64, client_data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify_assertion(
|
||||
&self,
|
||||
assertion_b64: &str,
|
||||
client_data: &[u8],
|
||||
public_key: &[u8],
|
||||
previous_counter: u32,
|
||||
challenge: &str,
|
||||
stored_challenge: &str,
|
||||
) -> Result<VerifiedAssertion, AppAttestError> {
|
||||
match self {
|
||||
Self::Apple(verifier) => verifier.verify_assertion(
|
||||
assertion_b64,
|
||||
client_data,
|
||||
public_key,
|
||||
previous_counter,
|
||||
challenge,
|
||||
stored_challenge,
|
||||
),
|
||||
#[cfg(feature = "dev-app-attest-bypass")]
|
||||
Self::Development => crate::dev_app_attest::verify_assertion(
|
||||
assertion_b64,
|
||||
client_data,
|
||||
public_key,
|
||||
previous_counter,
|
||||
challenge,
|
||||
stored_challenge,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,8 @@ pub struct Config {
|
||||
pub database_url: String,
|
||||
pub app_attest_app_id: String,
|
||||
pub app_attest_root_cert_path: PathBuf,
|
||||
#[cfg(feature = "dev-app-attest-bypass")]
|
||||
pub dev_app_attest_bypass: bool,
|
||||
/// Ordered current key first, followed by decrypt-only predecessors.
|
||||
pub grant_keys: Vec<KeyConfig>,
|
||||
/// Independent token-custody keyring. These keys MUST NOT be reused for
|
||||
@@ -150,19 +152,38 @@ impl Config {
|
||||
if enabled_profiles.is_empty() {
|
||||
return Err(ConfigError::Invalid("BUZZ_PUSH_ENABLED_PROFILES"));
|
||||
}
|
||||
let bind_addr = e
|
||||
.get("BUZZ_PUSH_BIND_ADDR")
|
||||
.map(String::as_str)
|
||||
.unwrap_or("0.0.0.0:8080")
|
||||
.parse::<SocketAddr>()
|
||||
.map_err(|_| ConfigError::Invalid("BUZZ_PUSH_BIND_ADDR"))?;
|
||||
let health_addr = e
|
||||
.get("BUZZ_PUSH_HEALTH_ADDR")
|
||||
.map(String::as_str)
|
||||
.unwrap_or("0.0.0.0:8081")
|
||||
.parse::<SocketAddr>()
|
||||
.map_err(|_| ConfigError::Invalid("BUZZ_PUSH_HEALTH_ADDR"))?;
|
||||
#[cfg(feature = "dev-app-attest-bypass")]
|
||||
let dev_app_attest_bypass = e
|
||||
.get("BUZZ_PUSH_DEV_APP_ATTEST_BYPASS")
|
||||
.is_some_and(|value| value == "true");
|
||||
#[cfg(feature = "dev-app-attest-bypass")]
|
||||
if dev_app_attest_bypass
|
||||
&& (!bind_addr.ip().is_loopback() || !health_addr.ip().is_loopback())
|
||||
{
|
||||
return Err(ConfigError::Invalid("BUZZ_PUSH_DEV_APP_ATTEST_BYPASS"));
|
||||
}
|
||||
#[cfg(feature = "dev-app-attest-bypass")]
|
||||
if dev_app_attest_bypass
|
||||
&& (enabled_profiles.len() != 1
|
||||
|| !enabled_profiles.contains(&crate::model::AppProfile::BuzzIosSandbox))
|
||||
{
|
||||
return Err(ConfigError::Invalid("BUZZ_PUSH_DEV_APP_ATTEST_BYPASS"));
|
||||
}
|
||||
Ok(Self {
|
||||
bind_addr: e
|
||||
.get("BUZZ_PUSH_BIND_ADDR")
|
||||
.map(String::as_str)
|
||||
.unwrap_or("0.0.0.0:8080")
|
||||
.parse()
|
||||
.map_err(|_| ConfigError::Invalid("BUZZ_PUSH_BIND_ADDR"))?,
|
||||
health_addr: e
|
||||
.get("BUZZ_PUSH_HEALTH_ADDR")
|
||||
.map(String::as_str)
|
||||
.unwrap_or("0.0.0.0:8081")
|
||||
.parse()
|
||||
.map_err(|_| ConfigError::Invalid("BUZZ_PUSH_HEALTH_ADDR"))?,
|
||||
bind_addr,
|
||||
health_addr,
|
||||
public_delivery_url,
|
||||
max_grant_lifetime_seconds,
|
||||
max_installation_lifetime_seconds,
|
||||
@@ -172,6 +193,8 @@ impl Config {
|
||||
database_url: req(e, "DATABASE_URL")?.to_owned(),
|
||||
app_attest_app_id: req(e, "BUZZ_PUSH_APP_ATTEST_APP_ID")?.to_owned(),
|
||||
app_attest_root_cert_path: req(e, "BUZZ_PUSH_APP_ATTEST_ROOT_CERT_PATH")?.into(),
|
||||
#[cfg(feature = "dev-app-attest-bypass")]
|
||||
dev_app_attest_bypass,
|
||||
grant_keys,
|
||||
token_keys,
|
||||
apns_cert_path: req(e, "BUZZ_PUSH_APNS_CERT_PATH")?.into(),
|
||||
@@ -225,6 +248,8 @@ mod tests {
|
||||
),
|
||||
("BUZZ_PUSH_APNS_CERT_PATH".into(), "/identity.pem".into()),
|
||||
("BUZZ_PUSH_APNS_TOPIC".into(), "app".into()),
|
||||
("BUZZ_PUSH_BIND_ADDR".into(), "127.0.0.1:8080".into()),
|
||||
("BUZZ_PUSH_HEALTH_ADDR".into(), "127.0.0.1:8081".into()),
|
||||
])
|
||||
}
|
||||
|
||||
@@ -288,6 +313,135 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn listener_defaults_remain_public_when_addresses_are_absent() {
|
||||
let mut env = base();
|
||||
env.remove("BUZZ_PUSH_BIND_ADDR");
|
||||
env.remove("BUZZ_PUSH_HEALTH_ADDR");
|
||||
|
||||
let config = Config::from_map(&env).unwrap();
|
||||
assert_eq!(config.bind_addr, "0.0.0.0:8080".parse().unwrap());
|
||||
assert_eq!(config.health_addr, "0.0.0.0:8081".parse().unwrap());
|
||||
}
|
||||
|
||||
#[cfg(feature = "dev-app-attest-bypass")]
|
||||
#[test]
|
||||
fn dev_app_attest_bypass_is_off_when_absent_or_exactly_false() {
|
||||
let absent = Config::from_map(&base()).unwrap();
|
||||
assert!(!absent.dev_app_attest_bypass);
|
||||
|
||||
let mut explicit_false = base();
|
||||
explicit_false.insert("BUZZ_PUSH_DEV_APP_ATTEST_BYPASS".into(), "false".into());
|
||||
let explicit_false = Config::from_map(&explicit_false).unwrap();
|
||||
assert!(!explicit_false.dev_app_attest_bypass);
|
||||
}
|
||||
|
||||
#[cfg(feature = "dev-app-attest-bypass")]
|
||||
#[test]
|
||||
fn dev_app_attest_bypass_selects_apple_for_every_value_other_than_exact_true() {
|
||||
for value in [
|
||||
None,
|
||||
Some(""),
|
||||
Some("false"),
|
||||
Some("TRUE"),
|
||||
Some("1"),
|
||||
Some("yes"),
|
||||
Some(" true"),
|
||||
] {
|
||||
let mut env = base();
|
||||
if let Some(value) = value {
|
||||
env.insert("BUZZ_PUSH_DEV_APP_ATTEST_BYPASS".into(), value.into());
|
||||
}
|
||||
let config = Config::from_map(&env).unwrap();
|
||||
assert!(!config.dev_app_attest_bypass, "enabled for {value:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "dev-app-attest-bypass")]
|
||||
#[test]
|
||||
fn dev_app_attest_bypass_requires_both_loopback_listeners() {
|
||||
for (key, value) in [
|
||||
("BUZZ_PUSH_BIND_ADDR", "0.0.0.0:8080"),
|
||||
("BUZZ_PUSH_HEALTH_ADDR", "[::]:8081"),
|
||||
] {
|
||||
let mut env = base();
|
||||
env.insert("BUZZ_PUSH_DEV_APP_ATTEST_BYPASS".into(), "true".into());
|
||||
env.insert(
|
||||
"BUZZ_PUSH_ENABLED_PROFILES".into(),
|
||||
"buzz-ios-sandbox".into(),
|
||||
);
|
||||
env.insert(key.into(), value.into());
|
||||
assert!(Config::from_map(&env).is_err(), "accepted {key}={value}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "dev-app-attest-bypass")]
|
||||
#[test]
|
||||
fn non_loopback_bind_is_rejected_before_loopback_equivalent_is_accepted() {
|
||||
let mut non_loopback = base();
|
||||
non_loopback.insert("BUZZ_PUSH_DEV_APP_ATTEST_BYPASS".into(), "true".into());
|
||||
non_loopback.insert(
|
||||
"BUZZ_PUSH_ENABLED_PROFILES".into(),
|
||||
"buzz-ios-sandbox".into(),
|
||||
);
|
||||
non_loopback.insert("BUZZ_PUSH_BIND_ADDR".into(), "0.0.0.0:8080".into());
|
||||
assert!(Config::from_map(&non_loopback).is_err());
|
||||
|
||||
non_loopback.insert("BUZZ_PUSH_BIND_ADDR".into(), "127.0.0.1:8080".into());
|
||||
assert!(
|
||||
Config::from_map(&non_loopback)
|
||||
.unwrap()
|
||||
.dev_app_attest_bypass
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "dev-app-attest-bypass")]
|
||||
#[test]
|
||||
fn dev_app_attest_bypass_requires_sandbox_as_the_only_profile() {
|
||||
for profiles in [
|
||||
"buzz-ios-production",
|
||||
"buzz-ios-sandbox,buzz-ios-production",
|
||||
] {
|
||||
let mut env = base();
|
||||
env.insert("BUZZ_PUSH_DEV_APP_ATTEST_BYPASS".into(), "true".into());
|
||||
env.insert("BUZZ_PUSH_ENABLED_PROFILES".into(), profiles.into());
|
||||
assert!(
|
||||
Config::from_map(&env).is_err(),
|
||||
"accepted profiles {profiles}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "dev-app-attest-bypass")]
|
||||
#[test]
|
||||
fn dev_app_attest_bypass_accepts_explicit_true_for_loopback_sandbox() {
|
||||
let mut env = base();
|
||||
env.insert("BUZZ_PUSH_DEV_APP_ATTEST_BYPASS".into(), "true".into());
|
||||
env.insert(
|
||||
"BUZZ_PUSH_ENABLED_PROFILES".into(),
|
||||
"buzz-ios-sandbox".into(),
|
||||
);
|
||||
assert!(Config::from_map(&env).unwrap().dev_app_attest_bypass);
|
||||
}
|
||||
|
||||
#[cfg(feature = "dev-app-attest-bypass")]
|
||||
#[test]
|
||||
fn second_profile_is_rejected_before_sandbox_only_is_accepted() {
|
||||
let mut env = base();
|
||||
env.insert("BUZZ_PUSH_DEV_APP_ATTEST_BYPASS".into(), "true".into());
|
||||
env.insert(
|
||||
"BUZZ_PUSH_ENABLED_PROFILES".into(),
|
||||
"buzz-ios-sandbox,buzz-ios-production".into(),
|
||||
);
|
||||
assert!(Config::from_map(&env).is_err());
|
||||
|
||||
env.insert(
|
||||
"BUZZ_PUSH_ENABLED_PROFILES".into(),
|
||||
"buzz-ios-sandbox".into(),
|
||||
);
|
||||
assert!(Config::from_map(&env).unwrap().dev_app_attest_bypass);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_or_empty_keyrings_fail_startup() {
|
||||
for (variable, value) in [
|
||||
|
||||
168
crates/buzz-push-gateway/src/dev_app_attest.rs
Normal file
168
crates/buzz-push-gateway/src/dev_app_attest.rs
Normal file
@@ -0,0 +1,168 @@
|
||||
//! Explicit development-only App Attest sentinel verification.
|
||||
//!
|
||||
//! This module is absent unless the non-default `dev-app-attest-bypass` Cargo
|
||||
//! feature is enabled. Runtime configuration adds a second, independent gate.
|
||||
|
||||
use crate::app_attest::{AppAttestError, VerifiedAssertion, VerifiedAttestation};
|
||||
use base64::{engine::general_purpose::STANDARD, Engine as _};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
const ATTESTATION_PREFIX: &[u8] = b"buzz-dev-app-attest-v1:";
|
||||
const ASSERTION_SENTINEL: &[u8] = b"buzz-dev-app-assertion-v1";
|
||||
const PUBLIC_KEY_PREFIX: &[u8] = b"buzz-dev-app-attest-public-key-v1:";
|
||||
const NONCE_BYTES: usize = 32;
|
||||
|
||||
pub fn assertion_sentinel() -> String {
|
||||
STANDARD.encode(ASSERTION_SENTINEL)
|
||||
}
|
||||
|
||||
pub fn verify_attestation(
|
||||
attestation_b64: &str,
|
||||
key_id_b64: &str,
|
||||
client_data: &[u8],
|
||||
) -> Result<VerifiedAttestation, AppAttestError> {
|
||||
let attestation = STANDARD
|
||||
.decode(attestation_b64)
|
||||
.map_err(|_| AppAttestError::Invalid)?;
|
||||
let supplied_key_id = STANDARD
|
||||
.decode(key_id_b64)
|
||||
.map_err(|_| AppAttestError::Invalid)?;
|
||||
let expected_key_id = Sha256::digest(&attestation);
|
||||
if !attestation.starts_with(ATTESTATION_PREFIX)
|
||||
|| attestation.len() != ATTESTATION_PREFIX.len() + NONCE_BYTES
|
||||
|| supplied_key_id.as_slice() != expected_key_id.as_slice()
|
||||
|| client_data.is_empty()
|
||||
{
|
||||
return Err(AppAttestError::Invalid);
|
||||
}
|
||||
let mut public_key = PUBLIC_KEY_PREFIX.to_vec();
|
||||
public_key.extend_from_slice(&expected_key_id);
|
||||
Ok(VerifiedAttestation {
|
||||
key_id: expected_key_id.to_vec(),
|
||||
public_key,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn verify_assertion(
|
||||
assertion_b64: &str,
|
||||
client_data: &[u8],
|
||||
public_key: &[u8],
|
||||
previous_counter: u32,
|
||||
challenge: &str,
|
||||
stored_challenge: &str,
|
||||
) -> Result<VerifiedAssertion, AppAttestError> {
|
||||
let assertion = STANDARD
|
||||
.decode(assertion_b64)
|
||||
.map_err(|_| AppAttestError::Invalid)?;
|
||||
if assertion != ASSERTION_SENTINEL
|
||||
|| client_data.is_empty()
|
||||
|| !public_key.starts_with(PUBLIC_KEY_PREFIX)
|
||||
|| public_key.len() != PUBLIC_KEY_PREFIX.len() + 32
|
||||
|| challenge.is_empty()
|
||||
|| challenge != stored_challenge
|
||||
{
|
||||
return Err(AppAttestError::Invalid);
|
||||
}
|
||||
Ok(VerifiedAssertion {
|
||||
counter: previous_counter
|
||||
.checked_add(1)
|
||||
.ok_or(AppAttestError::Invalid)?,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn attestation(nonce: u8) -> (String, String) {
|
||||
let mut sentinel = ATTESTATION_PREFIX.to_vec();
|
||||
sentinel.extend_from_slice(&[nonce; NONCE_BYTES]);
|
||||
let key_id = Sha256::digest(&sentinel);
|
||||
(STANDARD.encode(sentinel), STANDARD.encode(key_id))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn versioned_attestation_derives_a_unique_bound_key_id() {
|
||||
let (attestation_a, key_id_a) = attestation(1);
|
||||
let verified_a = verify_attestation(
|
||||
&attestation_a,
|
||||
&key_id_a,
|
||||
b"canonical enrollment transcript",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(verified_a.key_id.len(), 32);
|
||||
assert!(verified_a.public_key.starts_with(PUBLIC_KEY_PREFIX));
|
||||
|
||||
let (attestation_b, key_id_b) = attestation(2);
|
||||
let verified_b = verify_attestation(
|
||||
&attestation_b,
|
||||
&key_id_b,
|
||||
b"canonical enrollment transcript",
|
||||
)
|
||||
.unwrap();
|
||||
assert_ne!(verified_a.key_id, verified_b.key_id);
|
||||
|
||||
for (attestation, key, transcript) in [
|
||||
("bad".to_owned(), key_id_a.clone(), b"transcript".as_slice()),
|
||||
(attestation_a.clone(), key_id_b, b"transcript"),
|
||||
(attestation_a, key_id_a, b""),
|
||||
] {
|
||||
assert!(verify_attestation(&attestation, &key, transcript).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_attestation_sentinel_is_rejected_before_good_sentinel_is_accepted() {
|
||||
let (good_attestation, key_id) = attestation(1);
|
||||
let bad_attestation = STANDARD.encode(b"buzz-dev-app-attest-v1:bad");
|
||||
|
||||
assert!(verify_attestation(&bad_attestation, &key_id, b"transcript").is_err());
|
||||
assert!(verify_attestation(&good_attestation, &key_id, b"transcript").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_assertion_sentinel_and_stored_dev_marker_advance_counter() {
|
||||
let (attestation, key_id) = attestation(1);
|
||||
let public_key = verify_attestation(&attestation, &key_id, b"transcript")
|
||||
.unwrap()
|
||||
.public_key;
|
||||
let verified = verify_assertion(
|
||||
&assertion_sentinel(),
|
||||
b"canonical assertion transcript",
|
||||
&public_key,
|
||||
7,
|
||||
"challenge",
|
||||
"challenge",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(verified.counter, 8);
|
||||
|
||||
assert!(verify_assertion(
|
||||
"bad",
|
||||
b"canonical assertion transcript",
|
||||
&public_key,
|
||||
7,
|
||||
"challenge",
|
||||
"challenge",
|
||||
)
|
||||
.is_err());
|
||||
assert!(verify_assertion(
|
||||
&assertion_sentinel(),
|
||||
b"canonical assertion transcript",
|
||||
b"not-the-development-marker",
|
||||
7,
|
||||
"challenge",
|
||||
"challenge",
|
||||
)
|
||||
.is_err());
|
||||
assert!(verify_assertion(
|
||||
&assertion_sentinel(),
|
||||
b"canonical assertion transcript",
|
||||
&public_key,
|
||||
u32::MAX,
|
||||
"challenge",
|
||||
"challenge",
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Stateful installation, delegation, delivery, and health APIs.
|
||||
use crate::{
|
||||
apns::{DeliveryAttempt, DeliveryOutcome, PushTransport},
|
||||
app_attest::AppAttestVerifier,
|
||||
app_attest_policy::AppAttestPolicy,
|
||||
authority::{
|
||||
AuthorityError, AuthorityStore, Challenge, Delegation, DeliveryDisposition, NewInstallation,
|
||||
},
|
||||
@@ -36,7 +36,7 @@ use tower_http::{limit::RequestBodyLimitLayer, timeout::TimeoutLayer};
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub grant_keyring: Arc<GrantKeyring>,
|
||||
pub app_attest: Arc<AppAttestVerifier>,
|
||||
pub app_attest: Arc<AppAttestPolicy>,
|
||||
pub authority: Arc<dyn AuthorityStore>,
|
||||
pub token_keyring: Arc<TokenKeyring>,
|
||||
pub transport: Arc<dyn PushTransport>,
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
//! Stateful, capability-gated APNs last hop for NIP-PL.
|
||||
pub mod apns;
|
||||
pub mod app_attest;
|
||||
pub mod app_attest_policy;
|
||||
pub mod authority;
|
||||
pub mod config;
|
||||
#[cfg(feature = "dev-app-attest-bypass")]
|
||||
pub mod dev_app_attest;
|
||||
pub mod grant;
|
||||
pub mod http;
|
||||
pub mod metrics;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use buzz_push_gateway::{
|
||||
apns::ApnsTransport,
|
||||
app_attest::AppAttestVerifier,
|
||||
app_attest_policy::AppAttestPolicy,
|
||||
authority::AuthorityStore,
|
||||
config::Config,
|
||||
grant::{GrantKey, GrantKeyring},
|
||||
@@ -75,10 +76,19 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
}
|
||||
});
|
||||
let app_attest = Arc::new(AppAttestVerifier::new(
|
||||
c.app_attest_app_id,
|
||||
fs::read(&c.app_attest_root_cert_path)?,
|
||||
)?);
|
||||
let apple_app_attest =
|
||||
AppAttestVerifier::new(c.app_attest_app_id, fs::read(&c.app_attest_root_cert_path)?)?;
|
||||
#[cfg(feature = "dev-app-attest-bypass")]
|
||||
let app_attest = if c.dev_app_attest_bypass {
|
||||
tracing::warn!(
|
||||
"DEVELOPMENT APP ATTEST BYPASS ACTIVE; Apple attestation and assertion verification are disabled"
|
||||
);
|
||||
Arc::new(AppAttestPolicy::development())
|
||||
} else {
|
||||
Arc::new(AppAttestPolicy::apple(apple_app_attest))
|
||||
};
|
||||
#[cfg(not(feature = "dev-app-attest-bypass"))]
|
||||
let app_attest = Arc::new(AppAttestPolicy::apple(apple_app_attest));
|
||||
let accepting = Arc::new(AtomicBool::new(true));
|
||||
let (public, health) = router_with_metrics(
|
||||
AppState {
|
||||
|
||||
@@ -52,6 +52,13 @@ fn outcome_label(outcome: DeliveryOutcome) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
/// Record entry into the concrete APNs HTTP send seam. This counter is kept
|
||||
/// separate from terminal outcomes so a control scrape can distinguish
|
||||
/// "transport never reached" from "APNs send returned an error".
|
||||
pub fn record_apns_send_attempt() {
|
||||
metrics::counter!("push_gateway_apns_send_attempts_total").increment(1);
|
||||
}
|
||||
|
||||
/// Record the terminal APNs outcome and its send round-trip latency.
|
||||
pub fn record_apns_delivery(outcome: DeliveryOutcome, seconds: f64) {
|
||||
metrics::counter!("push_gateway_apns_deliveries_total", "outcome" => outcome_label(outcome))
|
||||
@@ -152,6 +159,7 @@ mod tests {
|
||||
fn recorder_renders_sanitized_bounded_series() {
|
||||
let handle = install().expect("recorder installs exactly once per test process");
|
||||
|
||||
record_apns_send_attempt();
|
||||
record_apns_delivery(DeliveryOutcome::Accepted, 0.012);
|
||||
record_apns_delivery(
|
||||
DeliveryOutcome::InvalidEndpoint {
|
||||
@@ -172,6 +180,7 @@ mod tests {
|
||||
|
||||
// All expected series are present.
|
||||
for needle in [
|
||||
"push_gateway_apns_send_attempts_total",
|
||||
"push_gateway_apns_deliveries_total",
|
||||
"push_gateway_apns_delivery_seconds",
|
||||
"push_gateway_admissions_total",
|
||||
|
||||
@@ -103,6 +103,9 @@ run_unit_tests() {
|
||||
|
||||
run_test_step "buzz-push-gateway tests" \
|
||||
cargo test -p buzz-push-gateway -- --nocapture
|
||||
|
||||
run_test_step "buzz-push-gateway dev App Attest bypass tests" \
|
||||
cargo test -p buzz-push-gateway --features dev-app-attest-bypass -- --nocapture
|
||||
}
|
||||
|
||||
# ---- DB / integration tests (infra required) --------------------------------
|
||||
|
||||
Reference in New Issue
Block a user