punycode: update to use WPACKET instead of using custom range checking

Add test for `.' overflows, remove the output size argument from
ossl_a2ulabel() since it was never used and greatly complicated the code.
Convert ossl_a2ulabel() to use WPACKET for building the output string.
Update the documentation to match the new definition of ossl_a2ulabel().

x509: let punycode handle the '\0' string termination.  Saves a memset(3)
and some size fiddling.  Also update to deal with the modified parameters.

Reviewed-by: Tomas Mraz <tomas@openssl.org>
Reviewed-by: Dmitry Belyavskiy <beldmit@gmail.com>
(Merged from https://github.com/openssl/openssl/pull/19591)
This commit is contained in:
Pauli 2022-11-03 11:55:13 +11:00
parent 373d901280
commit 905ba92439
5 changed files with 96 additions and 62 deletions

View File

@ -12,6 +12,7 @@
#include <openssl/e_os2.h>
#include "crypto/punycode.h"
#include "internal/common.h" /* for HAS_PREFIX */
#include "internal/packet.h" /* for WPACKET */
static const unsigned int base = 36;
static const unsigned int tmin = 1;
@ -239,12 +240,12 @@ static int codepoint2utf8(unsigned char *out, unsigned long utf)
/*-
* Return values:
* 1 - ok, *outlen contains valid buf length
* 0 - ok but buf was too short, *outlen contains valid buf length
* -1 - bad string passed
* 1 - ok
* 0 - ok but buf was too short
* -1 - bad string passed or other error
*/
int ossl_a2ulabel(const char *in, char *out, size_t *outlen)
int ossl_a2ulabel(const char *in, char *out, size_t outlen)
{
/*-
* Domain name has some parts consisting of ASCII chars joined with dot.
@ -252,63 +253,61 @@ int ossl_a2ulabel(const char *in, char *out, size_t *outlen)
* If it does not start with xn--, it becomes U-label as is.
* Otherwise we try to decode it.
*/
char *outptr = out;
const char *inptr = in;
size_t size = 0, maxsize;
int result = 1;
unsigned int i, j;
unsigned int i;
unsigned int buf[LABEL_BUF_SIZE]; /* It's a hostname */
WPACKET pkt;
if (out == NULL) {
result = 0;
maxsize = 0;
} else {
maxsize = *outlen;
}
/* Internal API, so should not fail */
if (!ossl_assert(out != NULL))
return -1;
#define PUSHC(c) \
do \
if (size++ < maxsize) \
*outptr++ = c; \
else \
result = 0; \
while (0)
if (!WPACKET_init_static_len(&pkt, (unsigned char *)out, outlen, 0))
return -1;
while (1) {
char *tmpptr = strchr(inptr, '.');
size_t delta = tmpptr != NULL ? (size_t)(tmpptr - inptr) : strlen(inptr);
if (!HAS_PREFIX(inptr, "xn--")) {
for (i = 0; i < delta + 1; i++)
PUSHC(inptr[i]);
if (!WPACKET_memcpy(&pkt, inptr, delta))
result = 0;
} else {
unsigned int bufsize = LABEL_BUF_SIZE;
if (ossl_punycode_decode(inptr + 4, delta - 4, buf, &bufsize) <= 0)
return -1;
if (ossl_punycode_decode(inptr + 4, delta - 4, buf, &bufsize) <= 0) {
result = -1;
goto end;
}
for (i = 0; i < bufsize; i++) {
unsigned char seed[6];
size_t utfsize = codepoint2utf8(seed, buf[i]);
if (utfsize == 0)
return -1;
if (utfsize == 0) {
result = -1;
goto end;
}
for (j = 0; j < utfsize; j++)
PUSHC(seed[j]);
if (!WPACKET_memcpy(&pkt, seed, utfsize))
result = 0;
}
PUSHC(tmpptr != NULL ? '.' : '\0');
}
if (tmpptr == NULL)
break;
if (!WPACKET_put_bytes_u8(&pkt, '.'))
result = 0;
inptr = tmpptr + 1;
}
#undef PUSHC
*outlen = size;
if (!WPACKET_put_bytes_u8(&pkt, '\0'))
result = 0;
end:
WPACKET_cleanup(&pkt);
return result;
}
@ -325,7 +324,7 @@ int ossl_a2ucompare(const char *a, const char *u)
char a_ulabel[LABEL_BUF_SIZE + 1];
size_t a_size = sizeof(a_ulabel);
if (ossl_a2ulabel(a, a_ulabel, &a_size) <= 0)
if (ossl_a2ulabel(a, a_ulabel, a_size) <= 0)
return -1;
return strcmp(a_ulabel, u) != 0;

View File

@ -632,7 +632,7 @@ static int nc_email_eai(ASN1_TYPE *emltype, ASN1_IA5STRING *base)
const char *emlptr;
const char *emlat;
char ulabel[256];
size_t size = sizeof(ulabel) - 1;
size_t size = sizeof(ulabel);
int ret = X509_V_OK;
size_t emlhostlen;
@ -659,18 +659,16 @@ static int nc_email_eai(ASN1_TYPE *emltype, ASN1_IA5STRING *base)
goto end;
}
memset(ulabel, 0, sizeof(ulabel));
/* Special case: initial '.' is RHS match */
if (*baseptr == '.') {
ulabel[0] = '.';
size -= 1;
if (ossl_a2ulabel(baseptr, ulabel + 1, &size) <= 0) {
if (ossl_a2ulabel(baseptr, ulabel + 1, size - 1) <= 0) {
ret = X509_V_ERR_UNSPECIFIED;
goto end;
}
if ((size_t)eml->length > strlen(ulabel)) {
emlptr += eml->length - (strlen(ulabel));
emlptr += eml->length - strlen(ulabel);
/* X509_V_OK */
if (ia5ncasecmp(ulabel, emlptr, strlen(ulabel)) == 0)
goto end;
@ -679,7 +677,7 @@ static int nc_email_eai(ASN1_TYPE *emltype, ASN1_IA5STRING *base)
goto end;
}
if (ossl_a2ulabel(baseptr, ulabel, &size) <= 0) {
if (ossl_a2ulabel(baseptr, ulabel, size) <= 0) {
ret = X509_V_ERR_UNSPECIFIED;
goto end;
}

View File

@ -12,7 +12,7 @@ ossl_punycode_decode, ossl_a2ulabel, ossl_a2ucompare
int ossl_punycode_decode(const char *pEncoded, const size_t enc_len,
unsigned int *pDecoded, unsigned int *pout_length);
int ossl_a2ulabel(const char *in, char *out, size_t *outlen);
int ossl_a2ulabel(const char *in, char *out, size_t outlen);
int ossl_a2ucompare(const char *a, const char *u);
@ -23,7 +23,7 @@ representation of hostnames in ASCII-only format. Some specifications,
such as RFC 8398, require comparison of hostnames encoded in UTF-8 charset.
ossl_a2ulabel() decodes NUL-terminated hostname from PUNYCODE to UTF-8,
using a provided buffer for output.
using a provided buffer for output. The output buffer is NUL-terminated.
ossl_a2ucompare() accepts two NUL-terminated hostnames, decodes the 1st
from PUNYCODE to UTF-8 and compares it with the 2nd one as is.
@ -33,12 +33,11 @@ a hostname, with stripped PUNYCODE marker I<xn-->.
=head1 RETURN VALUES
ossl_a2ulabel() returns 1 on success, 0 on not enough buf passed,
-1 on invalid PUNYCODE string passed. When valid string is provided, it sets the
I<*outlen> to the length of required buffer to perform correct decoding.
ossl_a2ulabel() returns 1 on success, 0 if the output buffer is too small and
-1 if an invalid PUNYCODE string is passed or another error occurs.
ossl_a2ucompare() returns 1 on non-equal strings, 0 on equal strings,
-1 when invalid PUNYCODE string passed.
-1 when an invalid PUNYCODE string is passed or another error occurs.
ossl_punycode_decode() returns 1 on success, 0 on error. On success,
*pout_length contains the number of codepoints decoded.

View File

@ -18,7 +18,8 @@ int ossl_punycode_decode (
unsigned int *pout_length
);
int ossl_a2ulabel(const char *in, char *out, size_t *outlen);
int ossl_a2ulabel(const char *in, char *out, size_t outlen);
int ossl_a2ucompare(const char *a, const char *u);
#endif

View File

@ -12,6 +12,7 @@
#include "crypto/punycode.h"
#include "internal/nelem.h"
#include "internal/packet.h"
#include "testutil.h"
@ -166,29 +167,17 @@ static int test_punycode(int n)
static int test_a2ulabel(void)
{
char out[50];
size_t outlen;
/*
* Test that no buffer correctly returns the true length.
* The punycode being passed in and parsed is malformed but we're not
* verifying that behaviour here.
*/
if (!TEST_int_eq(ossl_a2ulabel("xn--a.b.c", NULL, &outlen), 0)
|| !TEST_size_t_eq(outlen, 7)
|| !TEST_int_eq(ossl_a2ulabel("xn--a.b.c", out, &outlen), 1))
return 0;
/* Test that a short input length returns the true length */
outlen = 1;
if (!TEST_int_eq(ossl_a2ulabel("xn--a.b.c", out, &outlen), 0)
|| !TEST_size_t_eq(outlen, 7)
|| !TEST_int_eq(ossl_a2ulabel("xn--a.b.c", out, &outlen), 1)
|| !TEST_str_eq(out,"\xc2\x80.b.c"))
if (!TEST_int_eq(ossl_a2ulabel("xn--a.b.c", out, 1), 0)
|| !TEST_int_eq(ossl_a2ulabel("xn--a.b.c", out, 7), 1))
return 0;
/* Test for an off by one on the buffer size works */
outlen = 6;
if (!TEST_int_eq(ossl_a2ulabel("xn--a.b.c", out, &outlen), 0)
|| !TEST_size_t_eq(outlen, 7)
|| !TEST_int_eq(ossl_a2ulabel("xn--a.b.c", out, &outlen), 1)
if (!TEST_int_eq(ossl_a2ulabel("xn--a.b.c", out, 6), 0)
|| !TEST_int_eq(ossl_a2ulabel("xn--a.b.c", out, 7), 1)
|| !TEST_str_eq(out,"\xc2\x80.b.c"))
return 0;
return 1;
@ -211,9 +200,57 @@ static int test_puny_overrun(void)
return 1;
}
static int test_dotted_overflow(void)
{
static const char string[] = "a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a";
const size_t num_reps = OSSL_NELEM(string) / 2;
WPACKET p;
BUF_MEM *in;
char *out = NULL;
size_t i;
int res = 0;
/* Create out input punycode string */
if (!TEST_ptr(in = BUF_MEM_new()))
return 0;
if (!TEST_true(WPACKET_init_len(&p, in, 0))) {
BUF_MEM_free(in);
return 0;
}
for (i = 0; i < num_reps; i++) {
if (i > 1 && !TEST_true(WPACKET_put_bytes_u8(&p, '.')))
goto err;
if (!TEST_true(WPACKET_memcpy(&p, "xn--a", sizeof("xn--a") - 1)))
goto err;
}
if (!TEST_true(WPACKET_put_bytes_u8(&p, '\0')))
goto err;
if (!TEST_ptr(out = OPENSSL_malloc(in->length)))
goto err;
/* Test the decode into an undersized buffer */
memset(out, 0x7f, in->length - 1);
if (!TEST_int_le(ossl_a2ulabel(in->data, out, num_reps), 0)
|| !TEST_int_eq(out[num_reps], 0x7f))
goto err;
/* Test the decode works into a full size buffer */
if (!TEST_int_gt(ossl_a2ulabel(in->data, out, in->length), 0)
|| !TEST_size_t_eq(strlen(out), num_reps * 3))
goto err;
res = 1;
err:
WPACKET_cleanup(&p);
BUF_MEM_free(in);
OPENSSL_free(out);
return res;
}
int setup_tests(void)
{
ADD_ALL_TESTS(test_punycode, OSSL_NELEM(puny_cases));
ADD_TEST(test_dotted_overflow);
ADD_TEST(test_a2ulabel);
ADD_TEST(test_puny_overrun);
return 1;