cryptobyte: add support for UTCTime

Mostly cribbed from encoding/asn1.parseUTCTime.

Fixes golang/go#45411

Change-Id: I6c6ab2a2ad7c05a7f8bd81ecce4fcbb4e608e8db
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/273286
Trust: Roland Shoemaker <roland@golang.org>
Trust: Katie Hockman <katie@golang.org>
Run-TryBot: Roland Shoemaker <roland@golang.org>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Katie Hockman <katie@golang.org>
diff --git a/cryptobyte/asn1.go b/cryptobyte/asn1.go
index d3596ee..83c776d 100644
--- a/cryptobyte/asn1.go
+++ b/cryptobyte/asn1.go
@@ -117,6 +117,19 @@
 	})
 }
 
+// AddASN1UTCTime appends a DER-encoded ASN.1 UTCTime.
+func (b *Builder) AddASN1UTCTime(t time.Time) {
+	b.AddASN1(asn1.UTCTime, func(c *Builder) {
+		// As utilized by the X.509 profile, UTCTime can only
+		// represent the years 1950 through 2049.
+		if t.Year() < 1950 || t.Year() >= 2050 {
+			b.err = fmt.Errorf("cryptobyte: cannot represent %v as a UTCTime", t)
+			return
+		}
+		c.AddBytes([]byte(t.Format(defaultUTCTimeFormatStr)))
+	})
+}
+
 // AddASN1BitString appends a DER-encoded ASN.1 BIT STRING. This does not
 // support BIT STRINGs that are not a whole number of bytes.
 func (b *Builder) AddASN1BitString(data []byte) {
@@ -466,6 +479,45 @@
 	return true
 }
 
+const defaultUTCTimeFormatStr = "060102150405Z0700"
+
+// ReadASN1UTCTime decodes an ASN.1 UTCTime into out and advances.
+// It reports whether the read was successful.
+func (s *String) ReadASN1UTCTime(out *time.Time) bool {
+	var bytes String
+	if !s.ReadASN1(&bytes, asn1.UTCTime) {
+		return false
+	}
+	t := string(bytes)
+
+	formatStr := defaultUTCTimeFormatStr
+	var err error
+	res, err := time.Parse(formatStr, t)
+	if err != nil {
+		// Fallback to minute precision if we can't parse second
+		// precision. If we are following X.509 or X.690 we shouldn't
+		// support this, but we do.
+		formatStr = "0601021504Z0700"
+		res, err = time.Parse(formatStr, t)
+	}
+	if err != nil {
+		return false
+	}
+
+	if serialized := res.Format(formatStr); serialized != t {
+		return false
+	}
+
+	if res.Year() >= 2050 {
+		// UTCTime interprets the low order digits 50-99 as 1950-99.
+		// This only applies to its use in the X.509 profile.
+		// See https://tools.ietf.org/html/rfc5280#section-4.1.2.5.1
+		res = res.AddDate(-100, 0, 0)
+	}
+	*out = res
+	return true
+}
+
 // ReadASN1BitString decodes an ASN.1 BIT STRING into out and advances.
 // It reports whether the read was successful.
 func (s *String) ReadASN1BitString(out *encoding_asn1.BitString) bool {
diff --git a/cryptobyte/asn1_test.go b/cryptobyte/asn1_test.go
index 23fe4cc..8b0dbdb 100644
--- a/cryptobyte/asn1_test.go
+++ b/cryptobyte/asn1_test.go
@@ -311,6 +311,37 @@
 	}
 }
 
+func TestReadASN1UTCTime(t *testing.T) {
+	testData := []struct {
+		in  string
+		ok  bool
+		out time.Time
+	}{
+		{"000102030405Z", true, time.Date(2000, 01, 02, 03, 04, 05, 0, time.UTC)},
+		{"500102030405Z", true, time.Date(1950, 01, 02, 03, 04, 05, 0, time.UTC)},
+		{"490102030405Z", true, time.Date(2049, 01, 02, 03, 04, 05, 0, time.UTC)},
+		{"990102030405Z", true, time.Date(1999, 01, 02, 03, 04, 05, 0, time.UTC)},
+		{"250102030405Z", true, time.Date(2025, 01, 02, 03, 04, 05, 0, time.UTC)},
+		{"750102030405Z", true, time.Date(1975, 01, 02, 03, 04, 05, 0, time.UTC)},
+		{"000102030405+0905", true, time.Date(2000, 01, 02, 03, 04, 05, 0, time.FixedZone("", 9*60*60+5*60))},
+		{"000102030405-0905", true, time.Date(2000, 01, 02, 03, 04, 05, 0, time.FixedZone("", -9*60*60-5*60))},
+		{"0001020304Z", true, time.Date(2000, 01, 02, 03, 04, 0, 0, time.UTC)},
+		{"5001020304Z", true, time.Date(1950, 01, 02, 03, 04, 00, 0, time.UTC)},
+		{"0001020304+0905", true, time.Date(2000, 01, 02, 03, 04, 0, 0, time.FixedZone("", 9*60*60+5*60))},
+		{"0001020304-0905", true, time.Date(2000, 01, 02, 03, 04, 0, 0, time.FixedZone("", -9*60*60-5*60))},
+		{"000102030405Z0700", false, time.Time{}},
+		{"000102030405", false, time.Time{}},
+	}
+	for i, test := range testData {
+		in := String(append([]byte{byte(asn1.UTCTime), byte(len(test.in))}, test.in...))
+		var out time.Time
+		ok := in.ReadASN1UTCTime(&out)
+		if ok != test.ok || ok && !reflect.DeepEqual(out, test.out) {
+			t.Errorf("#%d: in.ReadASN1UTCTime() = %v, want %v; out = %q, want %q", i, ok, test.ok, out, test.out)
+		}
+	}
+}
+
 func TestReadASN1BitString(t *testing.T) {
 	testData := []struct {
 		in  []byte