summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIlya Etingof <etingof@gmail.com>2019-06-29 17:37:35 +0200
committerIlya Etingof <etingof@gmail.com>2019-06-29 17:37:35 +0200
commitaedb91be41699454363b08fdd8fd1223542a4a6c (patch)
tree99dd97358516473d4f5af0da4c9399f3cb7b9ac8
parent4a9abf7ae867e9ebabc850320d87a7c1230acfad (diff)
downloadpyasn1-git-fix-generalized-time-encoding.tar.gz
Improve CER/DER encoding of GeneralizedTimefix-generalized-time-encoding
- Added support for subseconds CER/DER encoding edge cases in `GeneralizedTime` codec - Fixed 3-digit fractional seconds value CER/DER encoding of `GeneralizedTime`
-rw-r--r--CHANGES.rst4
-rw-r--r--pyasn1/codec/cer/encoder.py71
-rw-r--r--tests/codec/cer/test_encoder.py24
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')