Add a relaxed ASN.1 Time parser
Since some certs contain invalid times in the validity
This commit is contained in:
parent
af14fca70f
commit
786e9e3460
|
@ -0,0 +1,209 @@
|
|||
package ctwatch
|
||||
|
||||
import (
|
||||
"time"
|
||||
"strconv"
|
||||
"errors"
|
||||
"unicode"
|
||||
"encoding/asn1"
|
||||
)
|
||||
|
||||
func isDigit (b byte) bool {
|
||||
return unicode.IsDigit(rune(b))
|
||||
}
|
||||
|
||||
func bytesToInt (bytes []byte) (int, error) {
|
||||
return strconv.Atoi(string(bytes))
|
||||
}
|
||||
|
||||
func parseUTCTime (bytes []byte) (time.Time, error) {
|
||||
var err error
|
||||
var year, month, day int
|
||||
var hour, min, sec int
|
||||
var tz *time.Location
|
||||
|
||||
// YYMMDDhhmm
|
||||
if len(bytes) < 10 {
|
||||
return time.Time{}, errors.New("UTCTime is too short")
|
||||
}
|
||||
year, err = bytesToInt(bytes[0:2])
|
||||
if err != nil { return time.Time{}, errors.New("UTCTime contains invalid integer: " + err.Error()) }
|
||||
|
||||
month, err = bytesToInt(bytes[2:4])
|
||||
if err != nil { return time.Time{}, errors.New("UTCTime contains invalid integer: " + err.Error()) }
|
||||
|
||||
day, err = bytesToInt(bytes[4:6])
|
||||
if err != nil { return time.Time{}, errors.New("UTCTime contains invalid integer: " + err.Error()) }
|
||||
|
||||
hour, err = bytesToInt(bytes[6:8])
|
||||
if err != nil { return time.Time{}, errors.New("UTCTime contains invalid integer: " + err.Error()) }
|
||||
|
||||
min, err = bytesToInt(bytes[8:10])
|
||||
if err != nil { return time.Time{}, errors.New("UTCTime contains invalid integer: " + err.Error()) }
|
||||
|
||||
bytes = bytes[10:]
|
||||
|
||||
// (optional) ss
|
||||
if len(bytes) >= 2 && isDigit(bytes[0]) {
|
||||
sec, err = bytesToInt(bytes[0:2])
|
||||
if err != nil {
|
||||
return time.Time{}, errors.New("UTCTime contains invalid integer: " + err.Error())
|
||||
}
|
||||
bytes = bytes[2:]
|
||||
}
|
||||
|
||||
// timezone (required but allow it to be omitted, since this is a common error)
|
||||
if len(bytes) >= 1 {
|
||||
if bytes[0] == 'Z' {
|
||||
tz = time.UTC
|
||||
bytes = bytes[1:]
|
||||
} else if bytes[0] == '+' {
|
||||
// +hhmm
|
||||
if len(bytes) < 5 {
|
||||
return time.Time{}, errors.New("UTCTime positive timezone offset is too short")
|
||||
}
|
||||
tzHour, err := bytesToInt(bytes[1:3])
|
||||
if err != nil { return time.Time{}, errors.New("UTCTime contains invalid integer: " + err.Error()) }
|
||||
|
||||
tzMin, err := bytesToInt(bytes[3:5])
|
||||
if err != nil { return time.Time{}, errors.New("UTCTime contains invalid integer: " + err.Error()) }
|
||||
|
||||
tz = time.FixedZone("", tzHour * 3600 + tzMin * 60)
|
||||
bytes = bytes[5:]
|
||||
} else if bytes[0] == '-' {
|
||||
// -hhmm
|
||||
if len(bytes) < 5 {
|
||||
return time.Time{}, errors.New("UTCTime negative timezone offset is too short")
|
||||
}
|
||||
tzHour, err := bytesToInt(bytes[1:3])
|
||||
if err != nil { return time.Time{}, errors.New("UTCTime contains invalid integer: " + err.Error()) }
|
||||
|
||||
tzMin, err := bytesToInt(bytes[3:5])
|
||||
if err != nil { return time.Time{}, errors.New("UTCTime contains invalid integer: " + err.Error()) }
|
||||
|
||||
tz = time.FixedZone("", -1 * (tzHour * 3600 + tzMin * 60))
|
||||
bytes = bytes[5:]
|
||||
}
|
||||
} else {
|
||||
tz = time.UTC
|
||||
}
|
||||
|
||||
if len(bytes) > 0 {
|
||||
return time.Time{}, errors.New("UTCTime has trailing garbage")
|
||||
}
|
||||
|
||||
// https://tools.ietf.org/html/rfc5280#section-4.1.2.5.1
|
||||
if year >= 50 {
|
||||
year = 1900 + year
|
||||
} else {
|
||||
year = 2000 + year
|
||||
}
|
||||
|
||||
return time.Date(year, time.Month(month), day, hour, min, sec, 0, tz), nil
|
||||
}
|
||||
|
||||
func parseGeneralizedTime (bytes []byte) (time.Time, error) {
|
||||
var err error
|
||||
var year, month, day int
|
||||
var hour, min, sec, ms int
|
||||
var tz *time.Location
|
||||
|
||||
// YYYYMMDDHH
|
||||
if len(bytes) < 10 {
|
||||
return time.Time{}, errors.New("GeneralizedTime is too short")
|
||||
}
|
||||
year, err = bytesToInt(bytes[0:4])
|
||||
if err != nil { return time.Time{}, errors.New("GeneralizedTime contains invalid integer: " + err.Error()) }
|
||||
|
||||
month, err = bytesToInt(bytes[4:6])
|
||||
if err != nil { return time.Time{}, errors.New("GeneralizedTime contains invalid integer: " + err.Error()) }
|
||||
|
||||
day, err = bytesToInt(bytes[6:8])
|
||||
if err != nil { return time.Time{}, errors.New("GeneralizedTime contains invalid integer: " + err.Error()) }
|
||||
|
||||
hour, err = bytesToInt(bytes[8:10])
|
||||
if err != nil { return time.Time{}, errors.New("GeneralizedTime contains invalid integer: " + err.Error()) }
|
||||
|
||||
bytes = bytes[10:]
|
||||
|
||||
// (optional) MM
|
||||
if len(bytes) >= 2 && isDigit(bytes[0]) {
|
||||
min, err = bytesToInt(bytes[0:2])
|
||||
if err != nil {
|
||||
return time.Time{}, errors.New("GeneralizedTime contains invalid integer: " + err.Error())
|
||||
}
|
||||
bytes = bytes[2:]
|
||||
// (optional) SS
|
||||
if len(bytes) >= 2 && isDigit(bytes[0]) {
|
||||
sec, err = bytesToInt(bytes[0:2])
|
||||
if err != nil {
|
||||
return time.Time{}, errors.New("GeneralizedTime contains invalid integer: " + err.Error())
|
||||
}
|
||||
bytes = bytes[2:]
|
||||
// (optional) .fff
|
||||
if len(bytes) >= 1 && bytes[0] == '.' {
|
||||
if len(bytes) < 4 {
|
||||
return time.Time{}, errors.New("GeneralizedTime fractional seconds is too short")
|
||||
}
|
||||
ms, err = bytesToInt(bytes[1:4])
|
||||
if err != nil {
|
||||
return time.Time{}, errors.New("GeneralizedTime contains invalid integer: " + err.Error())
|
||||
}
|
||||
bytes = bytes[4:]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// timezone (Z or +hhmm or -hhmm or nothing)
|
||||
if len(bytes) >= 1 {
|
||||
if bytes[0] == 'Z' {
|
||||
bytes = bytes[1:]
|
||||
tz = time.UTC
|
||||
} else if bytes[0] == '+' {
|
||||
// +hhmm
|
||||
if len(bytes) < 5 {
|
||||
return time.Time{}, errors.New("GeneralizedTime positive timezone offset is too short")
|
||||
}
|
||||
tzHour, err := bytesToInt(bytes[1:3])
|
||||
if err != nil { return time.Time{}, errors.New("GeneralizedTime contains invalid integer: " + err.Error()) }
|
||||
|
||||
tzMin, err := bytesToInt(bytes[3:5])
|
||||
if err != nil { return time.Time{}, errors.New("GeneralizedTime contains invalid integer: " + err.Error()) }
|
||||
|
||||
tz = time.FixedZone("", tzHour * 3600 + tzMin * 60)
|
||||
bytes = bytes[5:]
|
||||
} else if bytes[0] == '-' {
|
||||
// -hhmm
|
||||
if len(bytes) < 5 {
|
||||
return time.Time{}, errors.New("GeneralizedTime negative timezone offset is too short")
|
||||
}
|
||||
tzHour, err := bytesToInt(bytes[1:3])
|
||||
if err != nil { return time.Time{}, errors.New("GeneralizedTime contains invalid integer: " + err.Error()) }
|
||||
|
||||
tzMin, err := bytesToInt(bytes[3:5])
|
||||
if err != nil { return time.Time{}, errors.New("GeneralizedTime contains invalid integer: " + err.Error()) }
|
||||
|
||||
tz = time.FixedZone("", -1 * (tzHour * 3600 + tzMin * 60))
|
||||
bytes = bytes[5:]
|
||||
}
|
||||
} else {
|
||||
tz = time.UTC
|
||||
}
|
||||
|
||||
if len(bytes) > 0 {
|
||||
return time.Time{}, errors.New("GeneralizedTime has trailing garbage")
|
||||
}
|
||||
|
||||
return time.Date(year, time.Month(month), day, hour, min, sec, ms * 1000 * 1000, tz), nil
|
||||
}
|
||||
|
||||
func decodeASN1Time (value *asn1.RawValue) (time.Time, error) {
|
||||
if !value.IsCompound && value.Class == 0 {
|
||||
if value.Tag == asn1.TagUTCTime {
|
||||
return parseUTCTime(value.Bytes)
|
||||
} else if value.Tag == asn1.TagGeneralizedTime {
|
||||
return parseGeneralizedTime(value.Bytes)
|
||||
}
|
||||
}
|
||||
return time.Time{}, errors.New("Not a time value")
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
package ctwatch
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type timeTest struct {
|
||||
in string
|
||||
ok bool
|
||||
out time.Time
|
||||
}
|
||||
|
||||
var utcTimeTests = []timeTest{
|
||||
{ "9502101525Z", true, time.Date(1995, time.February, 10, 15, 25, 0, 0, time.UTC) },
|
||||
{ "950210152542Z", true, time.Date(1995, time.February, 10, 15, 25, 42, 0, time.UTC) },
|
||||
{ "1502101525Z", true, time.Date(2015, time.February, 10, 15, 25, 0, 0, time.UTC) },
|
||||
{ "150210152542Z", true, time.Date(2015, time.February, 10, 15, 25, 42, 0, time.UTC) },
|
||||
{ "1502101525+1000", true, time.Date(2015, time.February, 10, 15, 25, 0, 0, time.FixedZone("", 10 * 3600)) },
|
||||
{ "1502101525-1000", true, time.Date(2015, time.February, 10, 15, 25, 0, 0, time.FixedZone("", -1 * (10 * 3600))) },
|
||||
{ "1502101525+1035", true, time.Date(2015, time.February, 10, 15, 25, 0, 0, time.FixedZone("", 10 * 3600 + 35 * 60)) },
|
||||
{ "1502101525-1035", true, time.Date(2015, time.February, 10, 15, 25, 0, 0, time.FixedZone("", -1 * (10 * 3600 + 35 * 60))) },
|
||||
{ "150210152542+1000", true, time.Date(2015, time.February, 10, 15, 25, 42, 0, time.FixedZone("", 10 * 3600)) },
|
||||
{ "150210152542-1000", true, time.Date(2015, time.February, 10, 15, 25, 42, 0, time.FixedZone("", -1 * (10 * 3600))) },
|
||||
{ "150210152542+1035", true, time.Date(2015, time.February, 10, 15, 25, 42, 0, time.FixedZone("", 10 * 3600 + 35 * 60)) },
|
||||
{ "150210152542-1035", true, time.Date(2015, time.February, 10, 15, 25, 42, 0, time.FixedZone("", -1 * (10 * 3600 + 35 * 60))) },
|
||||
{ "1502101525", true, time.Date(2015, time.February, 10, 15, 25, 0, 0, time.UTC) },
|
||||
{ "150210152542", true, time.Date(2015, time.February, 10, 15, 25, 42, 0, time.UTC) },
|
||||
{ "", false, time.Time{} },
|
||||
{ "123", false, time.Time{} },
|
||||
{ "150210152542-10", false, time.Time{} },
|
||||
{ "150210152542F", false, time.Time{} },
|
||||
{ "150210152542ZF", false, time.Time{} },
|
||||
}
|
||||
|
||||
func TestUTCTime(t *testing.T) {
|
||||
for i, test := range utcTimeTests {
|
||||
ret, err := parseUTCTime([]byte(test.in))
|
||||
if err != nil {
|
||||
if test.ok {
|
||||
t.Errorf("#%d: parseUTCTime(%q) failed with error %v", i, test.in, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !test.ok {
|
||||
t.Errorf("#%d: parseUTCTime(%q) succeeded, should have failed", i, test.in)
|
||||
continue
|
||||
}
|
||||
if !test.out.Equal(ret) {
|
||||
t.Errorf("#%d: parseUTCTime(%q) = %v, want %v", i, test.in, ret, test.out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var generalizedTimeTests = []timeTest{
|
||||
{ "2015021015", true, time.Date(2015, time.February, 10, 15, 0, 0, 0, time.UTC) },
|
||||
{ "201502101525", true, time.Date(2015, time.February, 10, 15, 25, 0, 0, time.UTC) },
|
||||
{ "20150210152542", true, time.Date(2015, time.February, 10, 15, 25, 42, 0, time.UTC) },
|
||||
{ "20150210152542.123", true, time.Date(2015, time.February, 10, 15, 25, 42, 123000000, time.UTC) },
|
||||
{ "20150210152542.12", false, time.Time{} },
|
||||
{ "20150210152542.1", false, time.Time{} },
|
||||
{ "20150210152542.", false, time.Time{} },
|
||||
|
||||
{ "2015021015Z", true, time.Date(2015, time.February, 10, 15, 0, 0, 0, time.UTC) },
|
||||
{ "201502101525Z", true, time.Date(2015, time.February, 10, 15, 25, 0, 0, time.UTC) },
|
||||
{ "20150210152542Z", true, time.Date(2015, time.February, 10, 15, 25, 42, 0, time.UTC) },
|
||||
{ "20150210152542.123Z", true, time.Date(2015, time.February, 10, 15, 25, 42, 123000000, time.UTC) },
|
||||
{ "20150210152542.12Z", false, time.Time{} },
|
||||
{ "20150210152542.1Z", false, time.Time{} },
|
||||
{ "20150210152542.Z", false, time.Time{} },
|
||||
|
||||
{ "2015021015+1000", true, time.Date(2015, time.February, 10, 15, 0, 0, 0, time.FixedZone("", 10 * 3600)) },
|
||||
{ "201502101525+1000", true, time.Date(2015, time.February, 10, 15, 25, 0, 0, time.FixedZone("", 10 * 3600)) },
|
||||
{ "20150210152542+1000", true, time.Date(2015, time.February, 10, 15, 25, 42, 0, time.FixedZone("", 10 * 3600)) },
|
||||
{ "20150210152542.123+1000", true, time.Date(2015, time.February, 10, 15, 25, 42, 123000000, time.FixedZone("", 10 * 3600)) },
|
||||
{ "20150210152542.12+1000", false, time.Time{} },
|
||||
{ "20150210152542.1+1000", false, time.Time{} },
|
||||
{ "20150210152542.+1000", false, time.Time{} },
|
||||
|
||||
{ "2015021015-0835", true, time.Date(2015, time.February, 10, 15, 0, 0, 0, time.FixedZone("", -1 * (8 * 3600 + 35 * 60))) },
|
||||
{ "201502101525-0835", true, time.Date(2015, time.February, 10, 15, 25, 0, 0, time.FixedZone("", -1 * (8 * 3600 + 35 * 60))) },
|
||||
{ "20150210152542-0835", true, time.Date(2015, time.February, 10, 15, 25, 42, 0, time.FixedZone("", -1 * (8 * 3600 + 35 * 60))) },
|
||||
{ "20150210152542.123-0835", true, time.Date(2015, time.February, 10, 15, 25, 42, 123000000, time.FixedZone("", -1 * (8 * 3600 + 35 * 60))) },
|
||||
{ "20150210152542.12-0835", false, time.Time{} },
|
||||
{ "20150210152542.1-0835", false, time.Time{} },
|
||||
{ "20150210152542.-0835", false, time.Time{} },
|
||||
|
||||
|
||||
{ "", false, time.Time{} },
|
||||
{ "123", false, time.Time{} },
|
||||
{ "2015021015+1000Z", false, time.Time{} },
|
||||
{ "2015021015x", false, time.Time{} },
|
||||
{ "201502101525Zf", false, time.Time{} },
|
||||
}
|
||||
|
||||
func TestGeneralizedTime(t *testing.T) {
|
||||
for i, test := range generalizedTimeTests {
|
||||
ret, err := parseGeneralizedTime([]byte(test.in))
|
||||
if err != nil {
|
||||
if test.ok {
|
||||
t.Errorf("#%d: parseGeneralizedTime(%q) failed with error %v", i, test.in, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !test.ok {
|
||||
t.Errorf("#%d: parseGeneralizedTime(%q) succeeded, should have failed", i, test.in)
|
||||
continue
|
||||
}
|
||||
if !test.out.Equal(ret) {
|
||||
t.Errorf("#%d: parseGeneralizedTime(%q) = %v, want %v", i, test.in, ret, test.out)
|
||||
}
|
||||
}
|
||||
}
|
17
x509.go
17
x509.go
|
@ -141,12 +141,25 @@ func ParseTBSCertificate (tbsBytes []byte) (*TBSCertificate, error) {
|
|||
}
|
||||
|
||||
func (tbs *TBSCertificate) ParseValidity () (*CertValidity, error) {
|
||||
var validity CertValidity
|
||||
if rest, err := asn1.Unmarshal(tbs.Validity.FullBytes, &validity); err != nil {
|
||||
var rawValidity struct {
|
||||
NotBefore asn1.RawValue
|
||||
NotAfter asn1.RawValue
|
||||
}
|
||||
if rest, err := asn1.Unmarshal(tbs.Validity.FullBytes, &rawValidity); err != nil {
|
||||
return nil, errors.New("failed to parse validity: " + err.Error())
|
||||
} else if len(rest) > 0 {
|
||||
return nil, fmt.Errorf("trailing data after validity: %v", rest)
|
||||
}
|
||||
|
||||
var validity CertValidity
|
||||
var err error
|
||||
if validity.NotBefore, err = decodeASN1Time(&rawValidity.NotBefore); err != nil {
|
||||
return nil, errors.New("failed to decode notBefore time: " + err.Error())
|
||||
}
|
||||
if validity.NotAfter, err = decodeASN1Time(&rawValidity.NotAfter); err != nil {
|
||||
return nil, errors.New("failed to decode notAfter time: " + err.Error())
|
||||
}
|
||||
|
||||
return &validity, nil
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue