/src/node/src/quic/tokens.cc
Line | Count | Source (jump to first uncovered line) |
1 | | #if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC |
2 | | |
3 | | #include "tokens.h" |
4 | | #include <crypto/crypto_util.h> |
5 | | #include <ngtcp2/ngtcp2_crypto.h> |
6 | | #include <node_sockaddr-inl.h> |
7 | | #include <string_bytes.h> |
8 | | #include <util-inl.h> |
9 | | #include <algorithm> |
10 | | |
11 | | namespace node { |
12 | | namespace quic { |
13 | | |
14 | | // ============================================================================ |
15 | | // TokenSecret |
16 | | |
17 | 0 | TokenSecret::TokenSecret() : buf_() { |
18 | | // As a performance optimization later, we could consider creating an entropy |
19 | | // cache here similar to what we use for random CIDs so that we do not have |
20 | | // to engage CSPRNG on every call. That, however, is suboptimal for secrets. |
21 | | // If someone manages to get visibility into that cache then they would know |
22 | | // the secrets for a larger number of tokens, which could be bad. For now, |
23 | | // generating on each call is safer, even if less performant. |
24 | 0 | CHECK(crypto::CSPRNG(buf_, QUIC_TOKENSECRET_LEN).is_ok()); |
25 | 0 | } |
26 | | |
27 | 0 | TokenSecret::TokenSecret(const uint8_t* secret) : buf_() { |
28 | 0 | CHECK_NOT_NULL(secret); |
29 | 0 | memcpy(buf_, secret, QUIC_TOKENSECRET_LEN); |
30 | 0 | } |
31 | | |
32 | 0 | TokenSecret::~TokenSecret() { |
33 | 0 | memset(buf_, 0, QUIC_TOKENSECRET_LEN); |
34 | 0 | } |
35 | | |
36 | 0 | TokenSecret::operator const uint8_t*() const { |
37 | 0 | return buf_; |
38 | 0 | } |
39 | | |
40 | 0 | uint8_t TokenSecret::operator[](int pos) const { |
41 | 0 | CHECK_GE(pos, 0); |
42 | 0 | CHECK_LT(pos, QUIC_TOKENSECRET_LEN); |
43 | 0 | return buf_[pos]; |
44 | 0 | } |
45 | | |
46 | 0 | TokenSecret::operator const char*() const { |
47 | 0 | return reinterpret_cast<const char*>(buf_); |
48 | 0 | } |
49 | | |
50 | 0 | std::string TokenSecret::ToString() const { |
51 | 0 | char dest[QUIC_TOKENSECRET_LEN * 2]; |
52 | 0 | size_t written = StringBytes::hex_encode( |
53 | 0 | *this, QUIC_TOKENSECRET_LEN, dest, arraysize(dest)); |
54 | 0 | DCHECK_EQ(written, arraysize(dest)); |
55 | 0 | return std::string(dest, written); |
56 | 0 | } |
57 | | |
58 | | // ============================================================================ |
59 | | // StatelessResetToken |
60 | | |
61 | 122k | StatelessResetToken::StatelessResetToken() : ptr_(nullptr), buf_() {} |
62 | | |
63 | 0 | StatelessResetToken::StatelessResetToken(const uint8_t* token) : ptr_(token) {} |
64 | | |
65 | | StatelessResetToken::StatelessResetToken(const TokenSecret& secret, |
66 | | const CID& cid) |
67 | 0 | : ptr_(buf_) { |
68 | 0 | CHECK_EQ(ngtcp2_crypto_generate_stateless_reset_token( |
69 | 0 | buf_, secret, kStatelessTokenLen, cid), |
70 | 0 | 0); |
71 | 0 | } |
72 | | |
73 | | StatelessResetToken::StatelessResetToken(uint8_t* token, |
74 | | const TokenSecret& secret, |
75 | | const CID& cid) |
76 | 0 | : ptr_(token) { |
77 | 0 | CHECK_EQ(ngtcp2_crypto_generate_stateless_reset_token( |
78 | 0 | token, secret, kStatelessTokenLen, cid), |
79 | 0 | 0); |
80 | 0 | } |
81 | | |
82 | | StatelessResetToken::StatelessResetToken(const StatelessResetToken& other) |
83 | 0 | : ptr_(buf_) { |
84 | 0 | if (other) { |
85 | 0 | memcpy(buf_, other.ptr_, kStatelessTokenLen); |
86 | 0 | } else { |
87 | 0 | ptr_ = nullptr; |
88 | 0 | } |
89 | 0 | } |
90 | | |
91 | 0 | StatelessResetToken::operator const uint8_t*() const { |
92 | 0 | return ptr_ != nullptr ? ptr_ : buf_; |
93 | 0 | } |
94 | | |
95 | 0 | StatelessResetToken::operator const char*() const { |
96 | 0 | return reinterpret_cast<const char*>(ptr_ != nullptr ? ptr_ : buf_); |
97 | 0 | } |
98 | | |
99 | 0 | StatelessResetToken::operator bool() const { |
100 | 0 | return ptr_ != nullptr; |
101 | 0 | } |
102 | | |
103 | 0 | bool StatelessResetToken::operator==(const StatelessResetToken& other) const { |
104 | 0 | if (ptr_ == other.ptr_) return true; |
105 | 0 | if ((ptr_ == nullptr && other.ptr_ != nullptr) || |
106 | 0 | (ptr_ != nullptr && other.ptr_ == nullptr)) { |
107 | 0 | return false; |
108 | 0 | } |
109 | 0 | return memcmp(ptr_, other.ptr_, kStatelessTokenLen) == 0; |
110 | 0 | } |
111 | | |
112 | 0 | bool StatelessResetToken::operator!=(const StatelessResetToken& other) const { |
113 | 0 | return !(*this == other); |
114 | 0 | } |
115 | | |
116 | 0 | std::string StatelessResetToken::ToString() const { |
117 | 0 | if (ptr_ == nullptr) return std::string(); |
118 | 0 | char dest[kStatelessTokenLen * 2]; |
119 | 0 | size_t written = |
120 | 0 | StringBytes::hex_encode(*this, kStatelessTokenLen, dest, arraysize(dest)); |
121 | 0 | DCHECK_EQ(written, arraysize(dest)); |
122 | 0 | return std::string(dest, written); |
123 | 0 | } |
124 | | |
125 | | size_t StatelessResetToken::Hash::operator()( |
126 | 0 | const StatelessResetToken& token) const { |
127 | 0 | size_t hash = 0; |
128 | 0 | if (token.ptr_ == nullptr) return hash; |
129 | 0 | for (size_t n = 0; n < kStatelessTokenLen; n++) |
130 | 0 | hash ^= std::hash<uint8_t>{}(token.ptr_[n]) + 0x9e3779b9 + (hash << 6) + |
131 | 0 | (hash >> 2); |
132 | 0 | return hash; |
133 | 0 | } |
134 | | |
135 | | StatelessResetToken StatelessResetToken::kInvalid; |
136 | | |
137 | | // ============================================================================ |
138 | | // RetryToken and RegularToken |
139 | | namespace { |
140 | | ngtcp2_vec GenerateRetryToken(uint8_t* buffer, |
141 | | uint32_t version, |
142 | | const SocketAddress& address, |
143 | | const CID& retry_cid, |
144 | | const CID& odcid, |
145 | 0 | const TokenSecret& token_secret) { |
146 | 0 | ssize_t ret = |
147 | 0 | ngtcp2_crypto_generate_retry_token(buffer, |
148 | 0 | token_secret, |
149 | 0 | TokenSecret::QUIC_TOKENSECRET_LEN, |
150 | 0 | version, |
151 | 0 | address.data(), |
152 | 0 | address.length(), |
153 | 0 | retry_cid, |
154 | 0 | odcid, |
155 | 0 | uv_hrtime()); |
156 | 0 | DCHECK_GE(ret, 0); |
157 | 0 | DCHECK_LE(ret, RetryToken::kRetryTokenLen); |
158 | 0 | DCHECK_EQ(buffer[0], RetryToken::kTokenMagic); |
159 | | // This shouldn't be possible but we handle it anyway just to be safe. |
160 | 0 | if (ret == 0) return {nullptr, 0}; |
161 | 0 | return {buffer, static_cast<size_t>(ret)}; |
162 | 0 | } |
163 | | |
164 | | ngtcp2_vec GenerateRegularToken(uint8_t* buffer, |
165 | | uint32_t version, |
166 | | const SocketAddress& address, |
167 | 0 | const TokenSecret& token_secret) { |
168 | 0 | ssize_t ret = |
169 | 0 | ngtcp2_crypto_generate_regular_token(buffer, |
170 | 0 | token_secret, |
171 | 0 | TokenSecret::QUIC_TOKENSECRET_LEN, |
172 | 0 | address.data(), |
173 | 0 | address.length(), |
174 | 0 | uv_hrtime()); |
175 | 0 | DCHECK_GE(ret, 0); |
176 | 0 | DCHECK_LE(ret, RegularToken::kRegularTokenLen); |
177 | 0 | DCHECK_EQ(buffer[0], RegularToken::kTokenMagic); |
178 | | // This shouldn't be possible but we handle it anyway just to be safe. |
179 | 0 | if (ret == 0) return {nullptr, 0}; |
180 | 0 | return {buffer, static_cast<size_t>(ret)}; |
181 | 0 | } |
182 | | } // namespace |
183 | | |
184 | | RetryToken::RetryToken(uint32_t version, |
185 | | const SocketAddress& address, |
186 | | const CID& retry_cid, |
187 | | const CID& odcid, |
188 | | const TokenSecret& token_secret) |
189 | | : buf_(), |
190 | 0 | ptr_(GenerateRetryToken( |
191 | 0 | buf_, version, address, retry_cid, odcid, token_secret)) {} |
192 | | |
193 | | RetryToken::RetryToken(const uint8_t* token, size_t size) |
194 | 0 | : ptr_(ngtcp2_vec{const_cast<uint8_t*>(token), size}) { |
195 | 0 | DCHECK_LE(size, RetryToken::kRetryTokenLen); |
196 | 0 | DCHECK_IMPLIES(token == nullptr, size = 0); |
197 | 0 | } |
198 | | |
199 | | std::optional<CID> RetryToken::Validate(uint32_t version, |
200 | | const SocketAddress& addr, |
201 | | const CID& dcid, |
202 | | const TokenSecret& token_secret, |
203 | 0 | uint64_t verification_expiration) { |
204 | 0 | if (ptr_.base == nullptr || ptr_.len == 0) return std::nullopt; |
205 | 0 | ngtcp2_cid ocid; |
206 | 0 | int ret = ngtcp2_crypto_verify_retry_token( |
207 | 0 | &ocid, |
208 | 0 | ptr_.base, |
209 | 0 | ptr_.len, |
210 | 0 | token_secret, |
211 | 0 | TokenSecret::QUIC_TOKENSECRET_LEN, |
212 | 0 | version, |
213 | 0 | addr.data(), |
214 | 0 | addr.length(), |
215 | 0 | dcid, |
216 | 0 | std::min(verification_expiration, QUIC_MIN_RETRYTOKEN_EXPIRATION), |
217 | 0 | uv_hrtime()); |
218 | 0 | if (ret != 0) return std::nullopt; |
219 | 0 | return std::optional<CID>(ocid); |
220 | 0 | } |
221 | | |
222 | 0 | RetryToken::operator const ngtcp2_vec&() const { |
223 | 0 | return ptr_; |
224 | 0 | } |
225 | 0 | RetryToken::operator const ngtcp2_vec*() const { |
226 | 0 | return &ptr_; |
227 | 0 | } |
228 | | |
229 | 0 | std::string RetryToken::ToString() const { |
230 | 0 | if (ptr_.base == nullptr) return std::string(); |
231 | 0 | MaybeStackBuffer<char, 32> dest(ptr_.len * 2); |
232 | 0 | size_t written = |
233 | 0 | StringBytes::hex_encode(*this, ptr_.len, dest.out(), dest.length()); |
234 | 0 | DCHECK_EQ(written, dest.length()); |
235 | 0 | return std::string(dest.out(), written); |
236 | 0 | } |
237 | | |
238 | 0 | RetryToken::operator const char*() const { |
239 | 0 | return reinterpret_cast<const char*>(ptr_.base); |
240 | 0 | } |
241 | | |
242 | 0 | RetryToken::operator bool() const { |
243 | 0 | return ptr_.base != nullptr && ptr_.len > 0; |
244 | 0 | } |
245 | | |
246 | 0 | RegularToken::RegularToken() : buf_(), ptr_(ngtcp2_vec{nullptr, 0}) {} |
247 | | |
248 | | RegularToken::RegularToken(uint32_t version, |
249 | | const SocketAddress& address, |
250 | | const TokenSecret& token_secret) |
251 | | : buf_(), |
252 | 0 | ptr_(GenerateRegularToken(buf_, version, address, token_secret)) {} |
253 | | |
254 | | RegularToken::RegularToken(const uint8_t* token, size_t size) |
255 | 0 | : ptr_(ngtcp2_vec{const_cast<uint8_t*>(token), size}) { |
256 | 0 | DCHECK_LE(size, RegularToken::kRegularTokenLen); |
257 | 0 | DCHECK_IMPLIES(token == nullptr, size = 0); |
258 | 0 | } |
259 | | |
260 | 0 | RegularToken::operator bool() const { |
261 | 0 | return ptr_.base != nullptr && ptr_.len > 0; |
262 | 0 | } |
263 | | |
264 | | bool RegularToken::Validate(uint32_t version, |
265 | | const SocketAddress& addr, |
266 | | const TokenSecret& token_secret, |
267 | 0 | uint64_t verification_expiration) { |
268 | 0 | if (ptr_.base == nullptr || ptr_.len == 0) return false; |
269 | 0 | return ngtcp2_crypto_verify_regular_token( |
270 | 0 | ptr_.base, |
271 | 0 | ptr_.len, |
272 | 0 | token_secret, |
273 | 0 | TokenSecret::QUIC_TOKENSECRET_LEN, |
274 | 0 | addr.data(), |
275 | 0 | addr.length(), |
276 | 0 | std::min(verification_expiration, |
277 | 0 | QUIC_MIN_REGULARTOKEN_EXPIRATION), |
278 | 0 | uv_hrtime()) == 0; |
279 | 0 | } |
280 | | |
281 | 0 | RegularToken::operator const ngtcp2_vec&() const { |
282 | 0 | return ptr_; |
283 | 0 | } |
284 | 0 | RegularToken::operator const ngtcp2_vec*() const { |
285 | 0 | return &ptr_; |
286 | 0 | } |
287 | | |
288 | 0 | std::string RegularToken::ToString() const { |
289 | 0 | if (ptr_.base == nullptr) return std::string(); |
290 | 0 | MaybeStackBuffer<char, 32> dest(ptr_.len * 2); |
291 | 0 | size_t written = |
292 | 0 | StringBytes::hex_encode(*this, ptr_.len, dest.out(), dest.length()); |
293 | 0 | DCHECK_EQ(written, dest.length()); |
294 | 0 | return std::string(dest.out(), written); |
295 | 0 | } |
296 | | |
297 | 0 | RegularToken::operator const char*() const { |
298 | 0 | return reinterpret_cast<const char*>(ptr_.base); |
299 | 0 | } |
300 | | |
301 | | } // namespace quic |
302 | | } // namespace node |
303 | | |
304 | | #endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC |