// Copyright (c) 2022, 2024 blackshirt. All rights reserved. // Use of this source code is governed by a MIT License // that can be found in the LICENSE file. module asn1 import time // default_utctime_tag is the default tag of ASN.1 UTCTIME type. pub const default_utctime_tag = Tag{.universal, false, int(TagType.utctime)} // default_generalizedtime_tag is the default tag of ASN.1 GENERALIZEDTIME type. pub const default_generalizedtime_tag = Tag{.universal, false, int(TagType.generalizedtime)} const default_utctime_format = 'YYMMDDHHmmssZ' const default_genztime_format = 'YYYYMMDDHHmmssZ' // ASN.1 UNIVERSAL CLASS OF UTCTIME TYPE. // // For this time, UtcTime represented by simple string with format "YYMMDDhhmmssZ" // - the six digits YYMMDD where YY is the two low-order digits of the Christian year, // (RFC 5280 defines it as a range from 1950 to 2049 for X.509), MM is the month // (counting January as 01), and DD is the day of the month (01 to 31). // - the four digits hhmm where hh is hour (00 to 23) and mm is minutes (00 to 59); (SEE NOTE BELOW) // - the six digits hhmmss where hh and mm are as in above, and ss is seconds (00 to 59); // - the character Z; // - one of the characters + or -, followed by hhmm, where hh is hour and mm is minutes (NOT SUPPORTED) // // NOTE // ----- // - Restrictions employed by DER, the encoding shall terminate with "Z". // - The seconds element shall always be present, and DER (along with RFC 5280) specify that seconds must be present, // - Fractional seconds must not be present. // // TODO: // - check for invalid representation of date and hhmmss part. // - represented UtcTime in time.Time pub struct UtcTime { pub: value string } // new_utctime creates a new UtcTime element from string s. pub fn UtcTime.new(s string) !UtcTime { valid := validate_utctime(s)! if !valid { return error('UtcTime: fail on validate utctime') } return UtcTime{ value: s } } // from_time creates a new UtcTime element from standard `time.Time` in UTC time format. pub fn UtcTime.from_time(t time.Time) !UtcTime { // changes into utc time utime := t.local_to_utc() s := utime.custom_format(default_utctime_format) // 20241113060446+0 // Its rather a hack, not efieient as should be. // TODO: make it better str := s.split_any('+-') val := str[0] + 'Z' utc := UtcTime.new(val)! return utc } fn UtcTime.from_bytes(b []u8) !UtcTime { return UtcTime.new(b.bytestr())! } fn (utc UtcTime) str() string { if utc.value.len == 0 { return 'UtcTime ()' } return 'UtcTime (${utc.value})' } // tag retursn the tag of the UtcTime element. pub fn (utc UtcTime) tag() Tag { return default_utctime_tag } // payload returns the payload of the UtcTime element. pub fn (utc UtcTime) payload() ![]u8 { return utc.payload_with_rule(.der)! } // into_utctime turns this UtcTime into corresponding UTC time. pub fn (utc UtcTime) into_utctime() !time.Time { valid := validate_utctime(utc.value)! if !valid { return error('UtcTime: fail on validate utctime value') } st := time.parse_format(utc.value, default_utctime_format)! utime := st.local_to_utc() return utime } fn (utc UtcTime) payload_with_rule(rule EncodingRule) ![]u8 { valid := validate_utctime(utc.value)! if !valid { return error('UtcTime: fail on validate utctime') } return utc.value.bytes() } fn UtcTime.parse(mut p Parser) !UtcTime { tag := p.read_tag()! if !tag.equal(default_utctime_tag) { return error('Bad UtcTime tag') } length := p.read_length()! bytes := p.read_bytes(length)! res := UtcTime.from_bytes(bytes)! return res } // UtcTime.decode tries to decode bytes into UtcTime with DER rule fn UtcTime.decode(src []u8) !(UtcTime, int) { return UtcTime.decode_with_rule(src, .der)! } fn UtcTime.decode_with_rule(bytes []u8, rule EncodingRule) !(UtcTime, int) { tag, length_pos := Tag.decode_with_rule(bytes, 0, rule)! if !tag.equal(default_utctime_tag) { return error('Unexpected non-utctime tag') } length, content_pos := Length.decode_with_rule(bytes, length_pos, rule)! content := if length == 0 { []u8{} } else { if content_pos >= bytes.len || content_pos + length > bytes.len { return error('UtcTime: truncated payload bytes') } unsafe { bytes[content_pos..content_pos + length] } } utc := UtcTime.from_bytes(content)! next := content_pos + length return utc, next } // utility function for UtcTime // fn validate_utctime(s string) !bool { if !basic_utctime_check(s) { return false } // read contents src := s.bytes() mut pos := 0 mut year, mut month, mut day := u16(0), u8(0), u8(0) mut hour, mut minute, mut second := u8(0), u8(0), u8(0) // UtcTime only encodes times prior to 2050 year, pos = read_2_digits(src, pos)! year = u16(year) if year >= 50 { year = 1900 + year } else { year = 2000 + year } month, pos = read_2_digits(src, pos)! day, pos = read_2_digits(src, pos)! if !validate_date(year, month, day) { return false } // hhmmss parts hour, pos = read_2_digits(src, pos)! minute, pos = read_2_digits(src, pos)! second, pos = read_2_digits(src, pos)! if hour > 23 || minute > 59 || second > 59 { return false } // assert pos == src.len - 1 if src[pos] != 0x5A { return false } return true } fn basic_utctime_check(s string) bool { return s.len == 13 && valid_time_contents(s) } fn valid_time_contents(s string) bool { return s.ends_with('Z') && s.contains_any('0123456789') } // ASN.1 UNIVERSAL CLASS OF GENERALIZEDTIME TYPE. // // In DER Encoding scheme, GeneralizedTime should : // - The encoding shall terminate with a "Z" // - The seconds element shall always be present // - The fractional-seconds elements, if present, shall omit all trailing zeros; // - if the elements correspond to 0, they shall be wholly omitted, and the decimal point element also shall be omitted // // GeneralizedTime values MUST be: // - expressed in Greenwich Mean Time (Zulu) and MUST include seconds // (i.e., times are `YYYYMMDDHHMMSSZ`), even where the number of seconds // is zero. // - GeneralizedTime values MUST NOT include fractional seconds. pub struct GeneralizedTime { pub: value string } // GeneralizedTime.new creates a new GeneralizedTime from string s pub fn GeneralizedTime.new(s string) !GeneralizedTime { valid := validate_generalizedtime(s)! if !valid { return error('GeneralizedTime: failed on validate') } return GeneralizedTime{ value: s } } // from_time creates GeneralizedTime element from standard `time.Time` (as an UTC time). pub fn GeneralizedTime.from_time(t time.Time) !GeneralizedTime { u := t.local_to_utc() s := u.custom_format(default_genztime_format) // adds support directly from time.Time src := s.split_any('+-') val := src[0] + 'Z' gt := GeneralizedTime.new(val)! return gt } // into_utctime turns this GeneralizedTime into corresponding UTC time. pub fn (gt GeneralizedTime) into_utctime() !time.Time { valid := validate_generalizedtime(gt.value)! if !valid { return error('GeneralizedTime: fail on validate value') } st := time.parse_format(gt.value, default_genztime_format)! utime := st.local_to_utc() return utime } // The tag of GeneralizedTime element. pub fn (gt GeneralizedTime) tag() Tag { return default_generalizedtime_tag } // The payload of GeneralizedTime element. pub fn (gt GeneralizedTime) payload() ![]u8 { valid := validate_generalizedtime(gt.value)! if !valid { return error('GeneralizedTime: failed on validate') } return gt.value.bytes() } fn GeneralizedTime.from_bytes(b []u8) !GeneralizedTime { return GeneralizedTime.new(b.bytestr())! } // GeneralizedTime.parse tries to parse throught ongoing Parser into GeneralizedTime fn GeneralizedTime.parse(mut p Parser) !GeneralizedTime { tag := p.read_tag()! if !tag.equal(default_generalizedtime_tag) { return error('Bad GeneralizedTime tag') } length := p.read_length()! bytes := p.read_bytes(length)! res := GeneralizedTime.from_bytes(bytes)! return res } fn GeneralizedTime.decode(bytes []u8) !(GeneralizedTime, int) { return GeneralizedTime.decode_with_rule(bytes, .der)! } fn GeneralizedTime.decode_with_rule(bytes []u8, rule EncodingRule) !(GeneralizedTime, int) { tag, length_pos := Tag.decode_with_rule(bytes, 0, rule)! if !tag.equal(default_generalizedtime_tag) { return error('Get bad GeneralizedTime tag') } length, content_pos := Length.decode_with_rule(bytes, length_pos, rule)! content := if length == 0 { []u8{} } else { if content_pos >= bytes.len || content_pos + length > bytes.len { return error('GeneralizedTime: truncated payload bytes') } unsafe { bytes[content_pos..content_pos + length] } } gtc := GeneralizedTime.from_bytes(content)! next := content_pos + length return gtc, next } // utility function for GeneralizedTime // TODO: more clear and concise validation check fn min_generalizedtime_length(s string) bool { // minimum length without fractional element return s.len >= 15 } fn generalizedtime_contains_fraction(s string) bool { // contains '.' part return s.contains('.') } fn basic_generalizedtime_check(s string) bool { return min_generalizedtime_length(s) && valid_time_contents(s) } fn validate_generalizedtime(s string) !bool { if !basic_generalizedtime_check(s) { return false } // read contents src := s.bytes() mut pos := 0 mut year, mut month, mut day := u16(0), u8(0), u8(0) mut hour, mut minute, mut second := u8(0), u8(0), u8(0) // Generalized time format was "YYYYMMDDhhmmssZ" // TODO: support for second fractions part year, pos = read_4_digits(src, pos)! // year = u16(year) month, pos = read_2_digits(src, pos)! day, pos = read_2_digits(src, pos)! if !validate_date(year, month, day) { return false } // hhmmss parts hour, pos = read_2_digits(src, pos)! minute, pos = read_2_digits(src, pos)! second, pos = read_2_digits(src, pos)! if hour > 23 || minute > 59 || second > 59 { return false } // assert pos == src.len - 1 if src[pos] != 0x5A { return false } return true }