commit c7b6b7afa1c5377076a00640c615a2d5c379bee5
parent 30faa21785e30fe14caacc572ec114967c0f1614
Author: Henrique Chan Carvalho Machado <henriqueccmachado@tecnico.ulisboa.pt>
Date: Wed, 21 Jan 2026 10:35:08 +0100
Add configurable VC settings to kych.conf
Diffstat:
3 files changed, 96 insertions(+), 14 deletions(-)
diff --git a/kych_oauth2_gateway/kych.conf.example b/kych_oauth2_gateway/kych.conf.example
@@ -1,7 +1,7 @@
[kych-oauth2-gateway]
#HOST =
#PORT =
-UNIXPATH =
+UNIXPATH =
UNIXPATH_MODE = 666
DATABASE =
NONCE_BYTES = 32
@@ -10,6 +10,11 @@ AUTH_CODE_BYTES = 32
AUTH_CODE_TTL_MINUTES = 10
#ALLOWED_SCOPES = {family_name, given_name, birth_date}
+VC_TYPE = betaid-sdjwt
+VC_FORMAT = vc+sd-jwt
+VC_ALGORITHMS = {ES256}
+VC_CLAIMS = {family_name, given_name, birth_date, sex, place_of_origin, birth_place, nationality, portrait, personal_administrative_number, age_over_16, age_over_18, age_over_65, age_birth_year, document_number, issuance_date, expiry_date, additional_person_info, reference_id_type, reference_id_expiry_date, verification_type, verification_organization, issuing_authority, issuing_country}
+
# ---- Clients (one section per client) ----
[client_example]
diff --git a/kych_oauth2_gateway/src/config.rs b/kych_oauth2_gateway/src/config.rs
@@ -1,6 +1,7 @@
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use ini::Ini;
+use std::collections::HashSet;
use std::path::Path;
const MAIN_SECTION: &str = "kych-oauth2-gateway";
@@ -10,6 +11,7 @@ pub struct Config {
pub server: ServerConfig,
pub database: DatabaseConfig,
pub crypto: CryptoConfig,
+ pub vc: VcConfig,
pub allowed_scopes: Option<Vec<String>>,
pub clients: Vec<ClientConfig>,
}
@@ -71,6 +73,14 @@ pub struct ClientConfig {
pub accepted_issuer_dids: Option<String>,
}
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct VcConfig {
+ pub vc_type: String,
+ pub vc_format: String,
+ pub vc_algorithms: Vec<String>,
+ pub vc_claims: HashSet<String>,
+}
+
impl Config {
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
let ini = Ini::load_from_file(path.as_ref())
@@ -144,6 +154,46 @@ impl Config {
_ => None,
};
+ let vc_type = main_section
+ .get("VC_TYPE")
+ .filter(|s| !s.is_empty())
+ .context("missing required config: VC_TYPE")?
+ .to_string();
+
+ let vc_format = main_section
+ .get("VC_FORMAT")
+ .filter(|s| !s.is_empty())
+ .context("missing required config: VC_FORMAT")?
+ .to_string();
+
+ let vc_algorithms = parse_bracketed_list(
+ main_section
+ .get("VC_ALGORITHMS")
+ .context("missing required config: VC_ALGORITHMS")?,
+ "VC_ALGORITHMS",
+ )?;
+ if vc_algorithms.is_empty() {
+ anyhow::bail!("VC_ALGORITHMS must contain at least one algorithm");
+ }
+
+ let vc_claims_list = parse_bracketed_list(
+ main_section
+ .get("VC_CLAIMS")
+ .context("missing required config: VC_CLAIMS")?,
+ "VC_CLAIMS",
+ )?;
+ if vc_claims_list.is_empty() {
+ anyhow::bail!("VC_CLAIMS must contain at least one claim");
+ }
+ let vc_claims: HashSet<String> = vc_claims_list.into_iter().collect();
+
+ let vc = VcConfig {
+ vc_type,
+ vc_format,
+ vc_algorithms,
+ vc_claims,
+ };
+
let mut clients = Vec::new();
for (section_name, properties) in ini.iter() {
let section_name = match section_name {
@@ -186,6 +236,7 @@ impl Config {
server,
database,
crypto,
+ vc,
allowed_scopes,
clients,
})
diff --git a/kych_oauth2_gateway/src/handlers.rs b/kych_oauth2_gateway/src/handlers.rs
@@ -7,6 +7,7 @@ use axum::{
};
use chrono::Utc;
use serde_json::json;
+use std::collections::HashSet;
use crate::{crypto, db::sessions::SessionStatus, models::*, state::AppState};
@@ -51,6 +52,15 @@ fn parse_accepted_issuer_dids(raw: &str) -> Result<Vec<String>, &'static str> {
Ok(dids)
}
+fn validate_scope_claims(scope: &str, valid_claims: &HashSet<String>) -> Result<(), String> {
+ for claim in scope.split_whitespace() {
+ if !valid_claims.contains(claim) {
+ return Err(format!("invalid claim in scope: {}", claim));
+ }
+ }
+ Ok(())
+}
+
// Health check endpoint
pub async fn health_check() -> impl IntoResponse {
tracing::info!("Received Health Request");
@@ -364,8 +374,24 @@ pub async fn authorize(
}
}
- // Build presentation definition from scope
- let presentation_definition = build_presentation_definition(&data.scope);
+ if let Err(e) = validate_scope_claims(&data.scope, &state.config.vc.vc_claims) {
+ tracing::warn!(
+ "Rejected invalid scope claims for client {}: {}",
+ params.client_id,
+ e
+ );
+ return Err((
+ StatusCode::BAD_REQUEST,
+ Json(ErrorResponse::new(&e)),
+ ));
+ }
+
+ let presentation_definition = build_presentation_definition(
+ &data.scope,
+ &state.config.vc.vc_type,
+ &state.config.vc.vc_format,
+ &state.config.vc.vc_algorithms,
+ );
// Call Swiyu Verifier
let verifier_url = format!("{}{}", data.verifier_url, data.verifier_management_api_path);
@@ -534,10 +560,12 @@ pub async fn authorize(
}).into_response())
}
-/// Build a presentation definition from a space-delimited scope string
-///
-/// Example: "age_over_18" or "first_name last_name"
-fn build_presentation_definition(scope: &str) -> PresentationDefinition {
+fn build_presentation_definition(
+ scope: &str,
+ vc_type: &str,
+ vc_format: &str,
+ vc_algorithms: &[String],
+) -> PresentationDefinition {
use std::collections::HashMap;
use uuid::Uuid;
@@ -548,7 +576,6 @@ fn build_presentation_definition(scope: &str) -> PresentationDefinition {
attributes
);
- // First field: $.vct with filter for credential type
let vct_field = Field {
path: vec!["$.vct".to_string()],
id: None,
@@ -556,11 +583,10 @@ fn build_presentation_definition(scope: &str) -> PresentationDefinition {
purpose: None,
filter: Some(Filter {
filter_type: "string".to_string(),
- const_value: Some("betaid-sdjwt".to_string()),
+ const_value: Some(vc_type.to_string()),
}),
};
- // Attribute fields from scope
let mut fields: Vec<Field> = vec![vct_field];
for attr in &attributes {
fields.push(Field {
@@ -574,10 +600,10 @@ fn build_presentation_definition(scope: &str) -> PresentationDefinition {
let mut format = HashMap::new();
format.insert(
- "vc+sd-jwt".to_string(),
+ vc_format.to_string(),
FormatAlgorithm {
- sd_jwt_alg_values: vec!["ES256".to_string()],
- kb_jwt_alg_values: vec!["ES256".to_string()],
+ sd_jwt_alg_values: vc_algorithms.to_vec(),
+ kb_jwt_alg_values: vc_algorithms.to_vec(),
},
);
@@ -593,7 +619,7 @@ fn build_presentation_definition(scope: &str) -> PresentationDefinition {
id: Uuid::new_v4().to_string(),
name: Some("Over 18 Verification".to_string()),
purpose: Some("Verify age is over 18".to_string()),
- format: None, // No format at top level
+ format: None,
input_descriptors: vec![input_descriptor],
}
}