@@ -8,8 +8,9 @@ use actix_web::{
8
8
App , Error , HttpRequest , HttpServer ,
9
9
} ;
10
10
use actix_web_lab:: extract:: { Json , RequestSignature , RequestSignatureScheme } ;
11
+ use async_trait:: async_trait;
11
12
use bytes:: { BufMut as _, BytesMut } ;
12
- use ed25519_dalek:: { Signature , Verifier as _, VerifyingKey } ;
13
+ use ed25519_dalek:: { PublicKey , Signature , StreamVerifier , Verifier as _, VerifyingKey } ;
13
14
use hex_literal:: hex;
14
15
use once_cell:: sync:: Lazy ;
15
16
use rustls:: { pki_types:: PrivateKeyDer , ServerConfig } ;
@@ -24,13 +25,16 @@ static TS_HDR_NAME: HeaderName = HeaderName::from_static("x-signature-timestamp"
24
25
static APP_PUBLIC_KEY : Lazy < VerifyingKey > =
25
26
Lazy :: new ( || VerifyingKey :: from_bytes ( APP_PUBLIC_KEY_BYTES ) . unwrap ( ) ) ;
26
27
28
+ /// Signature scheme for Discord interactions/webhooks.
29
+ ///
30
+ /// Verification is done in `finalize` so this does not support optional verification.
27
31
#[ derive( Debug ) ]
28
32
struct DiscordWebhook {
29
33
/// Signature taken from webhook request header.
30
34
candidate_signature : Signature ,
31
35
32
- /// Cloned payload state .
33
- chunks : Vec < Bytes > ,
36
+ /// Signature verifier .
37
+ verifier : StreamVerifier ,
34
38
}
35
39
36
40
impl DiscordWebhook {
@@ -58,50 +62,44 @@ impl DiscordWebhook {
58
62
}
59
63
60
64
impl RequestSignatureScheme for DiscordWebhook {
61
- type Signature = ( BytesMut , Signature ) ;
65
+ type Signature = Signature ;
62
66
63
67
type Error = Error ;
64
68
65
69
async fn init ( req : & HttpRequest ) -> Result < Self , Self :: Error > {
66
70
let ts = Self :: get_timestamp ( req) ?. to_owned ( ) ;
67
71
let candidate_signature = Self :: get_signature ( req) ?;
68
72
73
+ let mut verifier = APP_PUBLIC_KEY
74
+ . verify_stream ( & candidate_signature)
75
+ . map_err ( error:: ErrorBadRequest ) ?;
76
+
77
+ verifier. update ( ts) ;
78
+
69
79
Ok ( Self {
70
80
candidate_signature,
71
- chunks : vec ! [ Bytes :: from ( ts ) ] ,
81
+ verifier ,
72
82
} )
73
83
}
74
84
75
85
async fn consume_chunk ( & mut self , _req : & HttpRequest , chunk : Bytes ) -> Result < ( ) , Self :: Error > {
76
- self . chunks . push ( chunk) ;
86
+ self . verifier . update ( chunk) ;
77
87
Ok ( ( ) )
78
88
}
79
89
80
90
async fn finalize ( self , _req : & HttpRequest ) -> Result < Self :: Signature , Self :: Error > {
81
- let buf_len = self . chunks . iter ( ) . map ( |chunk| chunk. len ( ) ) . sum ( ) ;
82
- let mut buf = BytesMut :: with_capacity ( buf_len) ;
83
-
84
- for chunk in self . chunks {
85
- buf. put ( chunk) ;
86
- }
91
+ self . verifier . finalize_and_verify ( ) . map_err ( |_| {
92
+ error:: ErrorUnauthorized ( "given signature does not match calculated signature" )
93
+ } ) ?;
87
94
88
- Ok ( ( buf , self . candidate_signature ) )
95
+ Ok ( self . candidate_signature )
89
96
}
90
97
91
98
fn verify (
92
- ( payload , candidate_signature ) : Self :: Signature ,
99
+ signature : Self :: Signature ,
93
100
_req : & HttpRequest ,
94
101
) -> Result < Self :: Signature , Self :: Error > {
95
- if APP_PUBLIC_KEY
96
- . verify ( & payload, & candidate_signature)
97
- . is_ok ( )
98
- {
99
- Ok ( ( payload, candidate_signature) )
100
- } else {
101
- Err ( error:: ErrorUnauthorized (
102
- "given signature does not match calculated signature" ,
103
- ) )
104
- }
102
+ Ok ( signature)
105
103
}
106
104
}
107
105
@@ -119,6 +117,7 @@ async fn main() -> io::Result<()> {
119
117
let ( Json ( form) , _) = body. into_parts ( ) ;
120
118
println ! ( "{}" , serde_json:: to_string_pretty( & form) . unwrap( ) ) ;
121
119
120
+ // reply with PONG code
122
121
web:: Json ( serde_json:: json!( {
123
122
"type" : 1
124
123
} ) )
0 commit comments