1+ use std:: path:: PathBuf ;
12use std:: pin:: Pin ;
23use std:: str:: FromStr ;
34use std:: sync:: Arc ;
45use std:: task:: { Context , Poll } ;
6+ use std:: time:: { Duration , Instant } ;
57
68use anyhow:: Result ;
79use axum:: body:: { Body , Bytes } ;
8- use axum:: http:: header:: { HeaderValue , ACCESS_CONTROL_ALLOW_ORIGIN , CONTENT_TYPE } ;
10+ use axum:: http:: header:: { HeaderValue , ACCESS_CONTROL_ALLOW_ORIGIN , CACHE_CONTROL , CONTENT_TYPE } ;
911use axum:: http:: { Method , Request , Response , StatusCode , Uri } ;
1012use http_body_util:: BodyExt ;
1113use payjoin:: directory:: { ShortId , ShortIdError , ENCAPSULATED_MESSAGE_BYTES } ;
14+ use tokio:: sync:: RwLock ;
1215use tracing:: { debug, error, trace, warn} ;
1316
1417use crate :: db:: { Db , Error as DbError , SendableError } ;
@@ -28,6 +31,90 @@ const V1_VERSION_UNSUPPORTED_RES_JSON: &str =
2831
2932pub type BoxError = Box < dyn std:: error:: Error + Send + Sync > ;
3033
34+ /// Two-slot OHTTP key set supporting rotation overlap.
35+ ///
36+ /// Key IDs alternate between 1 and 2. The current key is served to new
37+ /// clients; both slots are accepted for decapsulation so that clients
38+ /// with a cached previous key still work during the overlap window.
39+ #[ derive( Debug ) ]
40+ pub struct KeyRotatingServer {
41+ keys : [ Option < ohttp:: Server > ; 2 ] ,
42+ current_key_id : u8 ,
43+ current_key_created_at : Instant ,
44+ }
45+
46+ impl KeyRotatingServer {
47+ pub fn from_single ( server : ohttp:: Server , key_id : u8 ) -> Self {
48+ assert ! ( key_id == 1 || key_id == 2 , "key_id must be 1 or 2" ) ;
49+ let mut keys = [ None , None ] ;
50+ keys[ ( key_id - 1 ) as usize ] = Some ( server) ;
51+ Self { current_key_id : key_id, keys, current_key_created_at : Instant :: now ( ) }
52+ }
53+
54+ pub fn from_pair (
55+ current : ( u8 , ohttp:: Server ) ,
56+ previous : Option < ( u8 , ohttp:: Server ) > ,
57+ current_key_age : Duration ,
58+ ) -> Self {
59+ assert ! ( current. 0 == 1 || current. 0 == 2 , "key_id must be 1 or 2" ) ;
60+ let mut keys = [ None , None ] ;
61+ keys[ ( current. 0 - 1 ) as usize ] = Some ( current. 1 ) ;
62+ if let Some ( ( id, server) ) = previous {
63+ assert ! ( id == 1 || id == 2 , "key_id must be 1 or 2" ) ;
64+ keys[ ( id - 1 ) as usize ] = Some ( server) ;
65+ }
66+ let created_at = Instant :: now ( ) . checked_sub ( current_key_age) . unwrap_or_else ( Instant :: now) ;
67+ Self { current_key_id : current. 0 , keys, current_key_created_at : created_at }
68+ }
69+
70+ pub fn current_key_id ( & self ) -> u8 { self . current_key_id }
71+ pub fn current_key_created_at ( & self ) -> Instant { self . current_key_created_at }
72+ pub fn next_key_id ( & self ) -> u8 {
73+ if self . current_key_id == 1 {
74+ 2
75+ } else {
76+ 1
77+ }
78+ }
79+
80+ /// Look up the server matching the key_id in an OHTTP message and
81+ /// decapsulate. The first byte of an OHTTP encapsulated request is the
82+ /// key identifier (RFC 9458 Section 4.3).
83+ pub fn decapsulate (
84+ & self ,
85+ ohttp_body : & [ u8 ] ,
86+ ) -> std:: result:: Result < ( Vec < u8 > , ohttp:: ServerResponse ) , ohttp:: Error > {
87+ let key_id = ohttp_body. first ( ) . copied ( ) . unwrap_or ( 0 ) ;
88+ let server = key_id
89+ . checked_sub ( 1 )
90+ . filter ( |& i| ( i as usize ) < 2 )
91+ . and_then ( |i| self . keys [ i as usize ] . as_ref ( ) ) ;
92+ match server {
93+ Some ( s) => s. decapsulate ( ohttp_body) ,
94+ None => Err ( ohttp:: Error :: Truncated ) ,
95+ }
96+ }
97+
98+ /// Encode the current key's config for serving to clients.
99+ pub fn encode_current ( & self ) -> std:: result:: Result < Vec < u8 > , ohttp:: Error > {
100+ self . keys [ ( self . current_key_id - 1 ) as usize ]
101+ . as_ref ( )
102+ . expect ( "current key must exist" )
103+ . config ( )
104+ . encode ( )
105+ }
106+
107+ /// Install a new key as current, displacing whatever occupied that slot.
108+ ///
109+ /// The old current key remains in its slot for overlap decapsulation.
110+ pub fn rotate ( & mut self , server : ohttp:: Server ) {
111+ let new_key_id = self . next_key_id ( ) ;
112+ self . keys [ ( new_key_id - 1 ) as usize ] = Some ( server) ;
113+ self . current_key_id = new_key_id;
114+ self . current_key_created_at = Instant :: now ( ) ;
115+ }
116+ }
117+
31118/// Opaque blocklist of Bitcoin addresses stored as script pubkeys.
32119///
33120/// Addresses are converted to `ScriptBuf` at parse time so that
@@ -91,7 +178,9 @@ fn parse_address_lines(text: &str) -> std::collections::HashSet<bitcoin::ScriptB
91178#[ derive( Clone ) ]
92179pub struct Service < D : Db > {
93180 db : D ,
94- ohttp : ohttp:: Server ,
181+ ohttp : Arc < RwLock < KeyRotatingServer > > ,
182+ ohttp_keys_max_age : Option < Duration > ,
183+ ohttp_keys_dir : Option < PathBuf > ,
95184 sentinel_tag : SentinelTag ,
96185 v1 : Option < V1 > ,
97186}
@@ -117,8 +206,15 @@ where
117206}
118207
119208impl < D : Db > Service < D > {
120- pub fn new ( db : D , ohttp : ohttp:: Server , sentinel_tag : SentinelTag , v1 : Option < V1 > ) -> Self {
121- Self { db, ohttp, sentinel_tag, v1 }
209+ pub fn new (
210+ db : D ,
211+ ohttp : Arc < RwLock < KeyRotatingServer > > ,
212+ ohttp_keys_max_age : Option < Duration > ,
213+ ohttp_keys_dir : Option < PathBuf > ,
214+ sentinel_tag : SentinelTag ,
215+ v1 : Option < V1 > ,
216+ ) -> Self {
217+ Self { db, ohttp, ohttp_keys_max_age, ohttp_keys_dir, sentinel_tag, v1 }
122218 }
123219
124220 async fn serve_request < B > ( & self , req : Request < B > ) -> Result < Response < Body > >
@@ -200,9 +296,9 @@ impl<D: Db> Service<D> {
200296 . map_err ( |e| HandlerError :: BadRequest ( anyhow:: anyhow!( e. into( ) ) ) ) ?
201297 . to_bytes ( ) ;
202298
203- // Decapsulate OHTTP request
204- let ( bhttp_req , res_ctx ) = self
205- . ohttp
299+ // Decapsulate OHTTP request using the key matching the message's key_id
300+ let keyset = self . ohttp . read ( ) . await ;
301+ let ( bhttp_req , res_ctx ) = keyset
206302 . decapsulate ( & ohttp_body)
207303 . map_err ( |e| HandlerError :: OhttpKeyRejection ( e. into ( ) ) ) ?;
208304 let mut cursor = std:: io:: Cursor :: new ( bhttp_req) ;
@@ -377,14 +473,52 @@ impl<D: Db> Service<D> {
377473 }
378474 }
379475
476+ async fn maybe_rotate_keys ( & self ) -> Result < ( ) , HandlerError > {
477+ let max_age = match self . ohttp_keys_max_age {
478+ Some ( m) => m,
479+ None => return Ok ( ( ) ) ,
480+ } ;
481+ if self . ohttp . read ( ) . await . current_key_created_at ( ) . elapsed ( ) < max_age {
482+ return Ok ( ( ) ) ;
483+ }
484+ let mut keyset = self . ohttp . write ( ) . await ;
485+ if keyset. current_key_created_at ( ) . elapsed ( ) < max_age {
486+ return Ok ( ( ) ) ;
487+ }
488+ let new_key_id = keyset. next_key_id ( ) ;
489+ if let Some ( dir) = & self . ohttp_keys_dir {
490+ let _ = tokio:: fs:: remove_file ( dir. join ( format ! ( "{new_key_id}.ikm" ) ) ) . await ;
491+ }
492+ let config = crate :: key_config:: gen_ohttp_server_config_with_id ( new_key_id)
493+ . map_err ( HandlerError :: InternalServerError ) ?;
494+ if let Some ( dir) = & self . ohttp_keys_dir {
495+ crate :: key_config:: persist_key_config ( & config, dir)
496+ . map_err ( HandlerError :: InternalServerError ) ?;
497+ }
498+ let old_key_id = keyset. current_key_id ( ) ;
499+ keyset. rotate ( config. into_server ( ) ) ;
500+ tracing:: info!( "Rotated OHTTP keys: key_id {old_key_id} -> {new_key_id}" ) ;
501+ Ok ( ( ) )
502+ }
503+
380504 async fn get_ohttp_keys ( & self ) -> Result < Response < Body > , HandlerError > {
381- let ohttp_keys = self
382- . ohttp
383- . config ( )
384- . encode ( )
385- . map_err ( |e| HandlerError :: InternalServerError ( e. into ( ) ) ) ?;
505+ self . maybe_rotate_keys ( ) . await ?;
506+ let keyset = self . ohttp . read ( ) . await ;
507+ let ohttp_keys =
508+ keyset. encode_current ( ) . map_err ( |e| HandlerError :: InternalServerError ( e. into ( ) ) ) ?;
386509 let mut res = Response :: new ( full ( ohttp_keys) ) ;
387510 res. headers_mut ( ) . insert ( CONTENT_TYPE , HeaderValue :: from_static ( "application/ohttp-keys" ) ) ;
511+ if let Some ( max_age) = self . ohttp_keys_max_age {
512+ let remaining = max_age. saturating_sub ( keyset. current_key_created_at ( ) . elapsed ( ) ) ;
513+ res. headers_mut ( ) . insert (
514+ CACHE_CONTROL ,
515+ HeaderValue :: from_str ( & format ! (
516+ "public, s-maxage={}, immutable" ,
517+ remaining. as_secs( )
518+ ) )
519+ . expect ( "valid header value" ) ,
520+ ) ;
521+ }
388522 Ok ( res)
389523 }
390524
@@ -485,8 +619,8 @@ impl HandlerError {
485619 }
486620 HandlerError :: OhttpKeyRejection ( e) => {
487621 const OHTTP_KEY_REJECTION_RES_JSON : & str = r#"{"type":"https://iana.org/assignments/http-problem-types#ohttp-key", "title": "key identifier unknown"}"# ;
488- warn ! ( "Bad request: Key configuration rejected: {}" , e) ;
489- * res. status_mut ( ) = StatusCode :: BAD_REQUEST ;
622+ warn ! ( "Key configuration rejected: {}" , e) ;
623+ * res. status_mut ( ) = StatusCode :: UNPROCESSABLE_ENTITY ;
490624 res. headers_mut ( )
491625 . insert ( CONTENT_TYPE , HeaderValue :: from_static ( "application/problem+json" ) ) ;
492626 * res. body_mut ( ) = full ( OHTTP_KEY_REJECTION_RES_JSON ) ;
@@ -592,9 +726,9 @@ mod tests {
592726 async fn test_service ( v1 : Option < V1 > ) -> Service < FilesDb > {
593727 let dir = tempfile:: tempdir ( ) . expect ( "tempdir" ) ;
594728 let db = FilesDb :: init ( Duration :: from_millis ( 100 ) , dir. keep ( ) ) . await . expect ( "db init" ) ;
595- let ohttp : ohttp :: Server =
596- crate :: key_config :: gen_ohttp_server_config ( ) . expect ( "ohttp config" ) . into ( ) ;
597- Service :: new ( db, ohttp , SentinelTag :: new ( [ 0u8 ; 32 ] ) , v1)
729+ let config = crate :: key_config :: gen_ohttp_server_config ( ) . expect ( "ohttp config" ) ;
730+ let keyset = Arc :: new ( RwLock :: new ( KeyRotatingServer :: from_single ( config. into_server ( ) , 1 ) ) ) ;
731+ Service :: new ( db, keyset , None , None , SentinelTag :: new ( [ 0u8 ; 32 ] ) , v1)
598732 }
599733
600734 /// A valid ShortId encoded as bech32 for use in URL paths.
@@ -826,9 +960,9 @@ mod tests {
826960 let dir = tempfile:: tempdir ( ) . expect ( "tempdir" ) ;
827961 let db = FilesDb :: init ( Duration :: from_millis ( 100 ) , dir. keep ( ) ) . await . expect ( "db init" ) ;
828962 let db = MetricsDb :: new ( db, metrics) ;
829- let ohttp : ohttp :: Server =
830- crate :: key_config :: gen_ohttp_server_config ( ) . expect ( "ohttp config" ) . into ( ) ;
831- let svc = Service :: new ( db, ohttp , SentinelTag :: new ( [ 0u8 ; 32 ] ) , None ) ;
963+ let config = crate :: key_config :: gen_ohttp_server_config ( ) . expect ( "ohttp config" ) ;
964+ let keyset = Arc :: new ( RwLock :: new ( KeyRotatingServer :: from_single ( config. into_server ( ) , 1 ) ) ) ;
965+ let svc = Service :: new ( db, keyset , None , None , SentinelTag :: new ( [ 0u8 ; 32 ] ) , None ) ;
832966
833967 let id = valid_short_id_path ( ) ;
834968 let res = svc
0 commit comments