libmongocrypt-helper 1.20.3.0.1001 → 1.20.5.0.1001

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b7037c7cdeb5a2f1fe94446f583e7c953539328df98580028a07f46c296689ec
4
- data.tar.gz: 52a6c553f0e7ee8e0c6aca713184fe9c13f7e5babf97c18e4eff17235b8e7109
3
+ metadata.gz: b483fc7b479e693858209265fa305b4bd51e34592b452bbbd36c32c13f262802
4
+ data.tar.gz: 352d1052b07f14081865b5c02fe8b33ce2b0a739550db43a6e57d6c26cd1a08b
5
5
  SHA512:
6
- metadata.gz: 190dff33114c3f35a8ba07776144bdfde51523e75277176c4c0bf5bdef0be4d500500c3b11eed1520635e43d7b8201faec4faca54da7c99bd119da8d7c3f606e
7
- data.tar.gz: 20446b82d1838e42ce90980bea22112b4af933cd66daa0984e15b00f1dde2867f408d87f5ed2f86ec146db64b4f0201c0a9be9cdcc30923e48acff5793ab1aa5
6
+ metadata.gz: 945910ccf2515b64cbd9dd1d97a9de2822562a2544533006e3aa5fa7837107697f6b38e5f43d0da1ce3218fa98d36362a9f883362d9e1efb16bb90fbee7d5236
7
+ data.tar.gz: ef89918bdf09d579c60cce8aa5ecdd779a655edffc7223fef5a97fc00fa63ef787c3d5b360ce880812dc3d96562c1ebda7ca158ba4968377e1fd95fcb9c9a81c
@@ -1,5 +1,17 @@
1
1
  # ChangeLog
2
2
 
3
+ ## 1.20.5
4
+
5
+ ### Fixed
6
+ - Fix possible abort parsing endpoints.
7
+ - Reject more payloads that cannot be decrypted in explicit decryption.
8
+ - Fix more degenerate cases of masterKey parsing.
9
+
10
+ ## 1.20.4
11
+
12
+ ### Fixed
13
+ - Reject undersized decryption payloads.
14
+
3
15
  ## 1.20.3
4
16
 
5
17
  ### Fixed
@@ -57,7 +57,7 @@
57
57
  }
58
58
  ],
59
59
  "metadata": {
60
- "timestamp": "2026-08-07T17:48:10.053548+00:00",
60
+ "timestamp": "2026-09-03T12:34:15.836217+00:00",
61
61
  "tools": [
62
62
  {
63
63
  "externalReferences": [
@@ -100,7 +100,7 @@
100
100
  }
101
101
  ]
102
102
  },
103
- "serialNumber": "urn:uuid:464d2591-6478-4559-a1c4-9a726248fc1e",
103
+ "serialNumber": "urn:uuid:f38989f9-86cb-4a6e-94b5-bbcbf0a8fc1f",
104
104
  "version": 1,
105
105
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
106
106
  "bomFormat": "CycloneDX",
@@ -523,7 +523,12 @@ const _mongocrypt_buffer_t *mc_FLE2InsertUpdatePayloadV2_decrypt(_mongocrypt_cry
523
523
  return NULL;
524
524
  }
525
525
 
526
- _mongocrypt_buffer_resize(&iup->plaintext, fle2v2->get_plaintext_len(ciphertext.len, status));
526
+ const uint32_t plaintext_len = fle2v2->get_plaintext_len(ciphertext.len, status);
527
+ if (!mongocrypt_status_ok(status)) {
528
+ return NULL;
529
+ }
530
+
531
+ _mongocrypt_buffer_resize(&iup->plaintext, plaintext_len);
527
532
  uint32_t bytes_written;
528
533
 
529
534
  if (!fle2v2->do_decrypt(crypto, &iup->userKeyId, user_key, &ciphertext, &iup->plaintext, &bytes_written, status)) {
@@ -252,7 +252,12 @@ const _mongocrypt_buffer_t *mc_FLE2InsertUpdatePayload_decrypt(_mongocrypt_crypt
252
252
  return NULL;
253
253
  }
254
254
 
255
- _mongocrypt_buffer_resize(&iup->plaintext, fle2aead->get_plaintext_len(ciphertext.len, status));
255
+ const uint32_t plaintext_len = fle2aead->get_plaintext_len(ciphertext.len, status);
256
+ if (!mongocrypt_status_ok(status)) {
257
+ return NULL;
258
+ }
259
+
260
+ _mongocrypt_buffer_resize(&iup->plaintext, plaintext_len);
256
261
  uint32_t bytes_written; /* ignored */
257
262
 
258
263
  if (!fle2aead
@@ -353,7 +353,11 @@ static bool mc_FLE2IndexedEncryptedValue_decrypt(_mongocrypt_crypto_t *crypto,
353
353
  const _mongocrypt_buffer_t *token_buf = mc_ServerDataEncryptionLevel1Token_get(token);
354
354
  uint32_t bytes_written;
355
355
 
356
- _mongocrypt_buffer_resize(&iev->Inner, fle2alg->get_plaintext_len(iev->InnerEncrypted.len, status));
356
+ const uint32_t inner_len = fle2alg->get_plaintext_len(iev->InnerEncrypted.len, status);
357
+ if (!mongocrypt_status_ok(status)) {
358
+ return false;
359
+ }
360
+ _mongocrypt_buffer_resize(&iev->Inner, inner_len);
357
361
 
358
362
  /* Decrypt InnerEncrypted. */
359
363
  if (!fle2alg->do_decrypt(crypto,
@@ -445,7 +449,11 @@ bool mc_FLE2IndexedEncryptedValue_add_K_Key(_mongocrypt_crypto_t *crypto,
445
449
  return false;
446
450
  }
447
451
  /* Attempt to decrypt ClientEncryptedValue */
448
- _mongocrypt_buffer_resize(&iev->ClientValue, fle2aead->get_plaintext_len(iev->ClientEncryptedValue.len, status));
452
+ const uint32_t client_value_len = fle2aead->get_plaintext_len(iev->ClientEncryptedValue.len, status);
453
+ if (!mongocrypt_status_ok(status)) {
454
+ return false;
455
+ }
456
+ _mongocrypt_buffer_resize(&iev->ClientValue, client_value_len);
449
457
  uint32_t bytes_written;
450
458
  if (!fle2aead->do_decrypt(crypto,
451
459
  &iev->K_KeyId,
@@ -93,7 +93,7 @@ const _mongocrypt_buffer_t *_mc_FLE2UnindexedEncryptedValueCommon_decrypt(_mongo
93
93
  memcpy(AD.data + 1, key_uuid->data, key_uuid->len);
94
94
  AD.data[1 + key_uuid->len] = (uint8_t)original_bson_type;
95
95
  const uint32_t plaintext_len = fle2aead->get_plaintext_len(ciphertext->len, status);
96
- if (plaintext_len == 0) {
96
+ if (!mongocrypt_status_ok(status)) {
97
97
  _mongocrypt_buffer_cleanup(&AD);
98
98
  return NULL;
99
99
  }
@@ -443,9 +443,9 @@ static uint32_t _mongocrypt_calculate_plaintext_len(uint32_t inlen,
443
443
  _mongocrypt_hmac_type_t hmac,
444
444
  mongocrypt_status_t *status) {
445
445
  const uint32_t hmaclen = (hmac == HMAC_NONE) ? 0 : MONGOCRYPT_HMAC_LEN;
446
- const uint32_t mincipher = (mode == MODE_CTR) ? 0 : MONGOCRYPT_BLOCK_SIZE;
446
+ const uint32_t mincipher = (mode == MODE_CTR) ? 1 : MONGOCRYPT_BLOCK_SIZE;
447
447
  if (inlen < (MONGOCRYPT_IV_LEN + mincipher + hmaclen)) {
448
- CLIENT_ERR("input ciphertext too small. Must be at least %" PRIu32 " bytes",
448
+ CLIENT_ERR("input ciphertext too small. Must be more than %" PRIu32 " bytes",
449
449
  MONGOCRYPT_IV_LEN + mincipher + hmaclen);
450
450
  return 0;
451
451
  }
@@ -1346,7 +1346,11 @@ bool _mongocrypt_unwrap_key(_mongocrypt_crypto_t *crypto,
1346
1346
 
1347
1347
  // _mongocrypt_wrap_key() uses FLE1 algorithm parameters.
1348
1348
  _mongocrypt_buffer_init(dek);
1349
- _mongocrypt_buffer_resize(dek, fle1alg->get_plaintext_len(encrypted_dek->len, status));
1349
+ const uint32_t plaintext_len = fle1alg->get_plaintext_len(encrypted_dek->len, status);
1350
+ if (!mongocrypt_status_ok(status)) {
1351
+ return false;
1352
+ }
1353
+ _mongocrypt_buffer_resize(dek, plaintext_len);
1350
1354
 
1351
1355
  if (!fle1alg->do_decrypt(crypto, NULL /* associated data. */, kek, encrypted_dek, dek, &bytes_written, status)) {
1352
1356
  return false;
@@ -344,7 +344,7 @@ static bool _replace_FLE1Payload_with_plaintext(void *ctx,
344
344
 
345
345
  const _mongocrypt_value_encryption_algorithm_t *fle1alg = _mcFLE1Algorithm();
346
346
  plaintext.len = fle1alg->get_plaintext_len(ciphertext.data.len, status);
347
- CHECK_AND_RETURN(plaintext.len != 0);
347
+ CHECK_AND_RETURN(mongocrypt_status_ok(status));
348
348
  plaintext.data = bson_malloc0(plaintext.len);
349
349
  BSON_ASSERT(plaintext.data);
350
350
 
@@ -766,6 +766,25 @@ static void _cleanup(mongocrypt_ctx_t *ctx) {
766
766
  _mongocrypt_buffer_cleanup(&dctx->decrypted_doc);
767
767
  }
768
768
 
769
+ /* Returns true if a subtype-6 payload's first byte names a recognized FLE blob
770
+ * subtype. */
771
+ static bool _is_decryptable_fle_blob_subtype(uint8_t byte) {
772
+ switch (byte) {
773
+ case MC_SUBTYPE_FLE1DeterministicEncryptedValue:
774
+ case MC_SUBTYPE_FLE1RandomEncryptedValue:
775
+ case MC_SUBTYPE_FLE2InsertUpdatePayload:
776
+ case MC_SUBTYPE_FLE2UnindexedEncryptedValue:
777
+ case MC_SUBTYPE_FLE2IndexedEqualityEncryptedValue:
778
+ case MC_SUBTYPE_FLE2IndexedRangeEncryptedValue:
779
+ case MC_SUBTYPE_FLE2InsertUpdatePayloadV2:
780
+ case MC_SUBTYPE_FLE2IndexedEqualityEncryptedValueV2:
781
+ case MC_SUBTYPE_FLE2IndexedRangeEncryptedValueV2:
782
+ case MC_SUBTYPE_FLE2UnindexedEncryptedValueV2:
783
+ case MC_SUBTYPE_FLE2IndexedTextEncryptedValue: return true;
784
+ default: return false;
785
+ }
786
+ }
787
+
769
788
  bool mongocrypt_ctx_explicit_decrypt_init(mongocrypt_ctx_t *ctx, mongocrypt_binary_t *msg) {
770
789
  bson_iter_t iter;
771
790
  bson_t as_bson;
@@ -804,6 +823,14 @@ bool mongocrypt_ctx_explicit_decrypt_init(mongocrypt_ctx_t *ctx, mongocrypt_bina
804
823
  CLIENT_ERR("decryption expected BSON binary subtype %d, got %d", (int)BSON_SUBTYPE_ENCRYPTED, (int)subtype);
805
824
  return _mongocrypt_ctx_fail(ctx);
806
825
  }
826
+ if (binary_len == 0) {
827
+ CLIENT_ERR("unsupported FLE blob subtype: expected non-empty payload");
828
+ return _mongocrypt_ctx_fail(ctx);
829
+ }
830
+ if (!_is_decryptable_fle_blob_subtype(binary[0])) {
831
+ CLIENT_ERR("unsupported FLE blob subtype: %hhu", binary[0]);
832
+ return _mongocrypt_ctx_fail(ctx);
833
+ }
807
834
  }
808
835
 
809
836
  if (!mongocrypt_ctx_decrypt_init(ctx, msg)) {
@@ -34,6 +34,104 @@ void _mongocrypt_endpoint_destroy(_mongocrypt_endpoint_t *endpoint) {
34
34
  bson_free(endpoint);
35
35
  }
36
36
 
37
+ /* RFC 3986 section 2.3: unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" */
38
+ static bool _is_unreserved(char c) {
39
+ return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '.'
40
+ || c == '_' || c == '~';
41
+ }
42
+
43
+ /* RFC 3986 section 2.2: reserved = gen-delims / sub-delims */
44
+ static bool _is_reserved(char c) {
45
+ return NULL
46
+ != strchr(":/?#[]@" /* gen-delims */
47
+ "!$&'()*+,;=" /* sub-delims */,
48
+ c);
49
+ }
50
+
51
+ static bool _is_hexdig(char c) {
52
+ return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
53
+ }
54
+
55
+ /* _endpoint_chars_are_valid returns true if `endpoint_raw` contains only characters permitted in a URI
56
+ * by RFC 3986. Useful to prevent malformed endpoints being used to construct HTTP requests. */
57
+ static bool _endpoint_chars_are_valid(const char *endpoint_raw, mongocrypt_status_t *status) {
58
+ for (const char *c = endpoint_raw; *c != '\0'; c++) {
59
+ if (_is_unreserved(*c) || _is_reserved(*c)) {
60
+ continue;
61
+ }
62
+ if (*c == '%') {
63
+ /* Returns false if either character is the NUL terminator. */
64
+ if (!_is_hexdig(c[1])) {
65
+ CLIENT_ERR("Invalid percent-encoding in endpoint at offset %zu: 0x%02x",
66
+ (size_t)(c - endpoint_raw) + 1,
67
+ (unsigned char)c[1]);
68
+ return false;
69
+ }
70
+ if (!_is_hexdig(c[2])) {
71
+ CLIENT_ERR("Invalid percent-encoding in endpoint at offset %zu: 0x%02x",
72
+ (size_t)(c - endpoint_raw) + 2,
73
+ (unsigned char)c[2]);
74
+ return false;
75
+ }
76
+ c += 2;
77
+ continue;
78
+ }
79
+ CLIENT_ERR("Invalid character in endpoint at offset %zu: 0x%02x",
80
+ (size_t)(c - endpoint_raw),
81
+ (unsigned char)*c);
82
+ return false;
83
+ }
84
+ return true;
85
+ }
86
+
87
+ /* RFC 3986 section 2.2: sub-delims. Permitted in a reg-name; gen-delims are not. */
88
+ static bool _is_sub_delim(char c) {
89
+ return NULL != strchr("!$&'()*+,;=", c);
90
+ }
91
+
92
+ /* _host_chars_are_valid returns true if `host` is an RFC 3986 section 3.2.2 `reg-name`:
93
+ *
94
+ * reg-name = *( unreserved / pct-encoded / sub-delims )
95
+ *
96
+ * The gen-delims (":/?#[]@") are not permitted. The parse above already ends the host at ':', '/', and '?',
97
+ * so in practice this rejects '#', '@', and the brackets of an IP-literal.
98
+ *
99
+ * This is defense in depth. `host` reaches the Host header, where CR and LF are the only characters that can
100
+ * alter the message and kms-message rejects them, and `host_and_port` is passed to the consumer as the
101
+ * address to connect to, where a gen-delim yields a name that does not resolve rather than a structural
102
+ * problem. An `IP-literal` (RFC 3986 section 3.2.2) is not supported: the parse splits the host at the first
103
+ * colon with no bracket handling, so "[2001:db8::1]" yielded a host of "[2001" and a port of "db8::1]". */
104
+ static bool _host_chars_are_valid(const char *host, mongocrypt_status_t *status) {
105
+ for (const char *c = host; *c != '\0'; c++) {
106
+ if (_is_unreserved(*c) || _is_sub_delim(*c)) {
107
+ continue;
108
+ }
109
+ if (*c == '%') {
110
+ /* Already validated by _endpoint_chars_are_valid. */
111
+ c += 2;
112
+ continue;
113
+ }
114
+ CLIENT_ERR("Invalid character in endpoint host: 0x%02x", (unsigned char)*c);
115
+ return false;
116
+ }
117
+ return true;
118
+ }
119
+
120
+ /* _port_chars_are_valid returns true if `port` is a non-empty run of digits. */
121
+ static bool _port_chars_are_valid(const char *port, mongocrypt_status_t *status) {
122
+ if (*port == '\0') {
123
+ CLIENT_ERR("Invalid endpoint, expected a port after the colon");
124
+ return false;
125
+ }
126
+ for (const char *c = port; *c != '\0'; c++) {
127
+ if (*c < '0' || *c > '9') {
128
+ CLIENT_ERR("Invalid character in endpoint port: 0x%02x", (unsigned char)*c);
129
+ return false;
130
+ }
131
+ }
132
+ return true;
133
+ }
134
+
37
135
  /* Parses a subset of URIs of the form:
38
136
  * [protocol://][host[:port]][path][?query]
39
137
  */
@@ -61,6 +159,11 @@ _mongocrypt_endpoint_t *_mongocrypt_endpoint_new(const char *endpoint_raw,
61
159
  goto fail;
62
160
  }
63
161
 
162
+ /* Validate before parsing: every field parsed below is a substring of `original`. */
163
+ if (!_endpoint_chars_are_valid(endpoint->original, status)) {
164
+ goto fail;
165
+ }
166
+
64
167
  /* Parse optional protocol. */
65
168
  pos = strstr(endpoint->original, "://");
66
169
  if (pos) {
@@ -92,14 +195,15 @@ _mongocrypt_endpoint_t *_mongocrypt_endpoint_new(const char *endpoint_raw,
92
195
  colon = strstr(pos, ":");
93
196
  qmark = strstr(pos, "?");
94
197
  slash = strstr(pos, "/");
95
- if (colon) {
96
- host_end = colon;
97
- } else if (slash) {
198
+ /* The host ends at the first delimiter. A colon delimits a port only if it
199
+ * precedes the path and query: in "example.com/path:8080" the colon is part
200
+ * of the path, not a port separator. */
201
+ host_end = colon;
202
+ if (slash && (!host_end || slash < host_end)) {
98
203
  host_end = slash;
99
- } else if (qmark) {
204
+ }
205
+ if (qmark && (!host_end || qmark < host_end)) {
100
206
  host_end = qmark;
101
- } else {
102
- host_end = NULL;
103
207
  }
104
208
 
105
209
  if (host_end) {
@@ -112,12 +216,16 @@ _mongocrypt_endpoint_t *_mongocrypt_endpoint_new(const char *endpoint_raw,
112
216
  endpoint->host = bson_strdup(host_start);
113
217
  }
114
218
 
219
+ if (!_host_chars_are_valid(endpoint->host, status)) {
220
+ goto fail;
221
+ }
222
+
115
223
  /* Parse optional port */
116
- if (colon) {
224
+ if (colon && colon == host_end) {
117
225
  prev = colon + 1;
118
- qmark = strstr(pos, "?");
119
- slash = strstr(pos, "/");
120
- if (slash) {
226
+ qmark = strstr(prev, "?");
227
+ slash = strstr(prev, "/");
228
+ if (slash && (!qmark || slash < qmark)) {
121
229
  endpoint->port = bson_strndup(prev, (size_t)(slash - prev));
122
230
  } else if (qmark) {
123
231
  BSON_ASSERT(qmark >= prev);
@@ -125,6 +233,10 @@ _mongocrypt_endpoint_t *_mongocrypt_endpoint_new(const char *endpoint_raw,
125
233
  } else {
126
234
  endpoint->port = bson_strdup(prev);
127
235
  }
236
+
237
+ if (!_port_chars_are_valid(endpoint->port, status)) {
238
+ goto fail;
239
+ }
128
240
  }
129
241
 
130
242
  /* Parse optional path */
@@ -171,18 +171,25 @@ bool _mongocrypt_kek_parse_owned(const bson_t *bson, _mongocrypt_kek_t *kek, mon
171
171
  BSON_ASSERT_PARAM(bson);
172
172
  BSON_ASSERT_PARAM(kek);
173
173
 
174
+ /* Parse into a scratch KEK and only commit to `kek` once the whole
175
+ * definition is valid. Writing the provider tag before the union payload is
176
+ * populated would leave `kek` in a state where the tag and the union
177
+ * disagree, which _mongocrypt_kek_cleanup would act on. */
178
+ _mongocrypt_kek_t parsed;
179
+ memset(&parsed, 0, sizeof(parsed));
180
+
174
181
  if (!_mongocrypt_parse_required_utf8(bson, "provider", &kms_provider, status)) {
175
182
  goto done;
176
183
  }
177
184
 
178
- kek->kmsid = bson_strdup(kms_provider);
185
+ parsed.kmsid = bson_strdup(kms_provider);
179
186
 
180
187
  _mongocrypt_kms_provider_t type;
181
- if (!mc_kmsid_parse(kek->kmsid, &type, &kek->kmsid_name, status)) {
188
+ if (!mc_kmsid_parse(parsed.kmsid, &type, &parsed.kmsid_name, status)) {
182
189
  goto done;
183
190
  }
184
191
 
185
- kek->kms_provider = type;
192
+ parsed.kms_provider = type;
186
193
  switch (type) {
187
194
  default:
188
195
  case MONGOCRYPT_KMS_PROVIDER_NONE: {
@@ -190,7 +197,7 @@ bool _mongocrypt_kek_parse_owned(const bson_t *bson, _mongocrypt_kek_t *kek, mon
190
197
  goto done;
191
198
  }
192
199
  case MONGOCRYPT_KMS_PROVIDER_AWS: {
193
- if (!_mongocrypt_aws_kek_parse(&kek->provider.aws, kek->kmsid, bson, status)) {
200
+ if (!_mongocrypt_aws_kek_parse(&parsed.provider.aws, parsed.kmsid, bson, status)) {
194
201
  goto done;
195
202
  }
196
203
  break;
@@ -202,27 +209,32 @@ bool _mongocrypt_kek_parse_owned(const bson_t *bson, _mongocrypt_kek_t *kek, mon
202
209
  break;
203
210
  }
204
211
  case MONGOCRYPT_KMS_PROVIDER_AZURE: {
205
- if (!_mongocrypt_azure_kek_parse(&kek->provider.azure, kek->kmsid, bson, status)) {
212
+ if (!_mongocrypt_azure_kek_parse(&parsed.provider.azure, parsed.kmsid, bson, status)) {
206
213
  goto done;
207
214
  }
208
215
  break;
209
216
  }
210
217
  case MONGOCRYPT_KMS_PROVIDER_GCP: {
211
- if (!_mongocrypt_gcp_kek_parse(&kek->provider.gcp, kek->kmsid, bson, status)) {
218
+ if (!_mongocrypt_gcp_kek_parse(&parsed.provider.gcp, parsed.kmsid, bson, status)) {
212
219
  goto done;
213
220
  }
214
221
  break;
215
222
  }
216
223
  case MONGOCRYPT_KMS_PROVIDER_KMIP: {
217
- if (!_mongocrypt_kmip_kek_parse(&kek->provider.kmip, kek->kmsid, bson, status)) {
224
+ if (!_mongocrypt_kmip_kek_parse(&parsed.provider.kmip, parsed.kmsid, bson, status)) {
218
225
  goto done;
219
226
  }
220
227
  break;
221
228
  }
222
229
  }
223
230
 
231
+ /* Success: replace any prior contents of `kek` and take ownership. */
232
+ _mongocrypt_kek_cleanup(kek);
233
+ *kek = parsed;
234
+ memset(&parsed, 0, sizeof(parsed));
224
235
  ret = true;
225
236
  done:
237
+ _mongocrypt_kek_cleanup(&parsed);
226
238
  bson_free(kms_provider);
227
239
  return ret;
228
240
  }
@@ -181,6 +181,10 @@ bool _mongocrypt_key_parse_owned(const bson_t *bson, _mongocrypt_key_doc_t *out,
181
181
  }
182
182
 
183
183
  if (0 == strcmp("masterKey", field)) {
184
+ if (has_master_key) {
185
+ CLIENT_ERR("duplicate field 'masterKey' in key document");
186
+ return false;
187
+ }
184
188
  has_master_key = true;
185
189
  if (!_parse_masterkey(&iter, out, status)) {
186
190
  return false;
@@ -15,7 +15,7 @@
15
15
  "keyName": "example keyName",
16
16
  "provider": "azure"
17
17
  },
18
- "expect": "invalid endpoint"
18
+ "expect": "Invalid character in endpoint"
19
19
  },
20
20
  {
21
21
  "input": {
@@ -47,7 +47,7 @@
47
47
  "key": "example arn",
48
48
  "provider": "aws"
49
49
  },
50
- "expect": "invalid endpoint"
50
+ "expect": "Invalid character in endpoint"
51
51
  },
52
52
  {
53
53
  "input": {
@@ -79,7 +79,7 @@
79
79
  "keyRing": "example keyRing",
80
80
  "keyName": "example keyName"
81
81
  },
82
- "expect": "invalid endpoint"
82
+ "expect": "Invalid character in endpoint"
83
83
  },
84
84
  {
85
85
  "input": {
@@ -289,10 +289,74 @@ static void _test_mc_FLE2InsertUpdatePayloadV2_parse_errors(_mongocrypt_tester_t
289
289
  }
290
290
  }
291
291
 
292
+ /* Builds a minimal parseable FLE2InsertUpdatePayloadV2 with a 'v' of `vlen` bytes.
293
+ * 'v' is a 16 byte user key UUID followed by (vlen - 16) bytes of ciphertext. */
294
+ static void _make_iup_v2_with_value_len(_mongocrypt_buffer_t *out, uint32_t vlen) {
295
+ BSON_ASSERT(vlen >= UUID_LEN);
296
+ uint8_t token[32] = {0};
297
+ uint8_t uuid[UUID_LEN];
298
+ uint8_t value[128];
299
+
300
+ memset(uuid, 0xAB, sizeof(uuid));
301
+ memset(value, 0x41, sizeof(value));
302
+ BSON_ASSERT(vlen <= sizeof(value));
303
+ memcpy(value, uuid, sizeof(uuid));
304
+
305
+ bson_t payload;
306
+ bson_init(&payload);
307
+ ASSERT(bson_append_binary(&payload, "d", -1, BSON_SUBTYPE_BINARY, token, sizeof(token)));
308
+ ASSERT(bson_append_binary(&payload, "s", -1, BSON_SUBTYPE_BINARY, token, sizeof(token)));
309
+ ASSERT(bson_append_binary(&payload, "p", -1, BSON_SUBTYPE_BINARY, token, sizeof(token)));
310
+ ASSERT(bson_append_binary(&payload, "u", -1, BSON_SUBTYPE_UUID, uuid, sizeof(uuid)));
311
+ ASSERT(bson_append_int32(&payload, "t", -1, (int32_t)BSON_TYPE_UTF8));
312
+ ASSERT(bson_append_binary(&payload, "v", -1, BSON_SUBTYPE_BINARY, value, vlen));
313
+ ASSERT(bson_append_binary(&payload, "e", -1, BSON_SUBTYPE_BINARY, token, sizeof(token)));
314
+ ASSERT(bson_append_binary(&payload, "l", -1, BSON_SUBTYPE_BINARY, token, sizeof(token)));
315
+ ASSERT(bson_append_int64(&payload, "k", -1, 0));
316
+
317
+ _mongocrypt_buffer_init_size(out, 1 + (uint32_t)payload.len);
318
+ out->data[0] = (uint8_t)MC_SUBTYPE_FLE2InsertUpdatePayloadV2;
319
+ memcpy(out->data + 1, bson_get_data(&payload), payload.len);
320
+ bson_destroy(&payload);
321
+ }
322
+
323
+ /* A 'v' short enough that the plaintext length would be zero must be rejected
324
+ * with an error, not crash. */
325
+ static void _test_mc_FLE2InsertUpdatePayloadV2_decrypt_short_ciphertext(_mongocrypt_tester_t *tester) {
326
+ mongocrypt_t *crypt = _mongocrypt_tester_mongocrypt(TESTER_MONGOCRYPT_DEFAULT);
327
+ _mongocrypt_buffer_t user_key;
328
+ _mongocrypt_buffer_init_size(&user_key, MONGOCRYPT_KEY_LEN);
329
+ memset(user_key.data, 0xCD, user_key.len);
330
+
331
+ /* FLE2v2AEAD is CBC + HMAC-SHA256: ciphertext must be at least
332
+ * IV (16) + one block (16) + HMAC (32) bytes. */
333
+ const uint32_t vlens[] = {UUID_LEN, UUID_LEN + 1, UUID_LEN + 48, UUID_LEN + 63};
334
+ for (size_t i = 0; i < sizeof(vlens) / sizeof(vlens[0]); i++) {
335
+ _mongocrypt_buffer_t input;
336
+ _make_iup_v2_with_value_len(&input, vlens[i]);
337
+
338
+ mc_FLE2InsertUpdatePayloadV2_t iup;
339
+ mc_FLE2InsertUpdatePayloadV2_init(&iup);
340
+ mongocrypt_status_t *status = mongocrypt_status_new();
341
+ ASSERT_OK_STATUS(mc_FLE2InsertUpdatePayloadV2_parse(&iup, &input, status), status);
342
+
343
+ const _mongocrypt_buffer_t *got = mc_FLE2InsertUpdatePayloadV2_decrypt(crypt->crypto, &iup, &user_key, status);
344
+ ASSERT_FAILS_STATUS(got != NULL, status, "input ciphertext too small");
345
+
346
+ mc_FLE2InsertUpdatePayloadV2_cleanup(&iup);
347
+ mongocrypt_status_destroy(status);
348
+ _mongocrypt_buffer_cleanup(&input);
349
+ }
350
+
351
+ _mongocrypt_buffer_cleanup(&user_key);
352
+ mongocrypt_destroy(crypt);
353
+ }
354
+
292
355
  void _mongocrypt_tester_install_fle2_payload_iup_v2(_mongocrypt_tester_t *tester) {
293
356
  INSTALL_TEST(_test_FLE2InsertUpdatePayloadV2_parse);
294
357
  INSTALL_TEST(_test_mc_FLE2InsertUpdatePayloadV2_decrypt);
295
358
  INSTALL_TEST(_test_mc_FLE2InsertUpdatePayloadV2_includes_crypto_params);
296
359
  INSTALL_TEST(_test_mc_FLE2InsertUpdatePayloadV2_parses_crypto_params);
297
360
  INSTALL_TEST(_test_mc_FLE2InsertUpdatePayloadV2_parse_errors);
361
+ INSTALL_TEST(_test_mc_FLE2InsertUpdatePayloadV2_decrypt_short_ciphertext);
298
362
  }
@@ -182,8 +182,75 @@ static void test_FLE2InsertUpdatePayload_parse_errors(_mongocrypt_tester_t *test
182
182
  _mongocrypt_buffer_cleanup(&input_buf);
183
183
  }
184
184
 
185
+ /* Builds a minimal parseable FLE2InsertUpdatePayload with a 'v' of `vlen` bytes.
186
+ * 'v' is a 16 byte user key UUID followed by (vlen - 16) bytes of ciphertext. */
187
+ static void _make_iup_with_value_len(_mongocrypt_buffer_t *out, uint32_t vlen) {
188
+ BSON_ASSERT(vlen >= UUID_LEN);
189
+ uint8_t token[32] = {0};
190
+ uint8_t uuid[UUID_LEN];
191
+ uint8_t value[128];
192
+
193
+ memset(uuid, 0xAB, sizeof(uuid));
194
+ memset(value, 0x41, sizeof(value));
195
+ BSON_ASSERT(vlen <= sizeof(value));
196
+ memcpy(value, uuid, sizeof(uuid));
197
+
198
+ bson_t payload;
199
+ bson_init(&payload);
200
+ ASSERT(bson_append_binary(&payload, "d", -1, BSON_SUBTYPE_BINARY, token, sizeof(token)));
201
+ ASSERT(bson_append_binary(&payload, "s", -1, BSON_SUBTYPE_BINARY, token, sizeof(token)));
202
+ ASSERT(bson_append_binary(&payload, "c", -1, BSON_SUBTYPE_BINARY, token, sizeof(token)));
203
+ ASSERT(bson_append_binary(&payload, "p", -1, BSON_SUBTYPE_BINARY, token, sizeof(token)));
204
+ ASSERT(bson_append_binary(&payload, "u", -1, BSON_SUBTYPE_UUID, uuid, sizeof(uuid)));
205
+ ASSERT(bson_append_int32(&payload, "t", -1, (int32_t)BSON_TYPE_UTF8));
206
+ ASSERT(bson_append_binary(&payload, "v", -1, BSON_SUBTYPE_BINARY, value, vlen));
207
+ ASSERT(bson_append_binary(&payload, "e", -1, BSON_SUBTYPE_BINARY, token, sizeof(token)));
208
+
209
+ _mongocrypt_buffer_init_size(out, 1 + (uint32_t)payload.len);
210
+ out->data[0] = (uint8_t)MC_SUBTYPE_FLE2InsertUpdatePayload;
211
+ memcpy(out->data + 1, bson_get_data(&payload), payload.len);
212
+ bson_destroy(&payload);
213
+ }
214
+
215
+ /* A 'v' short enough that the plaintext length would be zero must be rejected
216
+ * with an error, not crash. */
217
+ static void test_FLE2InsertUpdatePayload_decrypt_short_ciphertext(_mongocrypt_tester_t *tester) {
218
+ if (!_aes_ctr_is_supported_by_os) {
219
+ TEST_PRINTF("Common Crypto with no CTR support detected. Skipping.");
220
+ return;
221
+ }
222
+
223
+ mongocrypt_t *crypt = _mongocrypt_tester_mongocrypt(TESTER_MONGOCRYPT_DEFAULT);
224
+ _mongocrypt_buffer_t user_key;
225
+ _mongocrypt_buffer_init_size(&user_key, MONGOCRYPT_KEY_LEN);
226
+ memset(user_key.data, 0xCD, user_key.len);
227
+
228
+ /* FLE2AEAD is CTR + HMAC-SHA256: ciphertext must exceed IV (16) + HMAC (32). */
229
+ const uint32_t vlens[] = {UUID_LEN, UUID_LEN + 1, UUID_LEN + 47, UUID_LEN + 48};
230
+ for (size_t i = 0; i < sizeof(vlens) / sizeof(vlens[0]); i++) {
231
+ _mongocrypt_buffer_t input;
232
+ _make_iup_with_value_len(&input, vlens[i]);
233
+
234
+ mc_FLE2InsertUpdatePayload_t iup;
235
+ mc_FLE2InsertUpdatePayload_init(&iup);
236
+ mongocrypt_status_t *status = mongocrypt_status_new();
237
+ ASSERT_OK_STATUS(mc_FLE2InsertUpdatePayload_parse(&iup, &input, status), status);
238
+
239
+ const _mongocrypt_buffer_t *got = mc_FLE2InsertUpdatePayload_decrypt(crypt->crypto, &iup, &user_key, status);
240
+ ASSERT_FAILS_STATUS(got != NULL, status, "input ciphertext too small");
241
+
242
+ mc_FLE2InsertUpdatePayload_cleanup(&iup);
243
+ mongocrypt_status_destroy(status);
244
+ _mongocrypt_buffer_cleanup(&input);
245
+ }
246
+
247
+ _mongocrypt_buffer_cleanup(&user_key);
248
+ mongocrypt_destroy(crypt);
249
+ }
250
+
185
251
  void _mongocrypt_tester_install_fle2_payload_iup(_mongocrypt_tester_t *tester) {
186
252
  INSTALL_TEST(test_FLE2InsertUpdatePayload_parse);
187
253
  INSTALL_TEST(test_FLE2InsertUpdatePayload_decrypt);
188
254
  INSTALL_TEST(test_FLE2InsertUpdatePayload_parse_errors);
255
+ INSTALL_TEST(test_FLE2InsertUpdatePayload_decrypt_short_ciphertext);
189
256
  }
@@ -46,6 +46,38 @@ static void _test_explicit_decrypt_init(_mongocrypt_tester_t *tester) {
46
46
  mongocrypt_destroy(crypt);
47
47
  }
48
48
 
49
+ /* Regression test for MONGOCRYPT-966 */
50
+ static void _test_explicit_decrypt_unknown_subtype(_mongocrypt_tester_t *tester) {
51
+ mongocrypt_t *const crypt = _mongocrypt_tester_mongocrypt(TESTER_MONGOCRYPT_DEFAULT);
52
+
53
+ const struct {
54
+ const char *desc;
55
+ const char *base64;
56
+ } cases[] = {
57
+ {"unrecognized subtype 0x08", "CKq7zA=="},
58
+ {"FLE1EncryptionPlaceholder (0x00)", "AA=="},
59
+ {"FLE2EncryptionPlaceholder (0x03)", "Aw=="},
60
+ {"FLE2FindEqualityPayload (0x05)", "BQ=="},
61
+ {"FLE2FindRangePayload (0x0a)", "Cg=="},
62
+ {"FLE2FindTextPayload (0x12)", "Eg=="},
63
+ {"zero-length payload", ""},
64
+ };
65
+
66
+ for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
67
+ mongocrypt_binary_t *const msg =
68
+ TEST_BSON("{ 'v': { '$binary': { 'subType': '06', 'base64': '%s' } } }", cases[i].base64);
69
+
70
+ mongocrypt_ctx_t *const ctx = mongocrypt_ctx_new(crypt);
71
+
72
+ ASSERT_FAILS(mongocrypt_ctx_explicit_decrypt_init(ctx, msg), ctx, "unsupported FLE blob subtype");
73
+ BSON_ASSERT(mongocrypt_ctx_state(ctx) == MONGOCRYPT_CTX_ERROR);
74
+
75
+ mongocrypt_ctx_destroy(ctx);
76
+ }
77
+
78
+ mongocrypt_destroy(crypt);
79
+ }
80
+
49
81
  /* Test individual ctx states. */
50
82
  static void _test_decrypt_init(_mongocrypt_tester_t *tester) {
51
83
  mongocrypt_t *crypt;
@@ -788,28 +820,21 @@ typedef struct {
788
820
  mongocrypt_binary_t *msg;
789
821
  mongocrypt_binary_t *keys_to_feed[3]; // NULL terminated list.
790
822
  mongocrypt_binary_t *expect;
791
- bool expect_ignored;
823
+ bool expect_error;
792
824
  } ed_testcase;
793
825
 
794
826
  static void ed_testcase_run(ed_testcase *tc) {
795
827
  printf(" explicit decrypt test: %s ... begin\n", tc->desc);
796
828
  mongocrypt_t *crypt = _mongocrypt_tester_mongocrypt(TESTER_MONGOCRYPT_DEFAULT);
797
829
  mongocrypt_ctx_t *ctx = mongocrypt_ctx_new(crypt);
798
- ASSERT_OK(mongocrypt_ctx_explicit_decrypt_init(ctx, tc->msg), ctx);
799
830
 
800
- if (tc->expect_ignored) {
801
- ASSERT_STATE_EQUAL(mongocrypt_ctx_state(ctx), MONGOCRYPT_CTX_READY);
802
- {
803
- mongocrypt_binary_t *got = mongocrypt_binary_new();
804
- bool ret = mongocrypt_ctx_finalize(ctx, got);
805
- ASSERT_OK(ret, ctx);
806
- // Expect input is returned unchanged.
807
- ASSERT_MONGOCRYPT_BINARY_EQUAL_BSON(tc->msg, got);
808
- mongocrypt_binary_destroy(got);
809
- }
831
+ if (tc->expect_error) {
832
+ ASSERT_FAILS(mongocrypt_ctx_explicit_decrypt_init(ctx, tc->msg), ctx, "unsupported FLE blob subtype");
833
+ ASSERT_STATE_EQUAL(mongocrypt_ctx_state(ctx), MONGOCRYPT_CTX_ERROR);
810
834
  goto cleanup;
811
835
  }
812
836
 
837
+ ASSERT_OK(mongocrypt_ctx_explicit_decrypt_init(ctx, tc->msg), ctx);
813
838
  ASSERT_STATE_EQUAL(mongocrypt_ctx_state(ctx), MONGOCRYPT_CTX_NEED_MONGO_KEYS);
814
839
  {
815
840
  for (size_t i = 0; i < sizeof(tc->keys_to_feed) / sizeof(tc->keys_to_feed[0]); i++) {
@@ -858,12 +883,12 @@ static void _test_explicit_decrypt(_mongocrypt_tester_t *tester) {
858
883
  ed_testcase_run(&tc);
859
884
  }
860
885
 
861
- // FLE1EncryptionPlaceholder is ignored.
886
+ // FLE1EncryptionPlaceholder is recognized but not decryptable: explicit decryption must error.
862
887
  // Payload is returned by query analysis and consumed by libmongocrypt. Payload is not expected to be given to user.
863
888
  {
864
889
  ed_testcase tc = {.desc = "FLE1EncryptionPlaceholder",
865
890
  .msg = TEST_FILE("./test/data/explicit-decrypt/FLE1EncryptionPlaceholder.json"),
866
- .expect_ignored = true};
891
+ .expect_error = true};
867
892
  ed_testcase_run(&tc);
868
893
  }
869
894
 
@@ -878,12 +903,12 @@ static void _test_explicit_decrypt(_mongocrypt_tester_t *tester) {
878
903
  ed_testcase_run(&tc);
879
904
  }
880
905
 
881
- // FLE2EncryptionPlaceholder is ignored.
906
+ // FLE2EncryptionPlaceholder is recognized but not decryptable: explicit decryption must error.
882
907
  // Payload is returned by query analysis and consumed by libmongocrypt. Payload is not expected to be given to user.
883
908
  {
884
909
  ed_testcase tc = {.desc = "FLE2EncryptionPlaceholder",
885
910
  .msg = TEST_FILE("./test/data/explicit-decrypt/FLE2EncryptionPlaceholder.json"),
886
- .expect_ignored = true};
911
+ .expect_error = true};
887
912
  ed_testcase_run(&tc);
888
913
  }
889
914
 
@@ -952,12 +977,12 @@ static void _test_explicit_decrypt(_mongocrypt_tester_t *tester) {
952
977
  ed_testcase_run(&tc);
953
978
  }
954
979
 
955
- // FLE2FindEqualityPayload is ignored.
980
+ // FLE2FindEqualityPayload is recognized but not decryptable: explicit decryption must error.
956
981
  // Payload does not contain encrypted ciphertext (only lookup tokens).
957
982
  {
958
983
  ed_testcase tc = {.desc = "FLE2FindEqualityPayload",
959
984
  .msg = TEST_FILE("./test/data/explicit-decrypt/FLE2FindEqualityPayload.json"),
960
- .expect_ignored = true};
985
+ .expect_error = true};
961
986
  ed_testcase_run(&tc);
962
987
  }
963
988
 
@@ -1030,18 +1055,19 @@ static void _test_explicit_decrypt(_mongocrypt_tester_t *tester) {
1030
1055
  ed_testcase_run(&tc);
1031
1056
  }
1032
1057
 
1033
- // FLE2FindEqualityPayloadV2 is ignored.
1058
+ // FLE2FindEqualityPayloadV2 is recognized but not decryptable: explicit decryption must error.
1034
1059
  // Payload does not contain encrypted ciphertext (only lookup tokens).
1035
1060
  {
1036
1061
  ed_testcase tc = {.desc = "FLE2FindEqualityPayloadV2",
1037
1062
  .msg = TEST_FILE("./test/data/explicit-decrypt/FLE2FindEqualityPayloadV2.json"),
1038
- .expect_ignored = true};
1063
+ .expect_error = true};
1039
1064
  ed_testcase_run(&tc);
1040
1065
  }
1041
1066
  }
1042
1067
 
1043
1068
  void _mongocrypt_tester_install_ctx_decrypt(_mongocrypt_tester_t *tester) {
1044
1069
  INSTALL_TEST(_test_explicit_decrypt_init);
1070
+ INSTALL_TEST(_test_explicit_decrypt_unknown_subtype);
1045
1071
  INSTALL_TEST(_test_decrypt_init);
1046
1072
  INSTALL_TEST(_test_decrypt_need_keys);
1047
1073
  INSTALL_TEST(_test_decrypt_ready);
@@ -66,6 +66,176 @@ static void _test_mongocrypt_endpoint(_mongocrypt_tester_t *tester) {
66
66
  ASSERT_STATUS_CONTAINS(status, "Invalid endpoint, expected dot separator in host, but got: malformed");
67
67
  _mongocrypt_endpoint_destroy(endpoint);
68
68
 
69
+ /* Test a colon in the path does not parse as a port. */
70
+ endpoint = _mongocrypt_endpoint_new("vault.example.com/path:8080", -1, NULL /* opts */, status);
71
+ BSON_ASSERT(endpoint);
72
+ ASSERT_STREQUAL(endpoint->host, "vault.example.com");
73
+ ASSERT_STREQUAL(endpoint->domain, "example.com");
74
+ BSON_ASSERT(!endpoint->port);
75
+ ASSERT_STREQUAL(endpoint->path, "path:8080");
76
+ ASSERT_STREQUAL(endpoint->host_and_port, "vault.example.com");
77
+ BSON_ASSERT(mongocrypt_status_ok(status));
78
+ _mongocrypt_endpoint_destroy(endpoint);
79
+
80
+ /* Test a colon in the query does not parse as a port. */
81
+ endpoint = _mongocrypt_endpoint_new("vault.example.com?query=a:b", -1, NULL /* opts */, status);
82
+ BSON_ASSERT(endpoint);
83
+ ASSERT_STREQUAL(endpoint->host, "vault.example.com");
84
+ BSON_ASSERT(!endpoint->port);
85
+ ASSERT_STREQUAL(endpoint->query, "query=a:b");
86
+ BSON_ASSERT(mongocrypt_status_ok(status));
87
+ _mongocrypt_endpoint_destroy(endpoint);
88
+
89
+ /* Test a port followed by a query, with no path. */
90
+ endpoint = _mongocrypt_endpoint_new("vault.example.com:443?query=value", -1, NULL /* opts */, status);
91
+ BSON_ASSERT(endpoint);
92
+ ASSERT_STREQUAL(endpoint->host, "vault.example.com");
93
+ ASSERT_STREQUAL(endpoint->port, "443");
94
+ ASSERT_STREQUAL(endpoint->query, "query=value");
95
+ ASSERT_STREQUAL(endpoint->host_and_port, "vault.example.com:443");
96
+ BSON_ASSERT(mongocrypt_status_ok(status));
97
+ _mongocrypt_endpoint_destroy(endpoint);
98
+
99
+ /* An endpoint may contain only the characters RFC 3986 permits in a URI. This is defense in depth:
100
+ * each context an endpoint reaches is validated where it is used. */
101
+ const char *const invalid[] = {
102
+ "a\r\nX-Injected: injected.example.com", /* forges a header */
103
+ "a\r\n\r\nGET /evil.example.com", /* ends the header section */
104
+ "example.com\", \"sub\": \"victim@example.com", /* injects a JWT claim */
105
+ "example.com\\x.example.com", /* backslash */
106
+ "example .com", /* space */
107
+ "example.com\x7f", /* DEL */
108
+ "exampl\xc3\xa9.com", /* non-ASCII */
109
+ };
110
+ for (size_t i = 0; i < sizeof(invalid) / sizeof(invalid[0]); i++) {
111
+ endpoint = _mongocrypt_endpoint_new(invalid[i], -1, NULL /* opts */, status);
112
+ BSON_ASSERT(!endpoint);
113
+ ASSERT_STATUS_CONTAINS(status, "Invalid character in endpoint");
114
+ }
115
+
116
+ /* The offending byte is reported by offset and hex value. The endpoint itself is not echoed: it is
117
+ * not yet validated here, and may contain a CR or LF that would forge a line in a consumer's log. */
118
+ endpoint = _mongocrypt_endpoint_new("example .com", -1, NULL /* opts */, status);
119
+ BSON_ASSERT(!endpoint);
120
+ ASSERT_STATUS_CONTAINS(status, "Invalid character in endpoint at offset 7: 0x20");
121
+
122
+ endpoint = _mongocrypt_endpoint_new("a\r\nX-Injected: injected.example.com", -1, NULL /* opts */, status);
123
+ BSON_ASSERT(!endpoint);
124
+ ASSERT_STATUS_CONTAINS(status, "Invalid character in endpoint at offset 1: 0x0d");
125
+
126
+ /* An IPv6 literal is rejected. The parse has no bracket handling, so such an endpoint was never
127
+ * usable. Without a dot it is rejected earlier still, for
128
+ * lacking a subdomain separator. */
129
+ memset(&opts, 0, sizeof(opts));
130
+ opts.allow_empty_subdomain = true;
131
+ endpoint = _mongocrypt_endpoint_new("[2001:db8::1]:443", -1, &opts, status);
132
+ BSON_ASSERT(!endpoint);
133
+ ASSERT_STATUS_CONTAINS(status, "Invalid character in endpoint host");
134
+
135
+ /* One with an embedded dot reaches the host check without the option. */
136
+ endpoint = _mongocrypt_endpoint_new("[::ffff:192.0.2.1]", -1, NULL /* opts */, status);
137
+ BSON_ASSERT(!endpoint);
138
+ ASSERT_STATUS_CONTAINS(status, "Invalid character in endpoint host");
139
+
140
+ /* An IPv4 literal is accepted, as before. */
141
+ endpoint = _mongocrypt_endpoint_new("192.0.2.1:443", -1, NULL /* opts */, status);
142
+ BSON_ASSERT(endpoint);
143
+ ASSERT_STREQUAL(endpoint->host, "192.0.2.1");
144
+ ASSERT_STREQUAL(endpoint->port, "443");
145
+ ASSERT_STREQUAL(endpoint->host_and_port, "192.0.2.1:443");
146
+ BSON_ASSERT(mongocrypt_status_ok(status));
147
+ _mongocrypt_endpoint_destroy(endpoint);
148
+
149
+ /* Percent-encoding must be well formed. */
150
+ endpoint = _mongocrypt_endpoint_new("example.com/%zz", -1, NULL /* opts */, status);
151
+ BSON_ASSERT(!endpoint);
152
+ ASSERT_STATUS_CONTAINS(status, "Invalid percent-encoding in endpoint at offset 13: 0x7a");
153
+
154
+ /* A '%' at the end of the string: the second hex digit is the NUL terminator. */
155
+ endpoint = _mongocrypt_endpoint_new("example.com/%2", -1, NULL /* opts */, status);
156
+ BSON_ASSERT(!endpoint);
157
+ ASSERT_STATUS_CONTAINS(status, "Invalid percent-encoding in endpoint at offset 14: 0x00");
158
+
159
+ endpoint = _mongocrypt_endpoint_new("example.com/a%2Fb", -1, NULL /* opts */, status);
160
+ BSON_ASSERT(endpoint);
161
+ ASSERT_STREQUAL(endpoint->path, "a%2Fb");
162
+ BSON_ASSERT(mongocrypt_status_ok(status));
163
+ _mongocrypt_endpoint_destroy(endpoint);
164
+
165
+ /* A host must be an RFC 3986 `reg-name`. The gen-delims are rejected; the parse already ends the host
166
+ * at ':', '/', and '?', so this covers '#', '@', and the brackets of an IP-literal. */
167
+ const char *const invalid_hosts[] = {
168
+ "vault.example.com@evil.example.com", /* userinfo separator */
169
+ "vault.example.com#frag",
170
+ "[2001:db8::1]",
171
+ };
172
+ memset(&opts, 0, sizeof(opts));
173
+ opts.allow_empty_subdomain = true;
174
+ for (size_t i = 0; i < sizeof(invalid_hosts) / sizeof(invalid_hosts[0]); i++) {
175
+ endpoint = _mongocrypt_endpoint_new(invalid_hosts[i], -1, &opts, status);
176
+ BSON_ASSERT(!endpoint);
177
+ ASSERT_STATUS_CONTAINS(status, "Invalid character in endpoint host");
178
+ }
179
+
180
+ endpoint = _mongocrypt_endpoint_new("vault.example.com@evil.example.com", -1, NULL /* opts */, status);
181
+ BSON_ASSERT(!endpoint);
182
+ ASSERT_STATUS_CONTAINS(status, "Invalid character in endpoint host: 0x40");
183
+
184
+ /* The sub-delims are permitted in a reg-name, so they are permitted here. An '&' reaching the Azure
185
+ * OAuth request body is prevented by percent-encoding it into the scope, not by this check. */
186
+ const char *const valid_hosts[] = {
187
+ "vault.example.com&x",
188
+ "vault.example.com=x",
189
+ "vault.example.com;x",
190
+ "vault.example.com,x",
191
+ "vault.example.com~x",
192
+ "vault.example.com%41",
193
+ };
194
+ for (size_t i = 0; i < sizeof(valid_hosts) / sizeof(valid_hosts[0]); i++) {
195
+ endpoint = _mongocrypt_endpoint_new(valid_hosts[i], -1, NULL /* opts */, status);
196
+ BSON_ASSERT(endpoint);
197
+ ASSERT_STREQUAL(endpoint->host, valid_hosts[i]);
198
+ BSON_ASSERT(mongocrypt_status_ok(status));
199
+ _mongocrypt_endpoint_destroy(endpoint);
200
+ }
201
+
202
+ /* The host check does not apply to the path or query, which may contain reserved characters. */
203
+ endpoint = _mongocrypt_endpoint_new("vault.example.com/p&q?a=b&c=d", -1, NULL /* opts */, status);
204
+ BSON_ASSERT(endpoint);
205
+ ASSERT_STREQUAL(endpoint->host, "vault.example.com");
206
+ ASSERT_STREQUAL(endpoint->path, "p&q");
207
+ ASSERT_STREQUAL(endpoint->query, "a=b&c=d");
208
+ BSON_ASSERT(mongocrypt_status_ok(status));
209
+ _mongocrypt_endpoint_destroy(endpoint);
210
+
211
+ /* A port may contain only digits. The port is appended to host_and_port, which is used as the Host
212
+ * header and as the address to connect to. */
213
+ const char *const invalid_ports[] = {
214
+ "vault.example.com:44&3",
215
+ "vault.example.com:443x",
216
+ "vault.example.com:-1",
217
+ "vault.example.com:44.3",
218
+ };
219
+ for (size_t i = 0; i < sizeof(invalid_ports) / sizeof(invalid_ports[0]); i++) {
220
+ endpoint = _mongocrypt_endpoint_new(invalid_ports[i], -1, NULL /* opts */, status);
221
+ BSON_ASSERT(!endpoint);
222
+ ASSERT_STATUS_CONTAINS(status, "Invalid character in endpoint port");
223
+ }
224
+
225
+ /* The offset is relative to the start of the port. */
226
+ endpoint = _mongocrypt_endpoint_new("vault.example.com:44&3", -1, NULL /* opts */, status);
227
+ BSON_ASSERT(!endpoint);
228
+ ASSERT_STATUS_CONTAINS(status, "Invalid character in endpoint port: 0x26");
229
+
230
+ /* A colon with no port is rejected. */
231
+ endpoint = _mongocrypt_endpoint_new("vault.example.com:", -1, NULL /* opts */, status);
232
+ BSON_ASSERT(!endpoint);
233
+ ASSERT_STATUS_CONTAINS(status, "Invalid endpoint, expected a port after the colon");
234
+
235
+ endpoint = _mongocrypt_endpoint_new("vault.example.com:/path", -1, NULL /* opts */, status);
236
+ BSON_ASSERT(!endpoint);
237
+ ASSERT_STATUS_CONTAINS(status, "Invalid endpoint, expected a port after the colon");
238
+
69
239
  /* A host without a dot separator is valid if the "allow_empty_subdomain"
70
240
  * option is true. */
71
241
  memset(&opts, 0, sizeof(opts));
@@ -96,6 +96,42 @@ static void test_mongocrypt_kek_parsing(_mongocrypt_tester_t *tester) {
96
96
  bson_destroy(&test_file);
97
97
  }
98
98
 
99
+ /* A failed parse must leave the KEK exactly as it was. Committing the new
100
+ * provider tag before the provider-specific parse repopulates the union would
101
+ * leave the previous provider's pointers readable under the new tag, which
102
+ * _mongocrypt_kek_cleanup would then free as the wrong type. See
103
+ * MONGOCRYPT-964. */
104
+ static void test_mongocrypt_kek_parse_failure_leaves_kek_intact(_mongocrypt_tester_t *tester) {
105
+ mongocrypt_status_t *const status = mongocrypt_status_new();
106
+ _mongocrypt_kek_t kek;
107
+
108
+ memset(&kek, 0, sizeof(_mongocrypt_kek_t));
109
+
110
+ ASSERT_OK_STATUS(_mongocrypt_kek_parse_owned(TMP_BSON("{'provider': 'gcp', 'projectId': 'project', "
111
+ "'location': 'global', 'keyRing': 'ring', "
112
+ "'keyName': 'name'}"),
113
+ &kek,
114
+ status),
115
+ status);
116
+
117
+ /* An AWS definition missing the required 'region' aborts mid-parse, after
118
+ * the provider tag would have been switched. */
119
+ ASSERT_FAILS_STATUS(_mongocrypt_kek_parse_owned(TMP_BSON("{'provider': 'aws', 'key': 'cmk-string'}"), &kek, status),
120
+ status,
121
+ "expected UTF-8 region");
122
+
123
+ /* The KEK still describes the GCP key it was parsed from. */
124
+ ASSERT_CMPINT(kek.kms_provider, ==, MONGOCRYPT_KMS_PROVIDER_GCP);
125
+ ASSERT_STREQUAL(kek.kmsid, "gcp");
126
+ ASSERT_STREQUAL(kek.provider.gcp.key_ring, "ring");
127
+ ASSERT_STREQUAL(kek.provider.gcp.key_name, "name");
128
+
129
+ /* Cleanup frees the GCP fields, matching the tag. */
130
+ _mongocrypt_kek_cleanup(&kek);
131
+ mongocrypt_status_destroy(status);
132
+ }
133
+
99
134
  void _mongocrypt_tester_install_kek(_mongocrypt_tester_t *tester) {
100
135
  INSTALL_TEST(test_mongocrypt_kek_parsing);
136
+ INSTALL_TEST(test_mongocrypt_kek_parse_failure_leaves_kek_intact);
101
137
  }
@@ -182,6 +182,31 @@ static void test_mongocrypt_key_parsing(_mongocrypt_tester_t *tester) {
182
182
  bson_destroy(&key_bson);
183
183
  }
184
184
 
185
+ /* Regression test for MONGOCRYPT-964. */
186
+ static void test_mongocrypt_key_parsing_duplicate_master_key(_mongocrypt_tester_t *tester) {
187
+ bson_t key_bson = BSON_INITIALIZER;
188
+ mongocrypt_status_t *const status = mongocrypt_status_new();
189
+
190
+ /* Two identical masterKey fields. */
191
+ _recreate_and_reset(tester, &key_bson, status, "masterKey", NULL);
192
+ bson_concat(&key_bson, TMP_BSON("{'masterKey': { 'provider': 'local' }}"));
193
+ bson_concat(&key_bson, TMP_BSON("{'masterKey': { 'provider': 'local' }}"));
194
+ _parse_fails(&key_bson, status, "duplicate field 'masterKey'");
195
+
196
+ /* A GCP masterKey followed by an AWS masterKey missing 'region'. The
197
+ * aborted second parse leaves the GCP 'keyRing' string in the union slot
198
+ * shared with aws.endpoint, which cleanup then frees as an endpoint. */
199
+ _recreate_and_reset(tester, &key_bson, status, "masterKey", NULL);
200
+ bson_concat(&key_bson,
201
+ TMP_BSON("{'masterKey': { 'provider': 'gcp', 'projectId': 'project', 'location': "
202
+ "'global', 'keyRing': 'ring', 'keyName': 'name' }}"));
203
+ bson_concat(&key_bson, TMP_BSON("{'masterKey': { 'provider': 'aws', 'key': 'cmk-string' }}"));
204
+ _parse_fails(&key_bson, status, "duplicate field 'masterKey'");
205
+
206
+ mongocrypt_status_destroy(status);
207
+ bson_destroy(&key_bson);
208
+ }
209
+
185
210
  static void test_mongocrypt_key_alt_name_from_iter(_mongocrypt_tester_t *tester) {
186
211
  mongocrypt_status_t *status;
187
212
  bson_iter_t iter;
@@ -230,5 +255,6 @@ static void test_mongocrypt_key_alt_name_from_iter(_mongocrypt_tester_t *tester)
230
255
 
231
256
  void _mongocrypt_tester_install_key(_mongocrypt_tester_t *tester) {
232
257
  INSTALL_TEST(test_mongocrypt_key_parsing);
258
+ INSTALL_TEST(test_mongocrypt_key_parsing_duplicate_master_key);
233
259
  INSTALL_TEST(test_mongocrypt_key_alt_name_from_iter);
234
260
  }
@@ -1,5 +1,5 @@
1
1
  module LibmongocryptHelper
2
- VERSION = '1.20.3.0.1001'
2
+ VERSION = '1.20.5.0.1001'
3
3
 
4
- LIBMONGOCRYPT_VERSION = '1.20.3'
4
+ LIBMONGOCRYPT_VERSION = '1.20.5'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: libmongocrypt-helper
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.20.3.0.1001
4
+ version: 1.20.5.0.1001
5
5
  platform: ruby
6
6
  authors:
7
7
  - The MongoDB Ruby Team
@@ -1250,7 +1250,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
1250
1250
  - !ruby/object:Gem::Version
1251
1251
  version: '0'
1252
1252
  requirements: []
1253
- rubygems_version: 4.0.19
1253
+ rubygems_version: 4.0.23
1254
1254
  specification_version: 4
1255
1255
  summary: libmongocrypt convenience package
1256
1256
  test_files: []