Skip to content
Merged
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
1 change: 1 addition & 0 deletions bin/tsa-server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ async fn run_serve() {
time_source: monitor,
max_request_bytes: cfg.max_request_bytes.max(0) as usize,
version: env!("CARGO_PKG_VERSION").to_string(),
cors_allowed_origin: cfg.cors_allowed_origin.clone(),
});

let listener = tokio::net::TcpListener::bind(bind_addr(&cfg.listen))
Expand Down
26 changes: 26 additions & 0 deletions crates/oe-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ pub struct Config {
/// pays sont imposés par le profil côté autorité.
pub subject_cn: String,
pub renew_before: Duration,
/// Origine autorisée à appeler l'API HTTP depuis un navigateur
/// (Access-Control-Allow-Origin). Vide/absent = pas de CORS, aucune
/// requête cross-origin n'est autorisée — comportement par défaut,
/// préservé pour tout déploiement qui n'a pas de démo web tierce.
pub cors_allowed_origin: Option<String>,
}

impl Config {
Expand Down Expand Up @@ -169,6 +174,9 @@ impl Config {
"OPENEIDAS_RENEW_BEFORE",
Duration::from_secs(30 * 24 * 3600),
)?,
cors_allowed_origin: env::var("OPENEIDAS_CORS_ALLOWED_ORIGIN")
.ok()
.filter(|s| !s.is_empty()),
})
}
}
Expand Down Expand Up @@ -337,6 +345,24 @@ mod tests {
assert_eq!(cfg.time_policy, Policy::Enforce);
assert_eq!(cfg.accuracy, Duration::from_secs(1));
assert_eq!(cfg.time_max_offset, Duration::from_millis(500));
assert_eq!(cfg.cors_allowed_origin, None);
clear_env();
}

#[test]
#[serial]
fn load_reads_cors_allowed_origin() {
clear_env();
env::set_var("OPENEIDAS_PIN", "1234");
env::set_var(
"OPENEIDAS_CORS_ALLOWED_ORIGIN",
"https://demo.open-eidas.eu",
);
let cfg = Config::load().unwrap();
assert_eq!(
cfg.cors_allowed_origin.as_deref(),
Some("https://demo.open-eidas.eu")
);
clear_env();
}

Expand Down
1 change: 1 addition & 0 deletions crates/oe-crosstsa/tests/against_local_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ async fn start_local_tsa() -> String {
time_source,
max_request_bytes: 64 * 1024,
version: "test".to_string(),
cors_allowed_origin: None,
});

let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
Expand Down
2 changes: 1 addition & 1 deletion crates/oe-httpapi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ oe-rfc3161-asn1 = { path = "../oe-rfc3161-asn1" }
oe-hsm = { path = "../oe-hsm" }
oe-timesource = { path = "../oe-timesource" }
axum = "0.8"
tower-http = { version = "0.6", features = ["trace", "limit"] }
tower-http = { version = "0.6", features = ["trace", "limit", "cors"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tracing = "0.1"
Expand Down
21 changes: 18 additions & 3 deletions crates/oe-httpapi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ pub struct Options {
pub time_source: Arc<oe_timesource::Monitor>,
pub max_request_bytes: usize,
pub version: String,
/// Origine autorisée en cross-origin (Access-Control-Allow-Origin) pour
/// les routes de lecture/soumission destinées à un navigateur
/// (`/api/v1/timestamp`, `/api/v1/certificate`, `/api/v1/policy`).
/// `None` = pas de couche CORS, comportement par défaut.
pub cors_allowed_origin: Option<String>,
}

#[derive(Clone)]
Expand All @@ -49,15 +54,25 @@ pub fn router(opts: Options) -> Router {
time_source: opts.time_source,
version: Arc::from(opts.version.as_str()),
};
Router::new()
let mut router = Router::new()
.route("/tsa", post(handle_rfc3161))
.route("/api/v1/timestamp", post(handle_json))
.route("/api/v1/policy", get(handle_policy))
.route("/api/v1/certificate", get(handle_certificate))
.route("/healthz", get(handle_health))
.layer(DefaultBodyLimit::max(opts.max_request_bytes))
.layer(tower_http::trace::TraceLayer::new_for_http())
.with_state(state)
.layer(tower_http::trace::TraceLayer::new_for_http());
if let Some(origin) = opts.cors_allowed_origin.as_deref() {
if let Ok(origin) = axum::http::HeaderValue::from_str(origin) {
router = router.layer(
tower_http::cors::CorsLayer::new()
.allow_origin(origin)
Comment on lines +65 to +69
.allow_methods([axum::http::Method::GET, axum::http::Method::POST])
.allow_headers([axum::http::header::CONTENT_TYPE]),
);
}
}
router.with_state(state)
}

async fn handle_rfc3161(
Expand Down
60 changes: 60 additions & 0 deletions crates/oe-httpapi/tests/end_to_end.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ async fn serves_a_verifiable_token_over_http() {
time_source,
max_request_bytes: 64 * 1024,
version: "test".to_string(),
cors_allowed_origin: None,
});

let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
Expand Down Expand Up @@ -153,3 +154,62 @@ async fn serves_a_verifiable_token_over_http() {
);
let _ = std::fs::remove_file(&out_path);
}

#[tokio::test]
async fn cors_header_present_only_when_configured() {
let time_source = || {
oe_timesource::Monitor::new(oe_timesource::Options {
policy: oe_timesource::Policy::Disabled,
..Default::default()
})
.unwrap()
};

// Sans cors_allowed_origin : pas d'en-tête CORS, même pour une origine
// qui enverrait un Origin arbitraire — le navigateur bloquera la lecture
// de la réponse.
let app = oe_httpapi::router(oe_httpapi::Options {
authority: load_authority(),
time_source: time_source(),
max_request_bytes: 64 * 1024,
version: "test".to_string(),
cors_allowed_origin: None,
});
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
axum::serve(listener, app).await.unwrap();
});
let resp = reqwest::Client::new()
.get(format!("http://{addr}/api/v1/policy"))
.header("Origin", "https://demo.open-eidas.eu")
.send()
.await
.unwrap();
assert!(resp.headers().get("access-control-allow-origin").is_none());

// Avec cors_allowed_origin configuré : l'en-tête reflète exactement
// l'origine autorisée, pas un wildcard ni l'origine de la requête.
let app = oe_httpapi::router(oe_httpapi::Options {
authority: load_authority(),
time_source: time_source(),
max_request_bytes: 64 * 1024,
version: "test".to_string(),
cors_allowed_origin: Some("https://demo.open-eidas.eu".to_string()),
});
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
axum::serve(listener, app).await.unwrap();
});
let resp = reqwest::Client::new()
.get(format!("http://{addr}/api/v1/policy"))
.header("Origin", "https://demo.open-eidas.eu")
.send()
.await
.unwrap();
assert_eq!(
resp.headers().get("access-control-allow-origin").unwrap(),
"https://demo.open-eidas.eu"
);
}
4 changes: 4 additions & 0 deletions deploy/helm/open-eidas/templates/tsa/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ spec:
# sont imposés par le profil côté autorité.
- name: OPENEIDAS_SUBJECT_CN
value: {{ .Values.tsa.subject.commonName | quote }}
{{- if .Values.tsa.corsAllowedOrigin }}
- name: OPENEIDAS_CORS_ALLOWED_ORIGIN
value: {{ .Values.tsa.corsAllowedOrigin | quote }}
{{- end }}
volumeMounts:
- name: softhsm
mountPath: /var/lib/softhsm/tokens
Expand Down
3 changes: 3 additions & 0 deletions deploy/helm/open-eidas/values-staging.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ tsa:
gateway:
enabled: true
host: api.staging.open-eidas.eu
# Démo web officielle (open-eidas/demo, GitHub Pages) — seule origine
# autorisée à appeler cette API de staging depuis un navigateur.
corsAllowedOrigin: "https://demo.open-eidas.eu"

ca:
# Gravée dans les points CRL et AIA des certificats émis : doit correspondre
Expand Down
4 changes: 4 additions & 0 deletions deploy/helm/open-eidas/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ tsa:
# PIN du token PKCS#11, généré aléatoirement et conservé dans un Secret
# si laissé vide (voir templates/secret-generated.yaml).
pin: ""
# Origine autorisée à appeler l'API depuis un navigateur
# (Access-Control-Allow-Origin) — ex. une démo web tierce. Vide = pas de
# CORS, comportement par défaut.
corsAllowedOrigin: ""
# Exposition externe via Gateway API (HTTPRoute), pas Ingress : la
# terminaison TLS est de la responsabilité du Gateway référencé, pas de ce
# chart.
Expand Down
Loading