@@ -44,21 +44,6 @@ fn validate_enrollment_jwt(token: &str, provisioner_key: &picky::key::PublicKey)
4444 )
4545}
4646
47- /// Timing-safe byte comparison for secret values.
48- ///
49- /// Both inputs are first hashed with SHA-256 to produce fixed 32-byte digests;
50- /// the digest comparison then runs in constant time (fixed-length XOR fold).
51- /// Hashing normalizes length so a leaked hash duration cannot reveal the
52- /// secret's length; and the constant-time fold prevents leaking which byte
53- /// differed. The function is *not* constant-time over input length, which is
54- /// why it is named after its intent (timing-safe) rather than its mechanism.
55- fn timing_safe_eq ( a : & [ u8 ] , b : & [ u8 ] ) -> bool {
56- use sha2:: { Digest , Sha256 } ;
57- let da = Sha256 :: digest ( a) ;
58- let db = Sha256 :: digest ( b) ;
59- da. iter ( ) . zip ( db. iter ( ) ) . fold ( 0u8 , |acc, ( x, y) | acc | ( x ^ y) ) == 0
60- }
61-
6247#[ derive( Deserialize ) ]
6348pub struct EnrollRequest {
6449 /// Agent-generated UUID (the agent owns its identity).
@@ -97,12 +82,8 @@ pub fn make_router<S>(state: DgwState) -> Router<S> {
9782
9883/// Enroll a new agent.
9984///
100- /// Requires a Bearer token that is either:
101- /// - a JWT signed by the configured provisioner key with `AgentEnroll` /
102- /// `Wildcard` scope (issued by DVLS — the only authority for agent
103- /// enrollment tokens), or
104- /// - the static `enrollment_secret` from the gateway configuration (admin
105- /// bootstrap fallback for environments without DVLS).
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.
10687///
10788/// The agent generates its own key pair and sends a CSR. The gateway signs it
10889/// and returns the certificate. The private key never leaves the agent.
@@ -113,13 +94,15 @@ async fn enroll_agent(
11394 ..
11495 } ) : State < DgwState > ,
11596 headers : HeaderMap ,
116- Json ( req) : Json < EnrollRequest > ,
97+ Json ( EnrollRequest {
98+ agent_id,
99+ agent_name,
100+ csr_pem,
101+ agent_hostname,
102+ } ) : Json < EnrollRequest > ,
117103) -> Result < Json < EnrollResponse > , HttpError > {
118104 // Validate agent name: 1-255 printable ASCII characters.
119- if req. agent_name . is_empty ( )
120- || 255 < req. agent_name . len ( )
121- || req. agent_name . bytes ( ) . any ( |b| !( 0x20 ..=0x7E ) . contains ( & b) )
122- {
105+ if agent_name. is_empty ( ) || 255 < agent_name. len ( ) || agent_name. bytes ( ) . any ( |b| !( 0x20 ..=0x7E ) . contains ( & b) ) {
123106 return Err ( HttpError :: bad_request ( ) . msg ( "agent name must be 1-255 printable ASCII characters" ) ) ;
124107 }
125108
@@ -139,30 +122,10 @@ async fn enroll_agent(
139122 . as_ref ( )
140123 . ok_or_else ( || HttpError :: not_found ( ) . msg ( "agent enrollment is not configured" ) ) ?;
141124
142- // Token validation order:
143- // 1. JWT signed by the configured provisioner key (scope == AgentEnroll)
144- // 2. Static enrollment secret from configuration (constant-time comparison)
145- let jwt_valid = validate_enrollment_jwt ( provided_token, & conf. provisioner_public_key ) ;
146-
147- if !jwt_valid {
148- // The JWT failed to validate against the provisioner key. The static
149- // `enrollment_secret` is only a fallback for environments without DVLS;
150- // when it is not configured, the request is simply unauthenticated, not
151- // a server-config issue — so 403, not 404 (404 is reserved for the agent
152- // tunnel feature itself being disabled).
153- let enrollment_secret = conf
154- . agent_tunnel
155- . enrollment_secret
156- . as_deref ( )
157- . ok_or_else ( || HttpError :: forbidden ( ) . msg ( "invalid enrollment token" ) ) ?;
158-
159- if !timing_safe_eq ( provided_token. as_bytes ( ) , enrollment_secret. as_bytes ( ) ) {
160- return Err ( HttpError :: forbidden ( ) . msg ( "invalid enrollment token" ) ) ;
161- }
125+ if !validate_enrollment_jwt ( provided_token, & conf. provisioner_public_key ) {
126+ return Err ( HttpError :: forbidden ( ) . msg ( "invalid enrollment token" ) ) ;
162127 }
163128
164- let agent_id = req. agent_id ;
165-
166129 // Reject duplicate agent IDs to prevent identity shadowing.
167130 if handle. registry ( ) . get ( & agent_id) . await . is_some ( ) {
168131 return Err (
@@ -172,7 +135,7 @@ async fn enroll_agent(
172135
173136 let signed = handle
174137 . ca_manager ( )
175- . 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 ( ) )
176139 . map_err ( HttpError :: bad_request ( ) . with_msg ( "invalid CSR" ) . err ( ) ) ?;
177140
178141 let quic_endpoint = format ! ( "{}:{}" , conf. hostname, conf. agent_tunnel. listen_port) ;
@@ -184,7 +147,7 @@ async fn enroll_agent(
184147
185148 info ! (
186149 %agent_id,
187- agent_name = %req . agent_name,
150+ agent_name = %agent_name,
188151 "Agent enrolled successfully" ,
189152 ) ;
190153
0 commit comments