11use super :: runtime:: tokio_runtime;
22use actix_web:: { App , HttpResponse , HttpServer , web} ;
33use base64:: Engine ;
4- use base64:: prelude:: BASE64_STANDARD ;
4+ use base64:: prelude:: { BASE64_STANDARD , BASE64_URL_SAFE_NO_PAD } ;
55use newrelic_agent_control:: opamp:: instance_id:: InstanceID ;
66use newrelic_agent_control:: opamp:: remote_config:: signature:: {
77 ED25519 , SIGNATURE_CUSTOM_CAPABILITY , SIGNATURE_CUSTOM_MESSAGE_TYPE , SignatureFields ,
@@ -15,6 +15,9 @@ use opamp_client::opamp::proto::{
1515use opamp_client:: operation:: instance_uid:: InstanceUid ;
1616use prost:: Message ;
1717use rcgen:: { CertificateParams , KeyPair , PKCS_ED25519 , PublicKeyData } ;
18+ use ring:: rand:: SystemRandom ;
19+ use ring:: signature:: { Ed25519KeyPair , KeyPair as _} ;
20+ use serde_json:: json;
1821use std:: hash:: { DefaultHasher , Hash , Hasher } ;
1922use std:: path:: PathBuf ;
2023use std:: sync:: Mutex ;
@@ -23,13 +26,17 @@ use tempfile::TempDir;
2326use tokio:: task:: JoinHandle ;
2427
2528const FAKE_SERVER_PATH : & str = "/opamp-fake-server" ;
29+ const JWKS_SERVER_PATH : & str = "/jwks" ;
2630const CERT_FILE : & str = "server.crt" ;
2731
2832/// Represents the state of the FakeServer.
2933struct ServerState {
3034 agent_state : HashMap < InstanceID , AgentState > ,
31- // Server private key to sign the remote config
32- key_pair : KeyPair ,
35+ // Key pair to sign remote configuration
36+ key_pair : Ed25519KeyPair ,
37+ // Use the legacy system (instead of key_pair) to sign remote configuration
38+ use_legacy_signatures : bool ,
39+ legacy_key_pair : KeyPair , // TODO: cleanup when no longer used
3340}
3441
3542#[ derive( Default ) ]
@@ -43,10 +50,12 @@ struct AgentState {
4350}
4451
4552impl ServerState {
46- fn new ( key_pair : KeyPair ) -> Self {
53+ fn new ( cert_key_pair : KeyPair , use_legacy_signatures : bool ) -> Self {
4754 Self {
4855 agent_state : HashMap :: new ( ) ,
49- key_pair,
56+ key_pair : generate_key_pair ( ) ,
57+ use_legacy_signatures,
58+ legacy_key_pair : cert_key_pair,
5059 }
5160 }
5261}
@@ -85,22 +94,36 @@ impl FakeServer {
8594 format ! ( "http://localhost:{}{}" , self . port, self . path)
8695 }
8796
97+ pub fn jwks_endpoint ( & self ) -> String {
98+ format ! ( "http://localhost:{}{}" , self . port, JWKS_SERVER_PATH )
99+ }
100+
88101 /// Starts and returns new FakeServer in a random port.
89102 pub fn start_new ( ) -> Self {
103+ Self :: start_new_with_legacy_signatures ( false )
104+ }
105+
106+ /// If `use_legacy_signatures` is set to true, the jwks endpoint is still available but
107+ /// configs are signed using the legacy system.
108+ pub fn start_new_with_legacy_signatures ( use_legacy_signatures : bool ) -> Self {
90109 // While binding to port 0, the kernel gives you a free ephemeral port.
91110 let listener = net:: TcpListener :: bind ( "0.0.0.0:0" ) . unwrap ( ) ;
92111 let port = listener. local_addr ( ) . unwrap ( ) . port ( ) ;
93112
94- let key_pair = KeyPair :: generate_for ( & PKCS_ED25519 ) . unwrap ( ) ;
113+ // Legacy certificate-based key pair
114+ let legacy_key_pair = KeyPair :: generate_for ( & PKCS_ED25519 ) . unwrap ( ) ;
95115 let cert = CertificateParams :: new ( vec ! [ "localhost" . to_string( ) ] )
96116 . unwrap ( )
97- . self_signed ( & key_pair )
117+ . self_signed ( & legacy_key_pair )
98118 . unwrap ( ) ;
99119
100120 let tmp_dir = tempfile:: tempdir ( ) . unwrap ( ) ;
101121 std:: fs:: write ( tmp_dir. path ( ) . join ( CERT_FILE ) , cert. pem ( ) ) . unwrap ( ) ;
102122
103- let state = Arc :: new ( Mutex :: new ( ServerState :: new ( key_pair) ) ) ;
123+ let state = Arc :: new ( Mutex :: new ( ServerState :: new (
124+ legacy_key_pair,
125+ use_legacy_signatures,
126+ ) ) ) ;
104127
105128 let handle = tokio_runtime ( ) . spawn ( Self :: run_http_server ( listener, state. clone ( ) ) ) ;
106129
@@ -118,6 +141,7 @@ impl FakeServer {
118141 App :: new ( )
119142 . app_data ( web:: Data :: new ( state. clone ( ) ) )
120143 . service ( web:: resource ( FAKE_SERVER_PATH ) . to ( opamp_handler) )
144+ . service ( web:: resource ( JWKS_SERVER_PATH ) . to ( jwks_handler) )
121145 } )
122146 . listen ( listener)
123147 . unwrap_or_else ( |err| panic ! ( "Could not bind the HTTP server to the listener: {err}" ) )
@@ -193,61 +217,96 @@ async fn opamp_handler(state: web::Data<Arc<Mutex<ServerState>>>, req: web::Byte
193217
194218 let mut server_state = state. lock ( ) . unwrap ( ) ;
195219
196- let state = server_state
220+ let agent_state = server_state
197221 . agent_state
198222 . entry ( identifier. clone ( ) )
199223 . or_default ( ) ;
200224
201225 // Check sequence number
202226 let mut flags = ServerToAgentFlags :: Unspecified as u64 ;
203- if message. sequence_num == ( state . sequence_number + 1 ) {
227+ if message. sequence_num == ( agent_state . sequence_number + 1 ) {
204228 // case 1: first opamp connection start with seq number 1
205229 // case 2: Any valid new sequence number
206- state . sequence_number += 1 ;
230+ agent_state . sequence_number += 1 ;
207231 } else {
208232 flags = ServerToAgentFlags :: ReportFullState as u64 ;
209233 // upon report full state the opamp client will send a new AgentToServer
210234 // increasing the seq number so current should be the valid
211- state . sequence_number = message. sequence_num ;
235+ agent_state . sequence_number = message. sequence_num ;
212236 }
213237
214238 if let Some ( health) = message. health {
215- state . health_status = Some ( health) ;
239+ agent_state . health_status = Some ( health) ;
216240 }
217241
218242 if let Some ( attributes) = message. agent_description {
219- state . attributes = attributes;
243+ agent_state . attributes = attributes;
220244 }
221245
222246 if let Some ( effective_cfg) = message. effective_config {
223- state . effective_config = effective_cfg;
247+ agent_state . effective_config = effective_cfg;
224248 }
225249
226250 // Process config status:
227251 // Stop sending the RemoteConfig once we got a RemoteConfigStatus response associated with the hash.
228252 // emulating what FC currently does.
229253 if let Some ( cfg_status) = message. remote_config_status {
230- if state. remote_config . as_ref ( ) . is_some_and ( |config_response| {
231- config_response. hash . encode_to_vec ( ) == cfg_status. last_remote_config_hash
232- } ) {
233- state. remote_config = None ;
254+ if agent_state
255+ . remote_config
256+ . as_ref ( )
257+ . is_some_and ( |config_response| {
258+ config_response. hash . encode_to_vec ( ) == cfg_status. last_remote_config_hash
259+ } )
260+ {
261+ agent_state. remote_config = None ;
234262 }
235- state . config_status = cfg_status;
263+ agent_state . config_status = cfg_status;
236264 }
237265
238- let server_to_agent = build_response (
239- identifier,
240- state. remote_config . clone ( ) ,
241- & server_state. key_pair ,
242- flags,
243- ) ;
266+ let remote_config = agent_state. remote_config . clone ( ) ;
267+
268+ let _ = agent_state; // We need to get rid of the mutable reference before leveraging another immutable.
269+
270+ let ( key_pair, key_id) = if server_state. use_legacy_signatures {
271+ (
272+ & Ed25519KeyPair :: from_pkcs8 ( & server_state. legacy_key_pair . serialize_der ( ) ) . unwrap ( ) ,
273+ public_key_fingerprint ( & server_state. legacy_key_pair . subject_public_key_info ( ) ) ,
274+ )
275+ } else {
276+ let public_key = server_state. key_pair . public_key ( ) . as_ref ( ) . to_vec ( ) ;
277+ ( & server_state. key_pair , public_key_fingerprint ( & public_key) )
278+ } ;
279+
280+ let server_to_agent = build_response ( identifier, remote_config, key_pair, key_id, flags) ;
244281 HttpResponse :: Ok ( ) . body ( server_to_agent)
245282}
246283
284+ async fn jwks_handler ( state : web:: Data < Arc < Mutex < ServerState > > > , _req : web:: Bytes ) -> HttpResponse {
285+ let server_state = state. lock ( ) . unwrap ( ) ;
286+ let public_key = server_state. key_pair . public_key ( ) . as_ref ( ) . to_vec ( ) ;
287+ let enc_public_key = BASE64_URL_SAFE_NO_PAD . encode ( & public_key) ;
288+ let payload = json ! ( {
289+ "keys" : [
290+ {
291+ "kty" : "OKP" ,
292+ "alg" : null,
293+ "use" : "sig" ,
294+ "kid" : public_key_fingerprint( & public_key) ,
295+ "n" : null,
296+ "x" : enc_public_key,
297+ "y" : null,
298+ "crv" : "Ed25519"
299+ }
300+ ]
301+ } ) ;
302+ HttpResponse :: Ok ( ) . json ( payload)
303+ }
304+
247305fn build_response (
248306 instance_id : InstanceID ,
249307 agent_remote_config : Option < RemoteConfig > ,
250- key_pair : & KeyPair ,
308+ key_pair : & Ed25519KeyPair ,
309+ key_id : String ,
251310 flags : u64 ,
252311) -> Vec < u8 > {
253312 let mut remote_config = None ;
@@ -267,16 +326,14 @@ fn build_response(
267326 } ) ,
268327 } ) ;
269328
270- let key_pair_ring =
271- ring:: signature:: Ed25519KeyPair :: from_pkcs8 ( & key_pair. serialize_der ( ) ) . unwrap ( ) ;
272- let signature = key_pair_ring. sign ( config. raw_body . as_bytes ( ) ) ;
329+ let signature = key_pair. sign ( config. raw_body . as_bytes ( ) ) ;
273330
274331 let custom_message_data = HashMap :: from ( [ (
275332 "fakeCRC" . to_string ( ) , //AC is not using the CRC.
276333 vec ! [ SignatureFields {
277334 signature: BASE64_STANDARD . encode( signature. as_ref( ) ) ,
278335 signing_algorithm: ED25519 ,
279- key_id: public_key_fingerprint ( & key_pair . subject_public_key_info ( ) ) ,
336+ key_id,
280337 } ] ,
281338 ) ] ) ;
282339
@@ -295,3 +352,8 @@ fn build_response(
295352 }
296353 . encode_to_vec ( )
297354}
355+
356+ fn generate_key_pair ( ) -> Ed25519KeyPair {
357+ let pkcs8 = Ed25519KeyPair :: generate_pkcs8 ( & SystemRandom :: new ( ) ) . unwrap ( ) ;
358+ Ed25519KeyPair :: from_pkcs8 ( pkcs8. as_ref ( ) ) . unwrap ( )
359+ }
0 commit comments