diff --git a/src/encoding/asn1/asn1.go b/src/encoding/asn1/asn1.go index 4e3f85de13..0b64f06d36 100644 --- a/src/encoding/asn1/asn1.go +++ b/src/encoding/asn1/asn1.go @@ -828,9 +828,18 @@ func parseField(v reflect.Value, bytes []byte, initOffset int, params fieldParam } // Special case for time: UTCTime and GeneralizedTime both map to the - // Go type time.Time. - if universalTag == TagUTCTime && t.tag == TagGeneralizedTime && t.class == ClassUniversal { - universalTag = TagGeneralizedTime + // Go type time.Time. getUniversalType returns the tag for UTCTime when + // it sees a time.Time, so if we see a different time type on the wire, + // or the field is tagged with a different type, we change the universal + // type to match. + if universalTag == TagUTCTime { + if t.class == ClassUniversal { + if t.tag == TagGeneralizedTime { + universalTag = t.tag + } + } else if params.timeType != 0 { + universalTag = params.timeType + } } if params.set { @@ -1103,6 +1112,13 @@ func setDefaultValue(v reflect.Value, params fieldParameters) (ok bool) { // numeric causes strings to be unmarshaled as ASN.1 NumericString values // utf8 causes strings to be unmarshaled as ASN.1 UTF8String values // +// When decoding an ASN.1 value with an IMPLICIT tag into a time.Time field, +// Unmarshal will default to a UTCTime, which doesn't support time zones or +// fractional seconds. To force usage of GeneralizedTime, use the following +// tag: +// +// generalized causes time.Times to be unmarshaled as ASN.1 GeneralizedTime values +// // If the type of the first field of a structure is RawContent then the raw // ASN1 contents of the struct will be stored in it. // diff --git a/src/encoding/asn1/asn1_test.go b/src/encoding/asn1/asn1_test.go index 60dae71df4..0597740bd5 100644 --- a/src/encoding/asn1/asn1_test.go +++ b/src/encoding/asn1/asn1_test.go @@ -1185,3 +1185,34 @@ func BenchmarkObjectIdentifierString(b *testing.B) { _ = oidPublicKeyRSA.String() } } + +func TestImplicitTypeRoundtrip(t *testing.T) { + type tagged struct { + IA5 string `asn1:"tag:1,ia5"` + Printable string `asn1:"tag:2,printable"` + UTF8 string `asn1:"tag:3,utf8"` + Numeric string `asn1:"tag:4,numeric"` + UTC time.Time `asn1:"tag:5,utc"` + Generalized time.Time `asn1:"tag:6,generalized"` + } + a := tagged{ + IA5: "ia5", + Printable: "printable", + UTF8: "utf8", + Numeric: "123 456", + UTC: time.Now().UTC().Truncate(time.Second), + Generalized: time.Now().UTC().Truncate(time.Second), + } + enc, err := Marshal(a) + if err != nil { + t.Fatalf("Marshal failed: %s", err) + } + var b tagged + if _, err := Unmarshal(enc, &b); err != nil { + t.Fatalf("Unmarshal failed: %s", err) + } + + if !reflect.DeepEqual(a, b) { + t.Fatalf("Unexpected diff after roundtripping struct\na: %#v\nb: %#v", a, b) + } +} diff --git a/src/encoding/asn1/marshal.go b/src/encoding/asn1/marshal.go index b9c0b8bce0..70e4fafc12 100644 --- a/src/encoding/asn1/marshal.go +++ b/src/encoding/asn1/marshal.go @@ -725,6 +725,7 @@ func makeField(v reflect.Value, params fieldParameters) (e encoder, err error) { // omitempty: causes empty slices to be skipped // printable: causes strings to be marshaled as ASN.1, PrintableString values // utf8: causes strings to be marshaled as ASN.1, UTF8String values +// numeric: causes strings to be marshaled as ASN.1, NumericString values // utc: causes time.Time to be marshaled as ASN.1, UTCTime values // generalized: causes time.Time to be marshaled as ASN.1, GeneralizedTime values func Marshal(val any) ([]byte, error) {