From dd6640a921a5de7f2b35d8bb852d6eb52527f0a7 Mon Sep 17 00:00:00 2001 From: Ilya Etingof Date: Sun, 30 Jun 2019 23:49:59 +0200 Subject: Improve CER/DER encoding of GeneralizedTime (#164) - Added support for subseconds CER/DER encoding edge cases in `GeneralizedTime` codec - Fixed 3-digit fractional seconds value CER/DER encoding of `GeneralizedTime` --- CHANGES.rst | 4 +++ pyasn1/codec/cer/encoder.py | 71 +++++++++++++++++++++++++++++------------ tests/codec/cer/test_encoder.py | 24 ++++++++++++-- 3 files changed, 76 insertions(+), 23 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index e979039..448e372 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -29,6 +29,10 @@ Revision 0.4.6, released XX-06-2019 - Added `PyAsn1UnicodeDecodeError`/`PyAsn1UnicodeDecodeError` exceptions to help the caller treating unicode errors happening internally to pyasn1 at the upper layers. +- Added support for subseconds CER/DER encoding edge cases in + `GeneralizedTime` codec. +- Fixed 3-digit fractional seconds value CER/DER encoding of + `GeneralizedTime`. - Fixed `AnyDecoder` to accept possible `TagMap` as `asn1Spec` to make dumping raw value operational diff --git a/pyasn1/codec/cer/encoder.py b/pyasn1/codec/cer/encoder.py index 788567f..3fdcc1d 100644 --- a/pyasn1/codec/cer/encoder.py +++ b/pyasn1/codec/cer/encoder.py @@ -31,17 +31,20 @@ class RealEncoder(encoder.RealEncoder): # specialized GeneralStringEncoder here class TimeEncoderMixIn(object): - zchar, = str2octs('Z') - pluschar, = str2octs('+') - minuschar, = str2octs('-') - commachar, = str2octs(',') - minLength = 12 - maxLength = 19 + Z_CHAR = ord('Z') + PLUS_CHAR = ord('+') + MINUS_CHAR = ord('-') + COMMA_CHAR = ord(',') + DOT_CHAR = ord('.') + ZERO_CHAR = ord('0') + + MIN_LENGTH = 12 + MAX_LENGTH = 19 def encodeValue(self, value, asn1Spec, encodeFun, **options): - # Encoding constraints: + # CER encoding constraints: # - minutes are mandatory, seconds are optional - # - sub-seconds must NOT be zero + # - sub-seconds must NOT be zero / no meaningless zeros # - no hanging fraction dot # - time in UTC (Z) # - only dot is allowed for fractions @@ -49,20 +52,46 @@ class TimeEncoderMixIn(object): if asn1Spec is not None: value = asn1Spec.clone(value) - octets = value.asOctets() - - if not self.minLength < len(octets) < self.maxLength: - raise error.PyAsn1Error('Length constraint violated: %r' % value) + numbers = value.asNumbers() - if self.pluschar in octets or self.minuschar in octets: - raise error.PyAsn1Error('Must be UTC time: %r' % octets) + if self.PLUS_CHAR in numbers or self.MINUS_CHAR in numbers: + raise error.PyAsn1Error('Must be UTC time: %r' % value) - if octets[-1] != self.zchar: - raise error.PyAsn1Error('Missing "Z" time zone specifier: %r' % octets) + if numbers[-1] != self.Z_CHAR: + raise error.PyAsn1Error('Missing "Z" time zone specifier: %r' % value) - if self.commachar in octets: + if self.COMMA_CHAR in numbers: raise error.PyAsn1Error('Comma in fractions disallowed: %r' % value) + if self.DOT_CHAR in numbers: + + isModified = False + + numbers = list(numbers) + + searchIndex = min(numbers.index(self.DOT_CHAR) + 4, len(numbers) - 1) + + while numbers[searchIndex] != self.DOT_CHAR: + if numbers[searchIndex] == self.ZERO_CHAR: + del numbers[searchIndex] + isModified = True + + searchIndex -= 1 + + searchIndex += 1 + + if searchIndex < len(numbers): + if numbers[searchIndex] == self.Z_CHAR: + # drop hanging comma + del numbers[searchIndex - 1] + isModified = True + + if isModified: + value = value.clone(numbers) + + if not self.MIN_LENGTH < len(numbers) < self.MAX_LENGTH: + raise error.PyAsn1Error('Length constraint violated: %r' % value) + options.update(maxChunkSize=1000) return encoder.OctetStringEncoder.encodeValue( @@ -71,13 +100,13 @@ class TimeEncoderMixIn(object): class GeneralizedTimeEncoder(TimeEncoderMixIn, encoder.OctetStringEncoder): - minLength = 12 - maxLength = 19 + MIN_LENGTH = 12 + MAX_LENGTH = 20 class UTCTimeEncoder(TimeEncoderMixIn, encoder.OctetStringEncoder): - minLength = 10 - maxLength = 14 + MIN_LENGTH = 10 + MAX_LENGTH = 14 class SetEncoder(encoder.SequenceEncoder): diff --git a/tests/codec/cer/test_encoder.py b/tests/codec/cer/test_encoder.py index ea8f813..130fe17 100644 --- a/tests/codec/cer/test_encoder.py +++ b/tests/codec/cer/test_encoder.py @@ -87,7 +87,7 @@ class GeneralizedTimeEncoderTestCase(BaseTestCase): def testDecimalCommaPoint(self): try: assert encoder.encode( - useful.GeneralizedTime('20150501120112,1Z') + useful.GeneralizedTime('20150501120112,1Z') ) except PyAsn1Error: pass @@ -96,9 +96,29 @@ class GeneralizedTimeEncoderTestCase(BaseTestCase): def testWithSubseconds(self): assert encoder.encode( - useful.GeneralizedTime('20170801120112.59Z') + useful.GeneralizedTime('20170801120112.59Z') ) == ints2octs((24, 18, 50, 48, 49, 55, 48, 56, 48, 49, 49, 50, 48, 49, 49, 50, 46, 53, 57, 90)) + def testWithSubsecondsWithZeros(self): + assert encoder.encode( + useful.GeneralizedTime('20170801120112.099Z') + ) == ints2octs((24, 18, 50, 48, 49, 55, 48, 56, 48, 49, 49, 50, 48, 49, 49, 50, 46, 57, 57, 90)) + + def testWithSubsecondsMax(self): + assert encoder.encode( + useful.GeneralizedTime('20170801120112.999Z') + ) == ints2octs((24, 19, 50, 48, 49, 55, 48, 56, 48, 49, 49, 50, 48, 49, 49, 50, 46, 57, 57, 57, 90)) + + def testWithSubsecondsMin(self): + assert encoder.encode( + useful.GeneralizedTime('20170801120112.000Z') + ) == ints2octs((24, 15, 50, 48, 49, 55, 48, 56, 48, 49, 49, 50, 48, 49, 49, 50, 90)) + + def testWithSubsecondsDanglingDot(self): + assert encoder.encode( + useful.GeneralizedTime('20170801120112.Z') + ) == ints2octs((24, 15, 50, 48, 49, 55, 48, 56, 48, 49, 49, 50, 48, 49, 49, 50, 90)) + def testWithSeconds(self): assert encoder.encode( useful.GeneralizedTime('20170801120112Z') -- cgit v1.2.1