From add101232e6ef6d7f3a834608a5348e42ae71ca9 Mon Sep 17 00:00:00 2001 From: Evgeny Margolis Date: Mon, 1 Aug 2022 17:31:10 -0700 Subject: [PATCH] Factored Out ASN1_TIME Encoding and Decoding Methods. (#21489) Added unit tests. --- src/lib/asn1/ASN1.h | 40 +++++++-- src/lib/asn1/ASN1Reader.cpp | 29 +----- src/lib/asn1/ASN1Time.cpp | 142 ++++++++++++++++++++++++++++++ src/lib/asn1/ASN1Writer.cpp | 39 +++----- src/lib/asn1/BUILD.gn | 1 + src/lib/asn1/tests/TestASN1.cpp | 74 ++++++++++++++++ src/tools/chip-cert/CertUtils.cpp | 39 ++++---- 7 files changed, 284 insertions(+), 80 deletions(-) create mode 100644 src/lib/asn1/ASN1Time.cpp diff --git a/src/lib/asn1/ASN1.h b/src/lib/asn1/ASN1.h index f016485704c01a..77e5b8290525be 100644 --- a/src/lib/asn1/ASN1.h +++ b/src/lib/asn1/ASN1.h @@ -1,6 +1,6 @@ /* * - * Copyright (c) 2020-2021 Project CHIP Authors + * Copyright (c) 2020-2022 Project CHIP Authors * Copyright (c) 2013-2017 Nest Labs, Inc. * All rights reserved. * @@ -89,14 +89,40 @@ enum ASN1UniversalTags : uint8_t kASN1UniversalTag_UniversalString = 28 }; +/** + * @struct ASN1UniversalTime + * + * @brief + * A data structure representing ASN1 universal time in a calendar format. + */ struct ASN1UniversalTime { - uint16_t Year; - uint8_t Month; - uint8_t Day; - uint8_t Hour; - uint8_t Minute; - uint8_t Second; + uint16_t Year; /**< Year component. Legal interval is 0..9999. */ + uint8_t Month; /**< Month component. Legal interval is 1..12. */ + uint8_t Day; /**< Day of month component. Legal interval is 1..31. */ + uint8_t Hour; /**< Hour component. Legal interval is 0..23. */ + uint8_t Minute; /**< Minute component. Legal interval is 0..59. */ + uint8_t Second; /**< Second component. Legal interval is 0..59. */ + + static constexpr size_t kASN1UTCTimeStringLength = 13; + static constexpr size_t kASN1GeneralizedTimeStringLength = 15; + static constexpr size_t kASN1TimeStringMaxLength = 15; + + /** + * @brief Set time from ASN1_TIME string. + * Two string formats are supported: + * YYMMDDHHMMSSZ - for years in the range 1950 - 2049 + * YYYYMMDDHHMMSSZ - other years + **/ + CHIP_ERROR ImportFrom_ASN1_TIME_string(const CharSpan & asn1_time); + + /** + * @brief Encode time as an ASN1_TIME string. + * Two string formats are supported: + * YYMMDDHHMMSSZ - for years in the range 1950 - 2049 + * YYYYMMDDHHMMSSZ - other years + **/ + CHIP_ERROR ExportTo_ASN1_TIME_string(MutableCharSpan & asn1_time) const; }; class DLL_EXPORT ASN1Reader diff --git a/src/lib/asn1/ASN1Reader.cpp b/src/lib/asn1/ASN1Reader.cpp index 30b8f1c493cb58..14e583b6007a5f 100644 --- a/src/lib/asn1/ASN1Reader.cpp +++ b/src/lib/asn1/ASN1Reader.cpp @@ -190,21 +190,8 @@ CHIP_ERROR ASN1Reader::GetUTCTime(ASN1UniversalTime & outTime) ReturnErrorCodeIf(ValueLen < 1, ASN1_ERROR_INVALID_ENCODING); ReturnErrorCodeIf(mElemStart + mHeadLen + ValueLen > mContainerEnd, ASN1_ERROR_UNDERRUN); VerifyOrReturnError(ValueLen == 13 && Value[12] == 'Z', ASN1_ERROR_UNSUPPORTED_ENCODING); - for (int i = 0; i < 12; i++) - { - VerifyOrReturnError(isdigit(Value[i]), ASN1_ERROR_INVALID_ENCODING); - } - - outTime.Year = static_cast((Value[0] - '0') * 10 + (Value[1] - '0')); - outTime.Month = static_cast((Value[2] - '0') * 10 + (Value[3] - '0')); - outTime.Day = static_cast((Value[4] - '0') * 10 + (Value[5] - '0')); - outTime.Hour = static_cast((Value[6] - '0') * 10 + (Value[7] - '0')); - outTime.Minute = static_cast((Value[8] - '0') * 10 + (Value[9] - '0')); - outTime.Second = static_cast((Value[10] - '0') * 10 + (Value[11] - '0')); - outTime.Year = static_cast(outTime.Year + ((outTime.Year >= 50) ? 1900 : 2000)); - - return CHIP_NO_ERROR; + return outTime.ImportFrom_ASN1_TIME_string(CharSpan(reinterpret_cast(Value), ValueLen)); } CHIP_ERROR ASN1Reader::GetGeneralizedTime(ASN1UniversalTime & outTime) @@ -214,20 +201,8 @@ CHIP_ERROR ASN1Reader::GetGeneralizedTime(ASN1UniversalTime & outTime) ReturnErrorCodeIf(ValueLen < 1, ASN1_ERROR_INVALID_ENCODING); ReturnErrorCodeIf(mElemStart + mHeadLen + ValueLen > mContainerEnd, ASN1_ERROR_UNDERRUN); VerifyOrReturnError(ValueLen == 15 && Value[14] == 'Z', ASN1_ERROR_UNSUPPORTED_ENCODING); - for (int i = 0; i < 14; i++) - { - VerifyOrReturnError(isdigit(Value[i]), ASN1_ERROR_INVALID_ENCODING); - } - outTime.Year = - static_cast((Value[0] - '0') * 1000 + (Value[1] - '0') * 100 + (Value[2] - '0') * 10 + (Value[3] - '0')); - outTime.Month = static_cast((Value[4] - '0') * 10 + (Value[5] - '0')); - outTime.Day = static_cast((Value[6] - '0') * 10 + (Value[7] - '0')); - outTime.Hour = static_cast((Value[8] - '0') * 10 + (Value[9] - '0')); - outTime.Minute = static_cast((Value[10] - '0') * 10 + (Value[11] - '0')); - outTime.Second = static_cast((Value[12] - '0') * 10 + (Value[13] - '0')); - - return CHIP_NO_ERROR; + return outTime.ImportFrom_ASN1_TIME_string(CharSpan(reinterpret_cast(Value), ValueLen)); } static uint8_t ReverseBits(uint8_t v) diff --git a/src/lib/asn1/ASN1Time.cpp b/src/lib/asn1/ASN1Time.cpp new file mode 100644 index 00000000000000..822c089a01a9bd --- /dev/null +++ b/src/lib/asn1/ASN1Time.cpp @@ -0,0 +1,142 @@ +/* + * + * Copyright (c) 2022 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @file + * This file implements Abstract Syntax Notation One (ASN.1) Time functions. + * + */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +namespace chip { +namespace ASN1 { + +namespace { + +/** + * @brief Parses the two first characters C-string interpreting its content as a two base-10 digits number. + * The C-string pointer is advansed by two characters. + */ +uint8_t atoi2(const char *& buf) +{ + uint8_t val = static_cast((buf[0] - '0') * 10 + (buf[1] - '0')); + buf += 2; + return val; +} + +/** + * @brief Converts two low significan base-10 digits of an integer value (val % 100) to a two character C-string. + * The C-string pointer is advansed by two characters. + */ +void itoa2(uint32_t val, char *& buf) +{ + buf[1] = static_cast('0' + (val % 10)); + val /= 10; + buf[0] = static_cast('0' + (val % 10)); + buf += 2; +} + +} // anonymous namespace + +CHIP_ERROR ASN1UniversalTime::ImportFrom_ASN1_TIME_string(const CharSpan & asn1_time) +{ + const char * p = asn1_time.data(); + const size_t size = asn1_time.size(); + + VerifyOrReturnError(p != nullptr, ASN1_ERROR_INVALID_STATE); + VerifyOrReturnError(size == kASN1UTCTimeStringLength || size == kASN1GeneralizedTimeStringLength, + ASN1_ERROR_UNSUPPORTED_ENCODING); + VerifyOrReturnError(p[size - 1] == 'Z', ASN1_ERROR_UNSUPPORTED_ENCODING); + for (size_t i = 0; i < size - 1; i++) + { + VerifyOrReturnError(isdigit(p[i]), ASN1_ERROR_INVALID_ENCODING); + } + + if (size == kASN1GeneralizedTimeStringLength) + { + Year = static_cast(atoi2(p) * 100 + atoi2(p)); + } + else + { + Year = atoi2(p); + Year = static_cast(Year + ((Year >= 50) ? 1900 : 2000)); + } + + Month = atoi2(p); + Day = atoi2(p); + Hour = atoi2(p); + Minute = atoi2(p); + Second = atoi2(p); + + VerifyOrReturnError(Month > 0 && Month <= kMonthsPerYear, ASN1_ERROR_INVALID_ENCODING); + VerifyOrReturnError(Day > 0 && Day <= kMaxDaysPerMonth, ASN1_ERROR_INVALID_ENCODING); + VerifyOrReturnError(Hour < kHoursPerDay, ASN1_ERROR_INVALID_ENCODING); + VerifyOrReturnError(Minute < kMinutesPerHour, ASN1_ERROR_INVALID_ENCODING); + VerifyOrReturnError(Second < kSecondsPerMinute, ASN1_ERROR_INVALID_ENCODING); + + return CHIP_NO_ERROR; +} + +CHIP_ERROR ASN1UniversalTime::ExportTo_ASN1_TIME_string(MutableCharSpan & asn1_time) const +{ + char * p = asn1_time.data(); + + VerifyOrReturnError(p != nullptr, ASN1_ERROR_INVALID_STATE); + + // X.509/RFC5280 mandates that times before 2050 UTC must be encoded as ASN.1 UTCTime values, while + // times equal or greater than 2050 must be encoded as GeneralizedTime values. The only difference + // (in the context of X.509 DER) is that GeneralizedTimes are encoded with a 4 digit year, while + // UTCTimes are encoded with a two-digit year. + if (Year < 1950 || Year >= 2050) + { + VerifyOrReturnError(asn1_time.size() >= kASN1GeneralizedTimeStringLength, ASN1_ERROR_UNDERRUN); + itoa2(Year / 100, p); + } + else + { + VerifyOrReturnError(asn1_time.size() >= kASN1UTCTimeStringLength, ASN1_ERROR_UNDERRUN); + } + + itoa2(Year, p); + itoa2(Month, p); + itoa2(Day, p); + itoa2(Hour, p); + itoa2(Minute, p); + itoa2(Second, p); + *p = 'Z'; + + asn1_time.reduce_size(static_cast(p - asn1_time.data() + 1)); + + return CHIP_NO_ERROR; +} + +} // namespace ASN1 +} // namespace chip diff --git a/src/lib/asn1/ASN1Writer.cpp b/src/lib/asn1/ASN1Writer.cpp index a67ca0e041e7c2..396f3e9356f763 100644 --- a/src/lib/asn1/ASN1Writer.cpp +++ b/src/lib/asn1/ASN1Writer.cpp @@ -255,34 +255,23 @@ CHIP_ERROR ASN1Writer::PutBitString(uint8_t unusedBitCount, chip::TLV::TLVReader return CHIP_NO_ERROR; } -static void itoa2(uint32_t val, uint8_t * buf) -{ - buf[1] = static_cast('0' + (val % 10)); - val /= 10; - buf[0] = static_cast('0' + (val % 10)); -} - CHIP_ERROR ASN1Writer::PutTime(const ASN1UniversalTime & val) { - uint8_t buf[15]; - - itoa2(val.Year / 100, buf); - itoa2(val.Year, buf + 2); - itoa2(val.Month, buf + 4); - itoa2(val.Day, buf + 6); - itoa2(val.Hour, buf + 8); - itoa2(val.Minute, buf + 10); - itoa2(val.Second, buf + 12); - buf[14] = 'Z'; - - // X.509/RFC5280 mandates that times before 2050 UTC must be encoded as ASN.1 UTCTime values, while - // times equal or greater than 2050 must be encoded as GeneralizedTime values. The only difference - // (in the context of X.509 DER) is that GeneralizedTimes are encoded with a 4 digit year, while - // UTCTimes are encoded with a two-digit year. - // + char buf[ASN1UniversalTime::kASN1TimeStringMaxLength]; + MutableCharSpan bufSpan(buf); + uint8_t tag; + + ReturnErrorOnFailure(val.ExportTo_ASN1_TIME_string(bufSpan)); + if (val.Year >= 2050) - return PutValue(kASN1TagClass_Universal, kASN1UniversalTag_GeneralizedTime, false, buf, 15); - return PutValue(kASN1TagClass_Universal, kASN1UniversalTag_UTCTime, false, buf + 2, 13); + { + tag = kASN1UniversalTag_GeneralizedTime; + } + else + { + tag = kASN1UniversalTag_UTCTime; + } + return PutValue(kASN1TagClass_Universal, tag, false, reinterpret_cast(buf), static_cast(bufSpan.size())); } CHIP_ERROR ASN1Writer::PutNull() diff --git a/src/lib/asn1/BUILD.gn b/src/lib/asn1/BUILD.gn index 6be0640635d84f..0d04176a689487 100644 --- a/src/lib/asn1/BUILD.gn +++ b/src/lib/asn1/BUILD.gn @@ -39,6 +39,7 @@ static_library("asn1") { "ASN1Macros.h", "ASN1OID.cpp", "ASN1Reader.cpp", + "ASN1Time.cpp", "ASN1Writer.cpp", ] diff --git a/src/lib/asn1/tests/TestASN1.cpp b/src/lib/asn1/tests/TestASN1.cpp index 9aa7a480889789..048ddeb91a38e7 100644 --- a/src/lib/asn1/tests/TestASN1.cpp +++ b/src/lib/asn1/tests/TestASN1.cpp @@ -301,6 +301,79 @@ static void TestASN1_NullWriter(nlTestSuite * inSuite, void * inContext) NL_TEST_ASSERT(inSuite, encodedLen == 0); } +static void TestASN1_ASN1UniversalTime(nlTestSuite * inSuite, void * inContext) +{ + struct ASN1TimeTestCase + { + ASN1UniversalTime asn1Time; + const char * asn1TimeStr; + }; + + struct ASN1TimeErrorTestCase + { + const char * asn1TimeStr; + CHIP_ERROR mExpectedResult; + }; + + // clang-format off + static ASN1TimeTestCase sASN1TimeTestCases[] = { + // ASN1 Universal Time ASN1_TIME String + // ==================================================== + { { 2020, 10, 15, 14, 23, 43 }, "201015142343Z" }, + { { 2020, 12, 1, 2, 34, 0 }, "201201023400Z" }, + { { 1979, 1, 30, 12, 0, 0 }, "790130120000Z" }, + { { 2079, 1, 30, 12, 0, 0 }, "20790130120000Z" }, + { { 2049, 3, 31, 23, 59, 59 }, "490331235959Z" }, + { { 1949, 3, 31, 23, 59, 59 }, "19490331235959Z" }, + { { 1950, 3, 31, 23, 59, 59 }, "500331235959Z" }, + }; + // clang-format on + + // clang-format off + static ASN1TimeErrorTestCase sASN1TimeErrorTestCases[] = { + // ASN1_TIME String Expected Result + // ======================================================= + { "201015142343z", ASN1_ERROR_UNSUPPORTED_ENCODING }, + { "20105142343Z", ASN1_ERROR_UNSUPPORTED_ENCODING }, + { "2010115142343Z", ASN1_ERROR_UNSUPPORTED_ENCODING }, + { "201014415142343Z", ASN1_ERROR_UNSUPPORTED_ENCODING }, + { "201O15142343Z", ASN1_ERROR_INVALID_ENCODING }, + { "200015142343Z", ASN1_ERROR_INVALID_ENCODING }, + { "201315142343Z", ASN1_ERROR_INVALID_ENCODING }, + { "201000142343Z", ASN1_ERROR_INVALID_ENCODING }, + { "201032142343Z", ASN1_ERROR_INVALID_ENCODING }, + { "201015242343Z", ASN1_ERROR_INVALID_ENCODING }, + { "201015146043Z", ASN1_ERROR_INVALID_ENCODING }, + { "201015142360Z", ASN1_ERROR_INVALID_ENCODING }, + }; + // clang-format on + + for (auto & testCase : sASN1TimeTestCases) + { + CharSpan testStr = CharSpan(testCase.asn1TimeStr, strlen(testCase.asn1TimeStr)); + ASN1UniversalTime result; + + NL_TEST_ASSERT(inSuite, result.ImportFrom_ASN1_TIME_string(testStr) == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, + result.Year == testCase.asn1Time.Year && result.Month == testCase.asn1Time.Month && + result.Day == testCase.asn1Time.Day && result.Hour == testCase.asn1Time.Hour && + result.Minute == testCase.asn1Time.Minute && result.Second == testCase.asn1Time.Second); + + char buf[ASN1UniversalTime::kASN1TimeStringMaxLength]; + MutableCharSpan resultTimeStr(buf); + NL_TEST_ASSERT(inSuite, result.ExportTo_ASN1_TIME_string(resultTimeStr) == CHIP_NO_ERROR); + NL_TEST_ASSERT(inSuite, resultTimeStr.data_equal(testStr)); + } + + for (auto & testCase : sASN1TimeErrorTestCases) + { + CharSpan testStr = CharSpan(testCase.asn1TimeStr, strlen(testCase.asn1TimeStr)); + ASN1UniversalTime result; + + NL_TEST_ASSERT(inSuite, result.ImportFrom_ASN1_TIME_string(testStr) == testCase.mExpectedResult); + } +} + static void TestASN1_ObjectID(nlTestSuite * inSuite, void * inContext) { CHIP_ERROR err; @@ -491,6 +564,7 @@ static const nlTest sTests[] = NL_TEST_DEF("Test ASN1 encoding macros", TestASN1_Encode), NL_TEST_DEF("Test ASN1 decoding macros", TestASN1_Decode), NL_TEST_DEF("Test ASN1 NULL writer", TestASN1_NullWriter), + NL_TEST_DEF("Test ASN1 universal time", TestASN1_ASN1UniversalTime), NL_TEST_DEF("Test ASN1 Object IDs", TestASN1_ObjectID), NL_TEST_DEF("Test ASN1 Init with ByteSpan", TestASN1_FromTLVReader), NL_TEST_SENTINEL() diff --git a/src/tools/chip-cert/CertUtils.cpp b/src/tools/chip-cert/CertUtils.cpp index 3cd79f7b14250f..a96f67bf996ccd 100644 --- a/src/tools/chip-cert/CertUtils.cpp +++ b/src/tools/chip-cert/CertUtils.cpp @@ -240,27 +240,24 @@ bool SetCertSerialNumber(X509 * cert) bool SetCertTimeField(ASN1_TIME * asn1Time, const struct tm & value) { - char timeStr[16]; - - // Encode the time as a string in the form YYYYMMDDHHMMSSZ. - snprintf(timeStr, sizeof(timeStr), "%04d%02d%02d%02d%02d%02dZ", - (value.tm_year == kX509NoWellDefinedExpirationDateYear) ? kX509NoWellDefinedExpirationDateYear - : (static_cast(value.tm_year + 1900) % 9999), - static_cast(value.tm_mon) % kMonthsPerYear + 1, static_cast(value.tm_mday) % (kMaxDaysPerMonth + 1), - static_cast(value.tm_hour) % kHoursPerDay, static_cast(value.tm_min) % kMinutesPerHour, - static_cast(value.tm_sec) % kSecondsPerMinute); - - // X.509/RFC-5280 mandates that times before 2050 UTC must be encoded as ASN.1 UTCTime values, while - // times equal or greater than 2050 must be encoded as GeneralizedTime values. The only difference - // between the two is the number of digits in the year -- 4 for GeneralizedTime, 2 for UTCTime. - // - // The OpenSSL ASN1_TIME_set_string() function DOES NOT handle picking the correct format based - // on the given year. Thus the caller MUST pass a correctly formatted string or the resultant - // certificate will be malformed. - - bool useUTCTime = ((value.tm_year + 1900) < 2050); - - if (!ASN1_TIME_set_string(asn1Time, timeStr + (useUTCTime ? 2 : 0))) + char timeStr[ASN1UniversalTime::kASN1TimeStringMaxLength + 1]; + MutableCharSpan timeSpan(timeStr); + ASN1UniversalTime val = { .Year = static_cast(value.tm_year + 1900), + .Month = static_cast(value.tm_mon + 1), + .Day = static_cast(value.tm_mday), + .Hour = static_cast(value.tm_hour), + .Minute = static_cast(value.tm_min), + .Second = static_cast(value.tm_sec) }; + + if (val.ExportTo_ASN1_TIME_string(timeSpan) != CHIP_NO_ERROR) + { + fprintf(stderr, "ExportTo_ASN1_TIME_string() failed\n"); + return false; + } + + timeSpan.data()[timeSpan.size()] = '\0'; + + if (!ASN1_TIME_set_string(asn1Time, timeStr)) { fprintf(stderr, "OpenSSL ASN1_TIME_set_string() failed\n"); return false;