11use axum:: extract:: { Path , State } ;
22use axum:: http:: HeaderMap ;
33use axum:: { Json , Router } ;
4+ use serde:: { Deserialize , Serialize } ;
45use uuid:: Uuid ;
56
67use crate :: DgwState ;
@@ -11,7 +12,7 @@ use crate::http::HttpError;
1112///
1213/// Returns `true` if the token is a well-formed JWT whose signature verifies
1314/// against `provisioner_key`, whose `exp` has not passed, and whose `scope`
14- /// is `TunnelEnroll ` (or `Wildcard`). Returns `false` for any failure.
15+ /// is `AgentEnroll ` (or `Wildcard`). Returns `false` for any failure.
1516///
1617/// The enrollment JWT carries extra claims (`jet_gw_url`, `jet_agent_name`)
1718/// that the *agent* reads locally from its own copy of the token — the Gateway
@@ -39,25 +40,10 @@ fn validate_enrollment_jwt(token: &str, provisioner_key: &picky::key::PublicKey)
3940
4041 matches ! (
4142 validated. state. claims. scope,
42- AccessScope :: TunnelEnroll | AccessScope :: Wildcard
43+ AccessScope :: AgentEnroll | AccessScope :: Wildcard
4344 )
4445}
4546
46- /// Timing-safe byte comparison for secret values.
47- ///
48- /// Both inputs are first hashed with SHA-256 to produce fixed 32-byte digests;
49- /// the digest comparison then runs in constant time (fixed-length XOR fold).
50- /// Hashing normalizes length so a leaked hash duration cannot reveal the
51- /// secret's length; and the constant-time fold prevents leaking which byte
52- /// differed. The function is *not* constant-time over input length, which is
53- /// why it is named after its intent (timing-safe) rather than its mechanism.
54- fn timing_safe_eq ( a : & [ u8 ] , b : & [ u8 ] ) -> bool {
55- use sha2:: { Digest , Sha256 } ;
56- let da = Sha256 :: digest ( a) ;
57- let db = Sha256 :: digest ( b) ;
58- da. iter ( ) . zip ( db. iter ( ) ) . fold ( 0u8 , |acc, ( x, y) | acc | ( x ^ y) ) == 0
59- }
60-
6147#[ derive( Deserialize ) ]
6248pub struct EnrollRequest {
6349 /// Agent-generated UUID (the agent owns its identity).
@@ -96,8 +82,8 @@ pub fn make_router<S>(state: DgwState) -> Router<S> {
9682
9783/// Enroll a new agent.
9884///
99- /// Requires a Bearer token matching the configured enrollment secret
100- /// or a valid one-time enrollment token from the store .
85+ /// Requires a Bearer token: a JWT signed by the configured provisioner key
86+ /// (e.g. DVLS, Hub, or any PEM service) with `AgentEnroll` or `Wildcard` scope .
10187///
10288/// The agent generates its own key pair and sends a CSR. The gateway signs it
10389/// and returns the certificate. The private key never leaves the agent.
@@ -108,13 +94,15 @@ async fn enroll_agent(
10894 ..
10995 } ) : State < DgwState > ,
11096 headers : HeaderMap ,
111- Json ( req) : Json < EnrollRequest > ,
97+ Json ( EnrollRequest {
98+ agent_id,
99+ agent_name,
100+ csr_pem,
101+ agent_hostname,
102+ } ) : Json < EnrollRequest > ,
112103) -> Result < Json < EnrollResponse > , HttpError > {
113104 // Validate agent name: 1-255 printable ASCII characters.
114- if req. agent_name . is_empty ( )
115- || 255 < req. agent_name . len ( )
116- || req. agent_name . bytes ( ) . any ( |b| !( 0x20 ..=0x7E ) . contains ( & b) )
117- {
105+ if agent_name. is_empty ( ) || 255 < agent_name. len ( ) || agent_name. bytes ( ) . any ( |b| !( 0x20 ..=0x7E ) . contains ( & b) ) {
118106 return Err ( HttpError :: bad_request ( ) . msg ( "agent name must be 1-255 printable ASCII characters" ) ) ;
119107 }
120108
@@ -134,30 +122,10 @@ async fn enroll_agent(
134122 . as_ref ( )
135123 . ok_or_else ( || HttpError :: not_found ( ) . msg ( "agent enrollment is not configured" ) ) ?;
136124
137- // Token validation order:
138- // 1. JWT signed by the configured provisioner key (scope == TunnelEnroll)
139- // 2. One-time enrollment token from the in-memory store
140- // 3. Static enrollment secret from configuration (constant-time comparison)
141- let jwt_valid = validate_enrollment_jwt ( provided_token, & conf. provisioner_public_key ) ;
142-
143- if !jwt_valid {
144- let token_valid = handle. enrollment_token_store ( ) . redeem ( provided_token) . await ;
145-
146- if !token_valid {
147- let enrollment_secret = conf
148- . agent_tunnel
149- . enrollment_secret
150- . as_deref ( )
151- . ok_or_else ( || HttpError :: not_found ( ) . msg ( "agent enrollment is not configured" ) ) ?;
152-
153- if !timing_safe_eq ( provided_token. as_bytes ( ) , enrollment_secret. as_bytes ( ) ) {
154- return Err ( HttpError :: forbidden ( ) . msg ( "invalid enrollment token" ) ) ;
155- }
156- }
125+ if !validate_enrollment_jwt ( provided_token, & conf. provisioner_public_key ) {
126+ return Err ( HttpError :: forbidden ( ) . msg ( "invalid enrollment token" ) ) ;
157127 }
158128
159- let agent_id = req. agent_id ;
160-
161129 // Reject duplicate agent IDs to prevent identity shadowing.
162130 if handle. registry ( ) . get ( & agent_id) . await . is_some ( ) {
163131 return Err (
@@ -167,7 +135,7 @@ async fn enroll_agent(
167135
168136 let signed = handle
169137 . ca_manager ( )
170- . sign_agent_csr ( agent_id, & req . agent_name , & req . csr_pem , req . agent_hostname . as_deref ( ) )
138+ . sign_agent_csr ( agent_id, & agent_name, & csr_pem, agent_hostname. as_deref ( ) )
171139 . map_err ( HttpError :: bad_request ( ) . with_msg ( "invalid CSR" ) . err ( ) ) ?;
172140
173141 let quic_endpoint = format ! ( "{}:{}" , conf. hostname, conf. agent_tunnel. listen_port) ;
@@ -179,7 +147,7 @@ async fn enroll_agent(
179147
180148 info ! (
181149 %agent_id,
182- agent_name = %req . agent_name,
150+ agent_name = %agent_name,
183151 "Agent enrolled successfully" ,
184152 ) ;
185153
@@ -282,7 +250,7 @@ mod tests {
282250 let ( priv_key, pub_key) = keypair ( ) ;
283251 let token = sign (
284252 json ! ( {
285- "scope" : "gateway.tunnel .enroll" ,
253+ "scope" : "gateway.agent .enroll" ,
286254 "nbf" : now_ts( ) - 60 ,
287255 "exp" : now_ts( ) + 3600 ,
288256 "jti" : Uuid :: new_v4( ) ,
@@ -334,7 +302,7 @@ mod tests {
334302 let ( priv_key, pub_key) = keypair ( ) ;
335303 let token = sign (
336304 json ! ( {
337- "scope" : "gateway.tunnel .enroll" ,
305+ "scope" : "gateway.agent .enroll" ,
338306 "nbf" : now_ts( ) - 7200 ,
339307 "exp" : now_ts( ) - 3600 ,
340308 "jti" : Uuid :: new_v4( ) ,
@@ -352,7 +320,7 @@ mod tests {
352320 let ( _, gateway_pub) = keypair ( ) ;
353321 let token = sign (
354322 json ! ( {
355- "scope" : "gateway.tunnel .enroll" ,
323+ "scope" : "gateway.agent .enroll" ,
356324 "nbf" : now_ts( ) - 60 ,
357325 "exp" : now_ts( ) + 3600 ,
358326 "jti" : Uuid :: new_v4( ) ,
@@ -369,7 +337,7 @@ mod tests {
369337 let ( priv_key, pub_key) = keypair ( ) ;
370338 let token = sign (
371339 json ! ( {
372- "scope" : "gateway.tunnel .enroll" ,
340+ "scope" : "gateway.agent .enroll" ,
373341 "nbf" : now_ts( ) - 60 ,
374342 "exp" : now_ts( ) + 3600 ,
375343 "jti" : Uuid :: new_v4( ) ,
0 commit comments