summary refs log tree commit diff
diff options
context:
space:
mode:
authorPeter Dettman <peter.dettman@bouncycastle.org>2022-11-08 13:18:17 +0700
committerPeter Dettman <peter.dettman@bouncycastle.org>2022-11-08 13:18:17 +0700
commit879bb29bb0058a0723326de6edebb201e4cbc0b8 (patch)
tree089e7769c2342a99dfa7549c57290522f72c451b
parentremoved unused SecureRandom (diff)
downloadBouncyCastle.NET-ed25519-879bb29bb0058a0723326de6edebb201e4cbc0b8.tar.xz
Overhaul GeneralizedTime classes
-rw-r--r--crypto/src/asn1/Asn1GeneralizedTime.cs422
-rw-r--r--crypto/src/asn1/Asn1UtcTime.cs18
-rw-r--r--crypto/src/asn1/DerGeneralizedTime.cs17
-rw-r--r--crypto/src/asn1/cms/Time.cs5
-rw-r--r--crypto/src/asn1/util/Asn1Dump.cs2
-rw-r--r--crypto/src/asn1/x509/Time.cs5
-rw-r--r--crypto/src/tsp/TimeStampTokenGenerator.cs61
-rw-r--r--crypto/src/tsp/TimeStampTokenInfo.cs21
-rw-r--r--crypto/src/util/Platform.cs15
-rw-r--r--crypto/src/util/date/DateTimeUtilities.cs28
-rw-r--r--crypto/test/src/asn1/test/GeneralizedTimeTest.cs131
-rw-r--r--crypto/test/src/tsp/test/NewTspTest.cs10
-rw-r--r--crypto/test/src/tsp/test/TimeStampTokenInfoTest.cs21
13 files changed, 238 insertions, 518 deletions
diff --git a/crypto/src/asn1/Asn1GeneralizedTime.cs b/crypto/src/asn1/Asn1GeneralizedTime.cs
index 139384c1a..beabbdfa5 100644
--- a/crypto/src/asn1/Asn1GeneralizedTime.cs
+++ b/crypto/src/asn1/Asn1GeneralizedTime.cs
@@ -7,35 +7,7 @@ using Org.BouncyCastle.Utilities;
 
 namespace Org.BouncyCastle.Asn1
 {
-    /**
-     * Base class representing the ASN.1 GeneralizedTime type.
-     * <p>
-     * The main difference between these and UTC time is a 4 digit year.
-     * </p><p>
-     * One second resolution date+time on UTC timezone (Z)
-     * with 4 digit year (valid from 0001 to 9999).
-     * </p><p>
-     * Timestamp format is:  yyyymmddHHMMSS'Z'
-     * </p><p>
-     * <h2>X.690</h2>
-     * This is what is called "restricted string",
-     * and it uses ASCII characters to encode digits and supplemental data.
-     *
-     * <h3>11: Restrictions on BER employed by both CER and DER</h3>
-     * <h4>11.7 GeneralizedTime </h4>
-     * </p><p>
-     * <b>11.7.1</b> The encoding shall terminate with a "Z",
-     * as described in the ITU-T Rec. X.680 | ISO/IEC 8824-1 clause on
-     * GeneralizedTime.
-     * </p><p>
-     * <b>11.7.2</b> The seconds element shall always be present.
-     * </p><p>
-     * <b>11.7.3</b> The fractional-seconds elements, if present,
-     * shall omit all trailing zeros; if the elements correspond to 0,
-     * they shall be wholly omitted, and the decimal point element also
-     * shall be omitted.
-     * </p>
-     */
+    /// <summary>GeneralizedTime ASN.1 type</summary>
     public class Asn1GeneralizedTime
         : Asn1Object
     {
@@ -85,15 +57,18 @@ namespace Org.BouncyCastle.Asn1
             return (Asn1GeneralizedTime)Meta.Instance.GetContextInstance(taggedObject, declaredExplicit);
         }
 
-        internal readonly byte[] m_contents;
+        private readonly string m_timeString;
+        private readonly bool m_timeStringCanonical;
+        private readonly DateTime m_dateTime;
 
-        public Asn1GeneralizedTime(string time)
+        public Asn1GeneralizedTime(string timeString)
         {
-            m_contents = Strings.ToByteArray(time);
+            m_timeString = timeString ?? throw new ArgumentNullException(nameof(timeString));
+            m_timeStringCanonical = false; // TODO Dynamic check?
 
             try
             {
-                ToDateTime();
+                m_dateTime = FromString(timeString);
             }
             catch (FormatException e)
             {
@@ -101,238 +76,45 @@ namespace Org.BouncyCastle.Asn1
             }
         }
 
-        public Asn1GeneralizedTime(DateTime time)
+        public Asn1GeneralizedTime(DateTime dateTime)
         {
-            DateTime utc = time.ToUniversalTime();
-            var formatStr = @"yyyyMMddHHmmss\Z";
-            var formatProvider = DateTimeFormatInfo.InvariantInfo;
-            string utcString = utc.ToString(formatStr, formatProvider);
-            m_contents = Strings.ToByteArray(utcString);
-        }
-
-        // TODO Custom locale constructor?
-        //public Asn1GeneralizedTime(DateTime time, Locale locale)
-        //{
-        //    SimpleDateFormat dateF = new SimpleDateFormat("yyyyMMddHHmmss\Z", locale);
-
-        //    dateF.setTimeZone(new SimpleTimeZone(0, "Z"));
-
-        //    this.contents = Strings.toByteArray(dateF.format(time));
-        //}
-
-        internal Asn1GeneralizedTime(byte[] bytes)
-        {
-            if (bytes == null)
-                throw new ArgumentNullException(nameof(bytes));
-            if (bytes.Length < 4)
-                throw new ArgumentException("GeneralizedTime string too short", nameof(bytes));
-
-            m_contents = bytes;
-
-            if (!(IsDigit(0) && IsDigit(1) && IsDigit(2) && IsDigit(3)))
-                throw new ArgumentException("illegal characters in GeneralizedTime string", nameof(bytes));
-        }
-
-        public string TimeString => Strings.FromByteArray(m_contents);
-
-        public string GetTime()
-        {
-            string stime = Strings.FromByteArray(m_contents);
-
-            //
-            // standardise the format.
-            //
-            if (stime[stime.Length - 1] == 'Z')
-                return stime.Substring(0, stime.Length - 1) + "GMT+00:00";
-
-            int signPos = stime.Length - 6;
-            char sign = stime[signPos];
-            if ((sign == '-' || sign == '+') && stime.IndexOf("GMT") == signPos - 3)
-            {
-                // already a GMT string!
-                return stime;
-            }
-
-            signPos = stime.Length - 5;
-            sign = stime[signPos];
-            if (sign == '-' || sign == '+')
-            {
-                return stime.Substring(0, signPos)
-                    + "GMT"
-                    + stime.Substring(signPos, 3)
-                    + ":"
-                    + stime.Substring(signPos + 3);
-            }
-
-            signPos = stime.Length - 3;
-            sign = stime[signPos];
-            if (sign == '-' || sign == '+')
-            {
-                return stime.Substring(0, signPos)
-                    + "GMT"
-                    + stime.Substring(signPos)
-                    + ":00";
-            }
+            dateTime = dateTime.ToUniversalTime();
 
-            return stime + CalculateGmtOffset(stime);
+            m_dateTime = dateTime;
+            m_timeString = ToStringCanonical(dateTime);
+            m_timeStringCanonical = true;
         }
 
-        private string CalculateGmtOffset(string stime)
-        {
-            TimeZoneInfo timeZone = TimeZoneInfo.Local;
-            TimeSpan offset = timeZone.BaseUtcOffset;
-
-            string sign = "+";
-            if (offset.CompareTo(TimeSpan.Zero) < 0)
-            {
-                sign = "-";
-                offset = offset.Duration();
-            }
-
-            int hours = offset.Hours;
-            int minutes = offset.Minutes;
-
-            try
-            {
-                if (timeZone.SupportsDaylightSavingTime)
-                {
-                    string d = stime + "GMT" + sign + Convert(hours) + ":" + Convert(minutes);
-                    string formatStr = CalculateGmtFormatString(d);
-
-                    DateTime dateTime = ParseDateString(d, formatStr, makeUniversal: true);
-
-                    if (timeZone.IsDaylightSavingTime(dateTime))
-                    {
-                        hours += sign.Equals("+") ? 1 : -1;
-                    }
-                }
-            }
-            catch (Exception)
-            {
-                // we'll do our best and ignore daylight savings
-            }
-
-            return "GMT" + sign + Convert(hours) + ":" + Convert(minutes);
-        }
+        // TODO TimeZoneInfo or other locale-specific constructors?
 
-        private string CalculateGmtFormatString(string d)
+        internal Asn1GeneralizedTime(byte[] contents)
+            : this(Encoding.ASCII.GetString(contents))
         {
-            if (HasFractionalSeconds())
-            {
-                int fCount = Platform.IndexOf(d, "GMT") - 1 - d.IndexOf('.');
-                return @"yyyyMMddHHmmss." + FString(fCount) + @"'GMT'zzz";
-            }
-
-            if (HasSeconds())
-                return @"yyyyMMddHHmmss'GMT'zzz";
-
-            if (HasMinutes())
-                return @"yyyyMMddHHmm'GMT'zzz";
-
-            return @"yyyyMMddHH'GMT'zzz";
         }
 
-        private string Convert(int time)
-        {
-            if (time < 10)
-                return "0" + time;
-
-            return time.ToString();
-        }
+        public string TimeString => m_timeString;
 
         public DateTime ToDateTime()
         {
-            string formatStr;
-            string stime = Strings.FromByteArray(m_contents);
-            string d = stime;
-            bool makeUniversal = false;
-
-            if (Platform.EndsWith(stime, "Z"))
-            {
-                if (HasFractionalSeconds())
-                {
-                    int fCount = d.Length - d.IndexOf('.') - 2;
-                    formatStr = @"yyyyMMddHHmmss." + FString(fCount) + @"\Z";
-                }
-                else if (HasSeconds())
-                {
-                    formatStr = @"yyyyMMddHHmmss\Z";
-                }
-                else if (HasMinutes())
-                {
-                    formatStr = @"yyyyMMddHHmm\Z";
-                }
-                else
-                {
-                    formatStr = @"yyyyMMddHH\Z";
-                }
-            }
-            else if (stime.IndexOf('-') > 0 || stime.IndexOf('+') > 0)
-            {
-                d = GetTime();
-                formatStr = CalculateGmtFormatString(d);
-                makeUniversal = true;
-            }
-            else
-            {
-                if (HasFractionalSeconds())
-                {
-                    int fCount = d.Length - 1 - d.IndexOf('.');
-                    formatStr = @"yyyyMMddHHmmss." + FString(fCount);
-                }
-                else if (HasSeconds())
-                {
-                    formatStr = @"yyyyMMddHHmmss";
-                }
-                else if (HasMinutes())
-                {
-                    formatStr = @"yyyyMMddHHmm";
-                }
-                else
-                {
-                    formatStr = @"yyyyMMddHH";
-                }
-            }
-
-            // TODO Epoch adjustment?
-            //return DateUtil.epochAdjust(dateF.parse(d));
-            return ParseDateString(d, formatStr, makeUniversal);
-        }
-
-        protected bool HasFractionalSeconds()
-        {
-            return m_contents.Length > 14 && m_contents[14] == '.';
-        }
-
-        protected bool HasSeconds()
-        {
-            return IsDigit(12) && IsDigit(13);
+            return m_dateTime;
         }
 
-        protected bool HasMinutes()
+        internal byte[] GetContents(int encoding)
         {
-            return IsDigit(10) && IsDigit(11);
-        }
+            if (encoding == Asn1OutputStream.EncodingDer && !m_timeStringCanonical)
+                return Encoding.ASCII.GetBytes(ToStringCanonical(m_dateTime));
 
-        private bool IsDigit(int pos)
-        {
-            return m_contents.Length > pos && m_contents[pos] >= '0' && m_contents[pos] <= '9';
+            return Encoding.ASCII.GetBytes(m_timeString);
         }
 
         internal override IAsn1Encoding GetEncoding(int encoding)
         {
-            if (Asn1OutputStream.EncodingDer == encoding)
-                return new PrimitiveEncoding(Asn1Tags.Universal, Asn1Tags.GeneralizedTime, GetDerTime());
-
-            return new PrimitiveEncoding(Asn1Tags.Universal, Asn1Tags.GeneralizedTime, m_contents);
+            return new PrimitiveEncoding(Asn1Tags.Universal, Asn1Tags.GeneralizedTime, GetContents(encoding));
         }
 
         internal override IAsn1Encoding GetEncodingImplicit(int encoding, int tagClass, int tagNo)
         {
-            if (Asn1OutputStream.EncodingDer == encoding)
-                return new PrimitiveEncoding(tagClass, tagNo, GetDerTime());
-
-            return new PrimitiveEncoding(tagClass, tagNo, m_contents);
+            return new PrimitiveEncoding(tagClass, tagNo, GetContents(encoding));
         }
 
         protected override bool Asn1Equals(Asn1Object asn1Object)
@@ -340,12 +122,17 @@ namespace Org.BouncyCastle.Asn1
             if (!(asn1Object is Asn1GeneralizedTime that))
                 return false;
 
-            return Arrays.AreEqual(m_contents, that.m_contents);
+            // TODO Performance
+            return Arrays.AreEqual(
+                this.GetContents(Asn1OutputStream.EncodingDer),
+                that.GetContents(Asn1OutputStream.EncodingDer));
         }
 
         protected override int Asn1GetHashCode()
         {
-            return Arrays.GetHashCode(m_contents);
+            // TODO Performance
+            return Arrays.GetHashCode(
+                this.GetContents(Asn1OutputStream.EncodingDer));
         }
 
         internal static Asn1GeneralizedTime CreatePrimitive(byte[] contents)
@@ -353,86 +140,123 @@ namespace Org.BouncyCastle.Asn1
             return new Asn1GeneralizedTime(contents);
         }
 
-        internal byte[] GetDerTime()
+        private static DateTime FromString(string s)
         {
-            if (m_contents[m_contents.Length - 1] != 'Z')
-            {
-                return m_contents; // TODO: is there a better way?
-            }
+            if (s.Length < 10)
+                throw new FormatException();
 
-            if (!HasMinutes())
-            {
-                byte[] derTime = new byte[m_contents.Length + 4];
+            s = s.Replace(',', '.');
 
-                Array.Copy(m_contents, 0, derTime, 0, m_contents.Length - 1);
-                Array.Copy(Strings.ToByteArray("0000Z"), 0, derTime, m_contents.Length - 1, 5);
-
-                return derTime;
-            }
-            else if (!HasSeconds())
+            if (Platform.EndsWith(s, "Z"))
             {
-                byte[] derTime = new byte[m_contents.Length + 2];
+                switch (s.Length)
+                {
+                case 11: return ParseUtc(s, @"yyyyMMddHH\Z");
+                case 13: return ParseUtc(s, @"yyyyMMddHHmm\Z");
+                case 15: return ParseUtc(s, @"yyyyMMddHHmmss\Z");
+                case 17: return ParseUtc(s, @"yyyyMMddHHmmss.f\Z");
+                case 18: return ParseUtc(s, @"yyyyMMddHHmmss.ff\Z");
+                case 19: return ParseUtc(s, @"yyyyMMddHHmmss.fff\Z");
+                case 20: return ParseUtc(s, @"yyyyMMddHHmmss.ffff\Z");
+                case 21: return ParseUtc(s, @"yyyyMMddHHmmss.fffff\Z");
+                case 22: return ParseUtc(s, @"yyyyMMddHHmmss.ffffff\Z");
+                case 23: return ParseUtc(s, @"yyyyMMddHHmmss.fffffff\Z");
+                default:
+                    throw new FormatException();
+                }
+            }
 
-                Array.Copy(m_contents, 0, derTime, 0, m_contents.Length - 1);
-                Array.Copy(Strings.ToByteArray("00Z"), 0, derTime, m_contents.Length - 1, 3);
+            int signIndex = IndexOfSign(s, System.Math.Max(10, s.Length - 5));
 
-                return derTime;
-            }
-            else if (HasFractionalSeconds())
+            if (signIndex < 0)
             {
-                int ind = m_contents.Length - 2;
-                while (ind > 0 && m_contents[ind] == '0')
+                switch (s.Length)
                 {
-                    ind--;
+                case 10: return ParseLocal(s, @"yyyyMMddHH");
+                case 12: return ParseLocal(s, @"yyyyMMddHHmm");
+                case 14: return ParseLocal(s, @"yyyyMMddHHmmss");
+                case 16: return ParseLocal(s, @"yyyyMMddHHmmss.f");
+                case 17: return ParseLocal(s, @"yyyyMMddHHmmss.ff");
+                case 18: return ParseLocal(s, @"yyyyMMddHHmmss.fff");
+                case 19: return ParseLocal(s, @"yyyyMMddHHmmss.ffff");
+                case 20: return ParseLocal(s, @"yyyyMMddHHmmss.fffff");
+                case 21: return ParseLocal(s, @"yyyyMMddHHmmss.ffffff");
+                case 22: return ParseLocal(s, @"yyyyMMddHHmmss.fffffff");
+                default:
+                    throw new FormatException();
                 }
+            }
 
-                if (m_contents[ind] == '.')
-                {
-                    byte[] derTime = new byte[ind + 1];
-
-                    Array.Copy(m_contents, 0, derTime, 0, ind);
-                    derTime[ind] = (byte)'Z';
-
-                    return derTime;
-                }
-                else
+            if (signIndex == s.Length - 5)
+            {
+                switch (s.Length)
                 {
-                    byte[] derTime = new byte[ind + 2];
-
-                    Array.Copy(m_contents, 0, derTime, 0, ind + 1);
-                    derTime[ind + 1] = (byte)'Z';
-
-                    return derTime;
+                case 15: return ParseTimeZone(s, @"yyyyMMddHHzzz");
+                case 17: return ParseTimeZone(s, @"yyyyMMddHHmmzzz");
+                case 19: return ParseTimeZone(s, @"yyyyMMddHHmmsszzz");
+                case 21: return ParseTimeZone(s, @"yyyyMMddHHmmss.fzzz");
+                case 22: return ParseTimeZone(s, @"yyyyMMddHHmmss.ffzzz");
+                case 23: return ParseTimeZone(s, @"yyyyMMddHHmmss.fffzzz");
+                case 24: return ParseTimeZone(s, @"yyyyMMddHHmmss.ffffzzz");
+                case 25: return ParseTimeZone(s, @"yyyyMMddHHmmss.fffffzzz");
+                case 26: return ParseTimeZone(s, @"yyyyMMddHHmmss.ffffffzzz");
+                case 27: return ParseTimeZone(s, @"yyyyMMddHHmmss.fffffffzzz");
+                default:
+                    throw new FormatException();
                 }
             }
-            else
+
+            if (signIndex == s.Length - 3)
             {
-                return m_contents;
+                switch (s.Length)
+                {
+                case 13: return ParseTimeZone(s, @"yyyyMMddHHzz");
+                case 15: return ParseTimeZone(s, @"yyyyMMddHHmmzz");
+                case 17: return ParseTimeZone(s, @"yyyyMMddHHmmsszz");
+                case 19: return ParseTimeZone(s, @"yyyyMMddHHmmss.fzz");
+                case 20: return ParseTimeZone(s, @"yyyyMMddHHmmss.ffzz");
+                case 21: return ParseTimeZone(s, @"yyyyMMddHHmmss.fffzz");
+                case 22: return ParseTimeZone(s, @"yyyyMMddHHmmss.ffffzz");
+                case 23: return ParseTimeZone(s, @"yyyyMMddHHmmss.fffffzz");
+                case 24: return ParseTimeZone(s, @"yyyyMMddHHmmss.ffffffzz");
+                case 25: return ParseTimeZone(s, @"yyyyMMddHHmmss.fffffffzz");
+                default:
+                    throw new FormatException();
+                }
             }
+
+            throw new FormatException();
         }
 
-        private static string FString(int count)
+        private static int IndexOfSign(string s, int startIndex)
         {
-            StringBuilder sb = new StringBuilder();
-            for (int i = 0; i < count; ++i)
+            int index = Platform.IndexOf(s, '+', startIndex);
+            if (index < 0)
             {
-                sb.Append('f');
+                index = Platform.IndexOf(s, '-', startIndex);
             }
-            return sb.ToString();
+            return index;
         }
 
-        private static DateTime ParseDateString(string s, string format, bool makeUniversal)
+        private static DateTime ParseLocal(string s, string format)
         {
-            DateTimeStyles dateTimeStyles = DateTimeStyles.None;
-            if (Platform.EndsWith(format, "Z"))
-            {
-                dateTimeStyles |= DateTimeStyles.AdjustToUniversal;
-                dateTimeStyles |= DateTimeStyles.AssumeUniversal;
-            }
+            return DateTime.ParseExact(s, format, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AssumeLocal);
+        }
 
-            DateTime dt = DateTime.ParseExact(s, format, DateTimeFormatInfo.InvariantInfo, dateTimeStyles);
+        private static DateTime ParseTimeZone(string s, string format)
+        {
+            return DateTime.ParseExact(s, format, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AdjustToUniversal);
+        }
+
+        private static DateTime ParseUtc(string s, string format)
+        {
+            return DateTime.ParseExact(s, format, DateTimeFormatInfo.InvariantInfo,
+                DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
+        }
 
-            return makeUniversal ? dt.ToUniversalTime() : dt;
+        private static string ToStringCanonical(DateTime dateTime)
+        {
+            return dateTime.ToUniversalTime().ToString(@"yyyyMMddHHmmss.FFFFFFFK", DateTimeFormatInfo.InvariantInfo);
         }
     }
 }
diff --git a/crypto/src/asn1/Asn1UtcTime.cs b/crypto/src/asn1/Asn1UtcTime.cs
index 478b5c485..bd06a3258 100644
--- a/crypto/src/asn1/Asn1UtcTime.cs
+++ b/crypto/src/asn1/Asn1UtcTime.cs
@@ -4,6 +4,7 @@ using System.IO;
 using System.Text;
 
 using Org.BouncyCastle.Utilities;
+using Org.BouncyCastle.Utilities.Date;
 
 namespace Org.BouncyCastle.Asn1
 {
@@ -69,10 +70,7 @@ namespace Org.BouncyCastle.Asn1
 
 		public Asn1UtcTime(string timeString)
 		{
-			if (timeString == null)
-				throw new ArgumentNullException(nameof(timeString));
-
-			m_timeString = timeString;
+			m_timeString = timeString ?? throw new ArgumentNullException(nameof(timeString));
 
 			try
 			{
@@ -88,8 +86,7 @@ namespace Org.BouncyCastle.Asn1
         [Obsolete("Use `Asn1UtcTime(DateTime, int)' instead")]
 		public Asn1UtcTime(DateTime dateTime)
 		{
-            DateTime utc = dateTime.ToUniversalTime();
-            dateTime = new DateTime(utc.Year, utc.Month, utc.Day, utc.Hour, utc.Minute, utc.Second, DateTimeKind.Utc);
+            dateTime = DateTimeUtilities.WithPrecisionSecond(dateTime.ToUniversalTime());
 
             m_dateTime = dateTime;
             m_dateTimeLocked = true;
@@ -98,8 +95,7 @@ namespace Org.BouncyCastle.Asn1
 
         public Asn1UtcTime(DateTime dateTime, int twoDigitYearMax)
         {
-            DateTime utc = dateTime.ToUniversalTime();
-            dateTime = new DateTime(utc.Year, utc.Month, utc.Day, utc.Hour, utc.Minute, utc.Second, DateTimeKind.Utc);
+            dateTime = DateTimeUtilities.WithPrecisionSecond(dateTime.ToUniversalTime());
 
             Validate(dateTime, twoDigitYearMax);
 
@@ -220,11 +216,9 @@ namespace Org.BouncyCastle.Asn1
                 return DateTime.ParseExact(s, @"yyMMddHHmmss\Z", provider,
                     DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
             case 15:
-                return DateTime.ParseExact(s, @"yyMMddHHmmzzz", provider,
-                    DateTimeStyles.AdjustToUniversal);
+                return DateTime.ParseExact(s, @"yyMMddHHmmzzz", provider, DateTimeStyles.AdjustToUniversal);
             case 17:
-                return DateTime.ParseExact(s, @"yyMMddHHmmsszzz", provider,
-                    DateTimeStyles.AdjustToUniversal);
+                return DateTime.ParseExact(s, @"yyMMddHHmmsszzz", provider, DateTimeStyles.AdjustToUniversal);
             default:
                 throw new FormatException();
             }
diff --git a/crypto/src/asn1/DerGeneralizedTime.cs b/crypto/src/asn1/DerGeneralizedTime.cs
index 0386ecb02..3d814de30 100644
--- a/crypto/src/asn1/DerGeneralizedTime.cs
+++ b/crypto/src/asn1/DerGeneralizedTime.cs
@@ -5,29 +5,30 @@ namespace Org.BouncyCastle.Asn1
     public class DerGeneralizedTime
         : Asn1GeneralizedTime
     {
-        public DerGeneralizedTime(byte[] time)
-            : base(time)
+        public DerGeneralizedTime(string timeString)
+            : base(timeString)
         {
         }
 
-        public DerGeneralizedTime(DateTime time)
-            : base(time)
+        public DerGeneralizedTime(DateTime dateTime)
+            : base(dateTime)
         {
         }
 
-        public DerGeneralizedTime(string time)
-            : base(time)
+        internal DerGeneralizedTime(byte[] contents)
+            : base(contents)
         {
         }
 
         internal override IAsn1Encoding GetEncoding(int encoding)
         {
-            return new PrimitiveEncoding(Asn1Tags.Universal, Asn1Tags.GeneralizedTime, GetDerTime());
+            return new PrimitiveEncoding(Asn1Tags.Universal, Asn1Tags.GeneralizedTime,
+                GetContents(Asn1OutputStream.EncodingDer));
         }
 
         internal override IAsn1Encoding GetEncodingImplicit(int encoding, int tagClass, int tagNo)
         {
-            return new PrimitiveEncoding(tagClass, tagNo, GetDerTime());
+            return new PrimitiveEncoding(tagClass, tagNo, GetContents(Asn1OutputStream.EncodingDer));
         }
     }
 }
diff --git a/crypto/src/asn1/cms/Time.cs b/crypto/src/asn1/cms/Time.cs
index f054e3809..7b6b49c30 100644
--- a/crypto/src/asn1/cms/Time.cs
+++ b/crypto/src/asn1/cms/Time.cs
@@ -101,7 +101,10 @@ namespace Org.BouncyCastle.Asn1.Cms
             if (m_timeObject is Asn1UtcTime utcTime)
                 return utcTime.ToDateTime(2049).ToString(@"yyyyMMddHHmmssK", DateTimeFormatInfo.InvariantInfo);
 
-            return ((Asn1GeneralizedTime)m_timeObject).GetTime();
+            if (m_timeObject is Asn1GeneralizedTime generalizedTime)
+                return generalizedTime.ToDateTime().ToString(@"yyyyMMddHHmmss.FFFFFFFK", DateTimeFormatInfo.InvariantInfo);
+
+            throw new InvalidOperationException();
         }
     }
 }
diff --git a/crypto/src/asn1/util/Asn1Dump.cs b/crypto/src/asn1/util/Asn1Dump.cs
index 6c005f512..3a19f1276 100644
--- a/crypto/src/asn1/util/Asn1Dump.cs
+++ b/crypto/src/asn1/util/Asn1Dump.cs
@@ -214,7 +214,7 @@ namespace Org.BouncyCastle.Asn1.Utilities
             else if (obj is Asn1GeneralizedTime generalizedTime)
             {
                 buf.Append(indent);
-                buf.AppendLine("GeneralizedTime(" + generalizedTime.GetTime() + ")");
+                buf.AppendLine("GeneralizedTime(" + generalizedTime.TimeString + ")");
             }
             else if (obj is DerEnumerated en)
             {
diff --git a/crypto/src/asn1/x509/Time.cs b/crypto/src/asn1/x509/Time.cs
index 7f2d43315..e03055f6d 100644
--- a/crypto/src/asn1/x509/Time.cs
+++ b/crypto/src/asn1/x509/Time.cs
@@ -102,7 +102,10 @@ namespace Org.BouncyCastle.Asn1.X509
             if (m_timeObject is Asn1UtcTime utcTime)
                 return utcTime.ToDateTime(2049).ToString(@"yyyyMMddHHmmssK", DateTimeFormatInfo.InvariantInfo);
 
-            return ((Asn1GeneralizedTime)m_timeObject).GetTime();
+            if (m_timeObject is Asn1GeneralizedTime generalizedTime)
+                return generalizedTime.ToDateTime().ToString(@"yyyyMMddHHmmss.FFFFFFFK", DateTimeFormatInfo.InvariantInfo);
+
+            throw new InvalidOperationException();
         }
     }
 }
diff --git a/crypto/src/tsp/TimeStampTokenGenerator.cs b/crypto/src/tsp/TimeStampTokenGenerator.cs
index 9e6a21f9c..930463ca3 100644
--- a/crypto/src/tsp/TimeStampTokenGenerator.cs
+++ b/crypto/src/tsp/TimeStampTokenGenerator.cs
@@ -16,6 +16,7 @@ using Org.BouncyCastle.Crypto.Operators;
 using Org.BouncyCastle.Math;
 using Org.BouncyCastle.Utilities;
 using Org.BouncyCastle.Utilities.Collections;
+using Org.BouncyCastle.Utilities.Date;
 using Org.BouncyCastle.X509;
 
 namespace Org.BouncyCastle.Tsp
@@ -334,15 +335,7 @@ namespace Org.BouncyCastle.Tsp
                 respExtensions = extGen.Generate();
             }
 
-            Asn1GeneralizedTime timeStampTime;
-            if (resolution == Resolution.R_SECONDS)
-            {
-                timeStampTime = new Asn1GeneralizedTime(genTime);
-            }
-            else
-            {
-                timeStampTime = CreateGeneralizedTime(genTime);
-            } 
+            var timeStampTime = new Asn1GeneralizedTime(WithResolution(genTime, resolution));
 
             TstInfo tstInfo = new TstInfo(tsaPolicy, messageImprint,
                 new DerInteger(serialNumber), timeStampTime, accuracy,
@@ -385,53 +378,21 @@ namespace Org.BouncyCastle.Tsp
             //}
         }
 
-        private Asn1GeneralizedTime CreateGeneralizedTime(DateTime genTime)
+        private static DateTime WithResolution(DateTime dateTime, Resolution resolution)
         {
-            string format = "yyyyMMddHHmmss.fff";
-           
-            StringBuilder sBuild = new StringBuilder(genTime.ToString(format));
-            int dotIndex = sBuild.ToString().IndexOf(".");
-
-            if (dotIndex <0)
-            {
-                sBuild.Append("Z");
-                return new Asn1GeneralizedTime(sBuild.ToString());
-            }
-
-            switch(resolution)
+            switch (resolution)
             {
+            case Resolution.R_SECONDS:
+                return DateTimeUtilities.WithPrecisionSecond(dateTime);
             case Resolution.R_TENTHS_OF_SECONDS:
-                if (sBuild.Length > dotIndex + 2)
-                {
-                    sBuild.Remove(dotIndex + 2, sBuild.Length-(dotIndex+2));
-                }
-                break;
+                return DateTimeUtilities.WithPrecisionDecisecond(dateTime);
             case Resolution.R_HUNDREDTHS_OF_SECONDS:
-                if (sBuild.Length > dotIndex + 3)
-                {
-                    sBuild.Remove(dotIndex + 3, sBuild.Length-(dotIndex+3));
-                }
-                break;
-
-
-            case Resolution.R_SECONDS:
+                return DateTimeUtilities.WithPrecisionCentisecond(dateTime);
             case Resolution.R_MILLISECONDS:
-                // do nothing.
-                break;
-            }
-
-            while (sBuild[sBuild.Length - 1] == '0')
-            {
-                sBuild.Remove(sBuild.Length - 1,1);
-            }
-
-            if (sBuild.Length - 1 == dotIndex)
-            {
-                sBuild.Remove(sBuild.Length - 1, 1);
+                return DateTimeUtilities.WithPrecisionMillisecond(dateTime);
+            default:
+                throw new InvalidOperationException();
             }
-
-            sBuild.Append("Z");
-            return new Asn1GeneralizedTime(sBuild.ToString());
         }
 
         private class TableGen
diff --git a/crypto/src/tsp/TimeStampTokenInfo.cs b/crypto/src/tsp/TimeStampTokenInfo.cs
index cdef826bc..b9f0e3195 100644
--- a/crypto/src/tsp/TimeStampTokenInfo.cs
+++ b/crypto/src/tsp/TimeStampTokenInfo.cs
@@ -8,10 +8,27 @@ namespace Org.BouncyCastle.Tsp
 {
 	public class TimeStampTokenInfo
 	{
-		private TstInfo		tstInfo;
+		private static TstInfo ParseTstInfo(byte[] tstInfoEncoding)
+		{
+            try
+            {
+                return TstInfo.GetInstance(tstInfoEncoding);
+            }
+            catch (Exception e)
+            {
+                throw new TspException("unable to parse TstInfo encoding: " + e.Message);
+            }
+        }
+
+        private TstInfo		tstInfo;
 		private DateTime	genTime;
 
-		public TimeStampTokenInfo(
+        public TimeStampTokenInfo(byte[] tstInfoEncoding)
+			: this(ParseTstInfo(tstInfoEncoding))
+		{
+        }
+
+        public TimeStampTokenInfo(
 			TstInfo tstInfo)
 		{
 			this.tstInfo = tstInfo;
diff --git a/crypto/src/util/Platform.cs b/crypto/src/util/Platform.cs
index 118c29918..78ff6cee4 100644
--- a/crypto/src/util/Platform.cs
+++ b/crypto/src/util/Platform.cs
@@ -31,11 +31,26 @@ namespace Org.BouncyCastle.Utilities
             d.Dispose();
         }
 
+        internal static int IndexOf(string source, char value)
+        {
+            return InvariantCompareInfo.IndexOf(source, value, CompareOptions.Ordinal);
+        }
+
         internal static int IndexOf(string source, string value)
         {
             return InvariantCompareInfo.IndexOf(source, value, CompareOptions.Ordinal);
         }
 
+        internal static int IndexOf(string source, char value, int startIndex)
+        {
+            return InvariantCompareInfo.IndexOf(source, value, startIndex, CompareOptions.Ordinal);
+        }
+
+        internal static int IndexOf(string source, string value, int startIndex)
+        {
+            return InvariantCompareInfo.IndexOf(source, value, startIndex, CompareOptions.Ordinal);
+        }
+
         internal static int LastIndexOf(string source, string value)
         {
             return InvariantCompareInfo.LastIndexOf(source, value, CompareOptions.Ordinal);
diff --git a/crypto/src/util/date/DateTimeUtilities.cs b/crypto/src/util/date/DateTimeUtilities.cs
index 3660e29c2..c46cde8db 100644
--- a/crypto/src/util/date/DateTimeUtilities.cs
+++ b/crypto/src/util/date/DateTimeUtilities.cs
@@ -53,5 +53,31 @@ namespace Org.BouncyCastle.Utilities.Date
 		{
 			return DateTimeToUnixMs(DateTime.UtcNow);
 		}
-	}
+
+        public static DateTime WithPrecisionCentisecond(DateTime dateTime)
+        {
+            int millisecond = dateTime.Millisecond - (dateTime.Millisecond % 10);
+            return new DateTime(dateTime.Year, dateTime.Month, dateTime.Day,
+                dateTime.Hour, dateTime.Minute, dateTime.Second, millisecond, dateTime.Kind);
+        }
+
+        public static DateTime WithPrecisionDecisecond(DateTime dateTime)
+        {
+            int millisecond = dateTime.Millisecond - (dateTime.Millisecond % 100);
+            return new DateTime(dateTime.Year, dateTime.Month, dateTime.Day,
+                dateTime.Hour, dateTime.Minute, dateTime.Second, millisecond, dateTime.Kind);
+        }
+
+        public static DateTime WithPrecisionMillisecond(DateTime dateTime)
+        {
+            return new DateTime(dateTime.Year, dateTime.Month, dateTime.Day,
+                dateTime.Hour, dateTime.Minute, dateTime.Second, dateTime.Millisecond, dateTime.Kind);
+        }
+
+        public static DateTime WithPrecisionSecond(DateTime dateTime)
+        {
+            return new DateTime(dateTime.Year, dateTime.Month, dateTime.Day,
+                dateTime.Hour, dateTime.Minute, dateTime.Second, dateTime.Kind);
+        }
+    }
 }
diff --git a/crypto/test/src/asn1/test/GeneralizedTimeTest.cs b/crypto/test/src/asn1/test/GeneralizedTimeTest.cs
index 1cc1d8433..266ca2753 100644
--- a/crypto/test/src/asn1/test/GeneralizedTimeTest.cs
+++ b/crypto/test/src/asn1/test/GeneralizedTimeTest.cs
@@ -40,56 +40,6 @@ namespace Org.BouncyCastle.Asn1.Tests
             "20020122122220.0001+1000"
         };
 
-        private static readonly string[] output =
-        {
-            "20020122122220",
-            "20020122122220GMT+00:00",
-            "20020122122220GMT-10:00",
-            "20020122122220GMT+00:00",
-            "20020122122220.1",
-            "20020122122220.1GMT+00:00",
-            "20020122122220.1GMT-10:00",
-            "20020122122220.1GMT+00:00",
-            "20020122122220.01",
-            "20020122122220.01GMT+00:00",
-            "20020122122220.01GMT-10:00",
-            "20020122122220.01GMT+00:00",
-            "20020122122220.001",
-            "20020122122220.001GMT+00:00",
-            "20020122122220.001GMT-10:00",
-            "20020122122220.001GMT+00:00",
-            "20020122122220.0001",
-            "20020122122220.0001GMT+00:00",
-            "20020122122220.0001GMT-10:00",
-            "20020122122220.0001GMT+00:00",
-            "20020122122220.0001GMT+10:00"
-        };
-
-        private static readonly string[] zOutput =
-        {
-            "20020122122220Z",
-            "20020122122220Z",
-            "20020122222220Z",
-            "20020122122220Z",
-            "20020122122220Z",
-            "20020122122220Z",
-            "20020122222220Z",
-            "20020122122220Z",
-            "20020122122220Z",
-            "20020122122220Z",
-            "20020122222220Z",
-            "20020122122220Z",
-            "20020122122220Z",
-            "20020122122220Z",
-            "20020122222220Z",
-            "20020122122220Z",
-            "20020122122220Z",
-            "20020122122220Z",
-            "20020122222220Z",
-            "20020122122220Z",
-            "20020122022220Z"
-        };
-        
         private static readonly string[] mzOutput =
         {
             "20020122122220.000Z",
@@ -161,38 +111,6 @@ namespace Org.BouncyCastle.Asn1.Tests
         {
             for (int i = 0; i != input.Length; i++)
             {
-                string ii = input[i], oi = output[i];
-
-                Asn1GeneralizedTime t = new Asn1GeneralizedTime(ii);
-                DateTime dt = t.ToDateTime();
-                string st = t.GetTime();
-
-                if (oi.IndexOf('G') > 0)   // don't check local time the same way
-                {
-                    if (!st.Equals(oi))
-                    {
-                        Fail("failed GMT conversion test " + i);
-                    }
-
-                    string dts = dt.ToString(@"yyyyMMddHHmmss\Z");
-                    string zi = zOutput[i];
-                    if (!dts.Equals(zi))
-                    {
-                        Fail("failed date conversion test " + i);
-                    }
-                }
-                else
-                {
-                    string offset = CalculateGmtOffset(dt);
-                    if (!st.Equals(oi + offset))
-                    {
-                        Fail("failed conversion test " + i);
-                    }
-                }
-            }
-
-            for (int i = 0; i != input.Length; i++)
-            {
                 Asn1GeneralizedTime t = new Asn1GeneralizedTime(input[i]);
 
                 if (!t.ToDateTime().ToString(@"yyyyMMddHHmmss.fff\Z").Equals(mzOutput[i]))
@@ -209,7 +127,7 @@ namespace Org.BouncyCastle.Asn1.Tests
 
                 if (!AreEqual(t.GetEncoded(), new Asn1GeneralizedTime(derMzOutput[i]).GetEncoded()))
                 {
-                    Fail("der encoding wrong");
+                    Fail("DER encoding wrong");
                 }
             }
 
@@ -219,7 +137,7 @@ namespace Org.BouncyCastle.Asn1.Tests
 
                 if (!AreEqual(t.GetEncoded(), new Asn1GeneralizedTime(derTruncOutput[i]).GetEncoded()))
                 {
-                    Fail("trunc der encoding wrong");
+                    Fail("trunc DER encoding wrong");
                 }
             }
 
@@ -239,20 +157,14 @@ namespace Org.BouncyCastle.Asn1.Tests
                 IsTrue(Arrays.AreEqual(Hex.Decode("180f32303232303830393132313530305a"), der.GetEncoded(Asn1Encodable.Der)));
             }
 
-            {
-                // check an actual GMT string comes back untampered
-                Asn1GeneralizedTime time = new Asn1GeneralizedTime("20190704031318GMT+00:00");
-
-                IsTrue("20190704031318GMT+00:00".Equals(time.GetTime()));
-            }
-
             try
             {
-                new DerGeneralizedTime(new byte[0]);
+                new DerGeneralizedTime(string.Empty);
+                Fail("Expected exception");
             }
             catch (ArgumentException e)
             {
-                IsTrue(e.Message.StartsWith("GeneralizedTime string too short"));
+                IsTrue(e.Message.StartsWith("invalid date string"));
             }
 
             /*
@@ -277,39 +189,6 @@ namespace Org.BouncyCastle.Asn1.Tests
             }
         }
 
-        private string CalculateGmtOffset(DateTime date)
-        {
-            TimeZoneInfo timeZone = TimeZoneInfo.Local;
-            TimeSpan offset = timeZone.BaseUtcOffset;
-
-            char sign = '+';
-            if (offset.CompareTo(TimeSpan.Zero) < 0)
-            {
-                sign = '-';
-                offset = offset.Duration();
-            }
-
-            int hours = offset.Hours;
-            int minutes = offset.Minutes;
-
-            if (timeZone.SupportsDaylightSavingTime && timeZone.IsDaylightSavingTime(date))
-            {
-                hours += sign.Equals("+") ? 1 : -1;
-            }
-
-            return "GMT" + sign + Convert(hours) + ":" + Convert(minutes);
-        }
-
-        private string Convert(int time)
-        {
-            if (time < 10)
-            {
-                return "0" + time;
-            }
-
-            return time.ToString();
-        }
-
         [Test]
         public void TestFunction()
         {
diff --git a/crypto/test/src/tsp/test/NewTspTest.cs b/crypto/test/src/tsp/test/NewTspTest.cs
index c98e5d191..69bec7958 100644
--- a/crypto/test/src/tsp/test/NewTspTest.cs
+++ b/crypto/test/src/tsp/test/NewTspTest.cs
@@ -787,12 +787,12 @@ namespace Org.BouncyCastle.Tsp.Tests
 		}
 
 		private void resolutionTest(AsymmetricKeyParameter privateKey, X509Certificate cert,
-			IStore<X509Certificate> certs, Resolution resoution, string timeString)
+			IStore<X509Certificate> certs, Resolution resolution, string timeString)
 		{
 			TimeStampTokenGenerator tsTokenGen = new TimeStampTokenGenerator(
-			 privateKey, cert, TspAlgorithms.Sha1, "1.2");
+				privateKey, cert, TspAlgorithms.Sha1, "1.2");
 
-			tsTokenGen.Resolution = resoution;
+			tsTokenGen.Resolution = resolution;
 			tsTokenGen.SetCertificates(certs);
 
 			TimeStampRequestGenerator reqGen = new TimeStampRequestGenerator();
@@ -813,14 +813,14 @@ namespace Org.BouncyCastle.Tsp.Tests
 			tsToken = tsResp.TimeStampToken;
 			Assert.AreEqual("19700101000009Z", tsToken.TimeStampInfo.TstInfo.GenTime.TimeString);
 
-			if ((int)resoution > (int)Resolution.R_HUNDREDTHS_OF_SECONDS)
+			if ((int)resolution > (int)Resolution.R_HUNDREDTHS_OF_SECONDS)
 			{
 				tsResp = tsRespGen.Generate(request, new BigInteger("23"), UnixEpoch.AddMilliseconds(9990));
 				tsToken = tsResp.TimeStampToken;
 				Assert.AreEqual("19700101000009.99Z", tsToken.TimeStampInfo.TstInfo.GenTime.TimeString);
 			}
 
-			if ((int)resoution > (int)Resolution.R_TENTHS_OF_SECONDS)
+			if ((int)resolution > (int)Resolution.R_TENTHS_OF_SECONDS)
 			{
 				tsResp = tsRespGen.Generate(request, new BigInteger("23"), UnixEpoch.AddMilliseconds(9900));
 				tsToken = tsResp.TimeStampToken;
diff --git a/crypto/test/src/tsp/test/TimeStampTokenInfoTest.cs b/crypto/test/src/tsp/test/TimeStampTokenInfoTest.cs
index a1a7355e5..03f611beb 100644
--- a/crypto/test/src/tsp/test/TimeStampTokenInfoTest.cs
+++ b/crypto/test/src/tsp/test/TimeStampTokenInfoTest.cs
@@ -1,7 +1,5 @@
 using NUnit.Framework;
 
-using Org.BouncyCastle.Asn1;
-using Org.BouncyCastle.Asn1.Tsp;
 using Org.BouncyCastle.Math;
 using Org.BouncyCastle.Utilities;
 using Org.BouncyCastle.Utilities.Date;
@@ -31,7 +29,7 @@ namespace Org.BouncyCastle.Tsp.Tests
 		[Test]
 		public void TestTstInfo1()
 		{
-			TimeStampTokenInfo tstInfo = getTimeStampTokenInfo(tstInfo1);
+			TimeStampTokenInfo tstInfo = CreateTimeStampTokenInfo(tstInfo1);
 
 			//
 			// verify
@@ -60,7 +58,7 @@ namespace Org.BouncyCastle.Tsp.Tests
 	    [Test]
 		public void TestTstInfo2()
 		{
-			TimeStampTokenInfo tstInfo = getTimeStampTokenInfo(tstInfo2);
+			TimeStampTokenInfo tstInfo = CreateTimeStampTokenInfo(tstInfo2);
 
 			//
 			// verify
@@ -81,7 +79,9 @@ namespace Org.BouncyCastle.Tsp.Tests
 
 			Assert.AreEqual(tstInfo.Nonce, BigInteger.ValueOf(100));
 
-			Assert.IsTrue(Arrays.AreEqual(Hex.Decode("ffffffffffffffffffffffffffffffffffffffff"), tstInfo.GetMessageImprintDigest()));
+			Assert.IsTrue(Arrays.AreEqual(
+				Hex.Decode("ffffffffffffffffffffffffffffffffffffffff"),
+				tstInfo.GetMessageImprintDigest()));
 
 			Assert.IsTrue(Arrays.AreEqual(tstInfo2, tstInfo.GetEncoded()));
 		}
@@ -89,7 +89,7 @@ namespace Org.BouncyCastle.Tsp.Tests
 	    [Test]
 		public void TestTstInfo3()
 		{
-			TimeStampTokenInfo tstInfo = getTimeStampTokenInfo(tstInfo3);
+			TimeStampTokenInfo tstInfo = CreateTimeStampTokenInfo(tstInfo3);
 
 			//
 			// verify
@@ -122,7 +122,7 @@ namespace Org.BouncyCastle.Tsp.Tests
 		{
 			try
 			{
-				getTimeStampTokenInfo(tstInfoDudDate);
+				CreateTimeStampTokenInfo(tstInfoDudDate);
 
 				Assert.Fail("dud date not detected.");
 			}
@@ -132,12 +132,9 @@ namespace Org.BouncyCastle.Tsp.Tests
 			}
 		}
 
-		private TimeStampTokenInfo getTimeStampTokenInfo(
-			byte[] tstInfo)
+		private TimeStampTokenInfo CreateTimeStampTokenInfo(byte[] tstInfo)
 		{
-			return new TimeStampTokenInfo(
-				TstInfo.GetInstance(
-					Asn1Object.FromByteArray(tstInfo)));
+			return new TimeStampTokenInfo(tstInfo);
 		}
 	}
 }