From 346b9622a0eafb10daa2a1c5c39d56c84d15056d Mon Sep 17 00:00:00 2001 From: Jeroen Koekkoek Date: Thu, 15 Aug 2024 17:11:36 +0200 Subject: [PATCH] Add semantic checks for DS and ZONEMD digests Fixes NLnetLabs/nsd#205. --- src/generic/types.h | 90 ++++++++++++++++++++-- tests/CMakeLists.txt | 2 +- tests/semantics.c | 179 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 264 insertions(+), 7 deletions(-) create mode 100644 tests/semantics.c diff --git a/src/generic/types.h b/src/generic/types.h index 4576569..1f2708a 100644 --- a/src/generic/types.h +++ b/src/generic/types.h @@ -1291,8 +1291,26 @@ static int32_t check_ds_rr( (r = check(&c, check_int8(parser, type, &f[2], o+c, n-c)))) return r; - // FIXME: can implement checking for digest length based on algorithm here. - // e.g. SHA-1 digest is 20 bytes, see RFC3658 section 2.4 + const uint8_t digest_algorithm = parser->rdata->octets[3]; + + if ((digest_algorithm & 0x7) == digest_algorithm) { + // https://www.iana.org/assignments/ds-rr-types + static const uint8_t digest_sizes[8] = { + 0, // 0: Reserved + 20, // 1: SHA-1 + 32, // 2: SHA-256 + 32, // 3: GOST R 34.11-94 + 48, // 4: SHA-384 + 48, // 5: GOST R 34.10-2012 + 48, // 6: SM3 + 0 // 7: Unassigned + }; + + const uint8_t digest_size = digest_sizes[ digest_algorithm ]; + + if (digest_size && n - 4 != digest_size) + SEMANTIC_ERROR(parser, "Invalid digest in %s", NAME(type)); + } if (c >= n) SYNTAX_ERROR(parser, "Invalid %s", NAME(type)); @@ -1322,6 +1340,28 @@ static int32_t parse_ds_rdata( if ((code = parse_base16_sequence(parser, type, &fields[3], rdata, token)) < 0) return code; + const uint8_t digest_algorithm = parser->rdata->octets[3]; + + if ((digest_algorithm & 0x7) == digest_algorithm) { + // https://www.iana.org/assignments/ds-rr-types + static const uint8_t digest_sizes[8] = { + 0, // 0: Reserved + 20, // 1: SHA-1 + 32, // 2: SHA-256 + 32, // 3: GOST R 34.11-94 + 48, // 4: SHA-384 + 48, // 5: GOST R 34.10-2012 + 48, // 6: SM3 + 0 // 7: Unassigned + }; + + const uint8_t digest_size = digest_sizes[ digest_algorithm ]; + size_t length = (uintptr_t)rdata->octets - (uintptr_t)parser->rdata->octets; + + if (digest_size && length - 4 != digest_size) + SEMANTIC_ERROR(parser, "Invalid digest in %s", NAME(type)); + } + return accept_rr(parser, type, rdata); } @@ -2005,10 +2045,32 @@ nonnull_all static int32_t check_zonemd_rr( parser_t *parser, const type_info_t *type, const rdata_t *rdata) { - // FIXME: RDATA contains digests, do extra checks? - assert(rdata->octets >= parser->rdata->octets); - if ((uintptr_t)rdata->octets - (uintptr_t)parser->rdata->octets < 6) - SYNTAX_ERROR(parser, "Invalid %s", NAME(type)); + int32_t r; + size_t c = 0; + const size_t n = (uintptr_t)rdata->octets - (uintptr_t)parser->rdata->octets; + const uint8_t *o = parser->rdata->octets; + const rdata_info_t *f = type->rdata.fields; + + if ((r = check(&c, check_int32(parser, type, &f[0], o, n))) || + (r = check(&c, check_int8(parser, type, &f[1], o+c, n-c))) || + (r = check(&c, check_int8(parser, type, &f[2], o+c, n-c)))) + return r; + + const uint8_t digest_algorithm = parser->rdata->octets[5]; + if ((digest_algorithm & 0x3) == digest_algorithm) { + // https://www.iana.org/assignments/dns-parameters#zonemd-hash-algorithms + static const uint8_t digest_sizes[4] = { + 0, // 0: Reserved + 48, // 1: SHA-384 + 64, // 2: SHA-512 + 0 // 3: Unassigned + }; + + const uint8_t digest_size = digest_sizes[ digest_algorithm ]; + if (digest_size && n - 6 != digest_size) + SEMANTIC_ERROR(parser, "Invalid digest in %s", NAME(type)); + } + return accept_rr(parser, type, rdata); } @@ -2035,6 +2097,22 @@ static int32_t parse_zonemd_rdata( if ((code = parse_base16_sequence(parser, type, &fields[3], rdata, token)) < 0) return code; + const uint8_t digest_algorithm = parser->rdata->octets[5]; + if ((digest_algorithm & 0x3) == digest_algorithm) { + // https://www.iana.org/assignments/dns-parameters#zonemd-hash-algorithms + static const uint8_t digest_sizes[4] = { + 0, // 0: Reserved + 48, // 1: SHA-384 + 64, // 2: SHA-512 + 0 // 3: Unassigned + }; + + const uint8_t digest_size = digest_sizes[ digest_algorithm ]; + size_t length = (uintptr_t)rdata->octets - (uintptr_t)parser->rdata->octets; + if (digest_size && length - 6 != digest_size) + SEMANTIC_ERROR(parser, "Invalid digest in %s", NAME(type)); + } + return accept_rr(parser, type, rdata); } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 77112af..66eacd6 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -9,7 +9,7 @@ if(HAVE_HASWELL) set_source_files_properties(haswell/bits.c PROPERTIES COMPILE_FLAGS "-march=haswell") endif() -cmocka_add_tests(zone-tests types.c include.c ip4.c time.c base32.c svcb.c syntax.c eui.c bounds.c bits.c) +cmocka_add_tests(zone-tests types.c include.c ip4.c time.c base32.c svcb.c syntax.c semantics.c eui.c bounds.c bits.c) set(xbounds ${CMAKE_CURRENT_SOURCE_DIR}/zones/xbounds.zone) set(xbounds_c "${CMAKE_CURRENT_BINARY_DIR}/xbounds.c") diff --git a/tests/semantics.c b/tests/semantics.c new file mode 100644 index 0000000..e367870 --- /dev/null +++ b/tests/semantics.c @@ -0,0 +1,179 @@ +/* + * syntax.c -- presentation format syntax test cases + * + * Copyright (c) 2023, NLnet Labs. All rights reserved. + * + * SPDX-License-Identifier: BSD-3-Clause + * + */ +#include +#include +#include +#include +#include +#include +#include +#if !_WIN32 +#include +#endif + +#include "zone.h" +#include "diagnostic.h" +#include "tools.h" + +static int32_t digest_test_accept_rr( + zone_parser_t *parser, + const zone_name_t *owner, + uint16_t type, + uint16_t class, + uint32_t ttl, + uint16_t rdlength, + const uint8_t *rdata, + void *user_data) +{ + (void)parser; + (void)owner; + (void)type; + (void)class; + (void)ttl; + (void)rdlength; + (void)rdata; + (void)user_data; + return 0; +} + +static int32_t parse_digest(const char *input) +{ + const uint8_t origin[] = { 0 }; + zone_parser_t parser; + zone_name_buffer_t name; + zone_rdata_buffer_t rdata; + zone_buffers_t buffers = { 1, &name, &rdata }; + zone_options_t options; + + memset(&options, 0, sizeof(options)); + options.accept.callback = digest_test_accept_rr; + options.origin.octets = origin; + options.origin.length = sizeof(origin); + options.default_ttl = 3600; + options.default_class = 1; + + fprintf(stderr, "INPUT: \"%s\"\n", input); + return zone_parse_string( + &parser, &options, &buffers, input, strlen(input), NULL); +} + +/*!cmocka */ +void ds_digest_lengths(void **state) +{ + static const char fmt[] = + "dskey.example.com. 86400 IN DS 60485 5 %c ( %.*s )"; + static const char hex_fmt[] = + "dskey.example.com. 86400 CLASS1 TYPE43 \\# %d EC45 05 0%c ( %.*s )"; + static const char hex[] = + "0123456789abcdef0123456789abcdef" + "0123456789abcdef0123456789abcdef" + "0123456789abcdef0123456789abcdef" + "0123456789abcdef0123456789abcdef"; + + static const struct { + int algorithm; + int digest_length; + int32_t code; + } tests[] = { + // 0: Reserved + { 0, 10, ZONE_SUCCESS }, + // 1: SHA-1 + { 1, 20, ZONE_SUCCESS }, + { 1, 19, ZONE_SEMANTIC_ERROR }, + { 1, 21, ZONE_SEMANTIC_ERROR }, + // 2: SHA-256 + { 2, 32, ZONE_SUCCESS }, + { 2, 31, ZONE_SEMANTIC_ERROR }, + { 2, 33, ZONE_SEMANTIC_ERROR }, + // 3: GOST R 34.11-94 + { 3, 32, ZONE_SUCCESS }, + { 3, 31, ZONE_SEMANTIC_ERROR }, + { 3, 33, ZONE_SEMANTIC_ERROR }, + // 4: SHA-384 + { 4, 48, ZONE_SUCCESS }, + { 4, 47, ZONE_SEMANTIC_ERROR }, + { 4, 49, ZONE_SEMANTIC_ERROR }, + // 5: GOST R 34.10-2012 + { 5, 48, ZONE_SUCCESS }, + { 5, 47, ZONE_SEMANTIC_ERROR }, + { 5, 49, ZONE_SEMANTIC_ERROR }, + // 6: SM3 + { 6, 48, ZONE_SUCCESS }, + { 6, 47, ZONE_SEMANTIC_ERROR }, + { 6, 49, ZONE_SEMANTIC_ERROR } + }; + + (void)state; + + int32_t code; + for (size_t i=0, n = sizeof(tests)/sizeof(tests[0]); i < n; i++) { + char buf[512]; + const char algo = tests[i].algorithm; + const int len = tests[i].digest_length; + + snprintf(buf, sizeof(buf), fmt, algo + 0x30, len * 2, hex); + code = parse_digest(buf); + assert_int_equal(code, tests[i].code); + + snprintf(buf, sizeof(buf), hex_fmt, 4 + len, algo + 0x30, len * 2, hex); + code = parse_digest(buf); + assert_int_equal(code, tests[i].code); + } +} + +/*!cmocka */ +void zonemd_digest_lengths(void **state) +{ + static const char fmt[] = + "example.com. 86400 IN ZONEMD 2018031500 1 %c ( %.*s )"; + static const char hex_fmt[] = + "example.com. 86400 CLASS1 TYPE63 \\# %d 7848B78C 01 0%c ( %.*s )"; + static const char hex[] = + "0123456789abcdef0123456789abcdef" + "0123456789abcdef0123456789abcdef" + "0123456789abcdef0123456789abcdef" + "0123456789abcdef0123456789abcdef" + "0123456789abcdef0123456789abcdef" + "0123456789abcdef0123456789abcdef"; + + static const struct { + int algorithm; + int digest_length; + int32_t code; + } tests[] = { + // 0: Reserved + { 0, 10, ZONE_SUCCESS }, + // 1: SHA-384 + { 1, 48, ZONE_SUCCESS }, + { 1, 47, ZONE_SEMANTIC_ERROR }, + { 1, 49, ZONE_SEMANTIC_ERROR }, + // 2: SHA-512 + { 2, 64, ZONE_SUCCESS }, + { 2, 63, ZONE_SEMANTIC_ERROR }, + { 2, 65, ZONE_SEMANTIC_ERROR } + }; + + (void)state; + + int32_t code; + + for (size_t i=0, n = sizeof(tests)/sizeof(tests[0]); i < n; i++) { + char buf[512]; + const char algo = tests[i].algorithm; + const int len = tests[i].digest_length; + + snprintf(buf, sizeof(buf), fmt, algo + 0x30, len * 2, hex); + code = parse_digest(buf); + assert_int_equal(code, tests[i].code); + + snprintf(buf, sizeof(buf), hex_fmt, 6 + len, algo + 0x30, len * 2, hex); + code = parse_digest(buf); + assert_int_equal(code, tests[i].code); + } +}