Skip to content

Commit

Permalink
Factored Out ASN1_TIME Encoding and Decoding Methods. (#21489)
Browse files Browse the repository at this point in the history
Added unit tests.
  • Loading branch information
emargolis authored and pull[bot] committed Jul 21, 2023
1 parent 7c85d4b commit add1012
Show file tree
Hide file tree
Showing 7 changed files with 284 additions and 80 deletions.
40 changes: 33 additions & 7 deletions src/lib/asn1/ASN1.h
Original file line number Diff line number Diff line change
@@ -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.
*
Expand Down Expand Up @@ -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
Expand Down
29 changes: 2 additions & 27 deletions src/lib/asn1/ASN1Reader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<uint16_t>((Value[0] - '0') * 10 + (Value[1] - '0'));
outTime.Month = static_cast<uint8_t>((Value[2] - '0') * 10 + (Value[3] - '0'));
outTime.Day = static_cast<uint8_t>((Value[4] - '0') * 10 + (Value[5] - '0'));
outTime.Hour = static_cast<uint8_t>((Value[6] - '0') * 10 + (Value[7] - '0'));
outTime.Minute = static_cast<uint8_t>((Value[8] - '0') * 10 + (Value[9] - '0'));
outTime.Second = static_cast<uint8_t>((Value[10] - '0') * 10 + (Value[11] - '0'));

outTime.Year = static_cast<uint16_t>(outTime.Year + ((outTime.Year >= 50) ? 1900 : 2000));

return CHIP_NO_ERROR;
return outTime.ImportFrom_ASN1_TIME_string(CharSpan(reinterpret_cast<const char *>(Value), ValueLen));
}

CHIP_ERROR ASN1Reader::GetGeneralizedTime(ASN1UniversalTime & outTime)
Expand All @@ -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<uint16_t>((Value[0] - '0') * 1000 + (Value[1] - '0') * 100 + (Value[2] - '0') * 10 + (Value[3] - '0'));
outTime.Month = static_cast<uint8_t>((Value[4] - '0') * 10 + (Value[5] - '0'));
outTime.Day = static_cast<uint8_t>((Value[6] - '0') * 10 + (Value[7] - '0'));
outTime.Hour = static_cast<uint8_t>((Value[8] - '0') * 10 + (Value[9] - '0'));
outTime.Minute = static_cast<uint8_t>((Value[10] - '0') * 10 + (Value[11] - '0'));
outTime.Second = static_cast<uint8_t>((Value[12] - '0') * 10 + (Value[13] - '0'));

return CHIP_NO_ERROR;
return outTime.ImportFrom_ASN1_TIME_string(CharSpan(reinterpret_cast<const char *>(Value), ValueLen));
}

static uint8_t ReverseBits(uint8_t v)
Expand Down
142 changes: 142 additions & 0 deletions src/lib/asn1/ASN1Time.cpp
Original file line number Diff line number Diff line change
@@ -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 <getopt.h>
#include <inttypes.h>
#include <limits.h>
#include <memory>
#include <stdint.h>
#include <string.h>
#include <unistd.h>

#include <ctype.h>
#include <stdio.h>

#include <lib/asn1/ASN1.h>
#include <lib/support/TimeUtils.h>

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<uint8_t>((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<char>('0' + (val % 10));
val /= 10;
buf[0] = static_cast<char>('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<uint16_t>(atoi2(p) * 100 + atoi2(p));
}
else
{
Year = atoi2(p);
Year = static_cast<uint16_t>(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<size_t>(p - asn1_time.data() + 1));

return CHIP_NO_ERROR;
}

} // namespace ASN1
} // namespace chip
39 changes: 14 additions & 25 deletions src/lib/asn1/ASN1Writer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<uint8_t>('0' + (val % 10));
val /= 10;
buf[0] = static_cast<uint8_t>('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<uint8_t *>(buf), static_cast<uint16_t>(bufSpan.size()));
}

CHIP_ERROR ASN1Writer::PutNull()
Expand Down
1 change: 1 addition & 0 deletions src/lib/asn1/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ static_library("asn1") {
"ASN1Macros.h",
"ASN1OID.cpp",
"ASN1Reader.cpp",
"ASN1Time.cpp",
"ASN1Writer.cpp",
]

Expand Down
74 changes: 74 additions & 0 deletions src/lib/asn1/tests/TestASN1.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()
Expand Down
Loading

0 comments on commit add1012

Please sign in to comment.