diff --git a/asn1time.go b/asn1time.go new file mode 100644 index 0000000..7b66d4f --- /dev/null +++ b/asn1time.go @@ -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") +} diff --git a/asn1time_test.go b/asn1time_test.go new file mode 100644 index 0000000..58ad9f0 --- /dev/null +++ b/asn1time_test.go @@ -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) + } + } +} diff --git a/x509.go b/x509.go index 45b0248..cc5484a 100644 --- a/x509.go +++ b/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 }