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 @@ func (b *Builder) AddASN1GeneralizedTime(t time.Time) { }) } +// 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 @@ func (s *String) ReadASN1GeneralizedTime(out *time.Time) bool { 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 TestReadASN1GeneralizedTime(t *testing.T) { } } +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