# Copyright (c) 2021 by the Zeek Project. See LICENSE for details.

module ASN1;

###############################################################################
# ASN.1 structure decoding
#
# A Layman's Guide to a Subset of ASN.1, BER, and DER
#   http://luca.ntop.org/Teaching/Appunti/asn1.html
#
# ASN.1 Tutorial from Computer Networks and Open Systems:
#   An Application Development Perspective
#   https://www.obj-sys.com/asn1tutorial/asn1only.html
#
# The ASN1JS tool (http://lapo.it/asn1js and https://github.com/lapo-luchini/asn1js)
#   is invaluable in debugging ASN.1
###############################################################################

import spicy;

#- ASN.1 data types ----------------------------------------------------------
# https://www.obj-sys.com/asn1tutorial/node124.html
# https://www.obj-sys.com/asn1tutorial/node10.html

public type ASN1Type = enum {
  Boolean = 1,
  Integer = 2,
  BitString = 3,
  OctetString = 4,
  NullVal = 5,
  ObjectIdentifier = 6,
  ObjectDescriptor = 7,
  InstanceOf = 8,
  Real = 9,
  Enumerated = 10,
  EmbeddedPDV = 11,
  UTF8String = 12,
  RelativeOID = 13,
  Sequence = 16,
  Set = 17,
  NumericString = 18,
  PrintableString = 19,
  TeletextString = 20,
  VideotextString = 21,
  IA5String = 22,
  UTCTime = 23,
  GeneralizedTime = 24,
  GraphicString = 25,
  VisibleString = 26,
  GeneralString = 27,
  UniversalString = 28,
  CharacterString = 29,
  BMPString = 30
};

#- ASN.1 data classes --------------------------------------------------------

public type ASN1Class = enum {
  Universal = 0,
  Application = 1,
  ContextSpecific = 2,
  Private = 3
};

#- ASN.1 tag definition (including length) ------------------------------------

type LengthType = unit {
  var len: uint64;
  var tag_len: uint8;

  data : bitfield(8) {
    num: 0..6;
    islong: 7;
  };


  switch ( self.data.islong ) {
    0 -> : void {
      self.len = self.data.num;
      self.tag_len = 1;
    }
    1 -> : bytes &size=self.data.num
           &convert=$$.to_uint(spicy::ByteOrder::Network) {
      self.len = $$;
      self.tag_len = self.data.num + 1;
    }
  };
};

type ASN1Tag = unit {
 : bitfield(8) {
    type_: 0..4 &convert=ASN1Type($$);
    constructed: 5 &convert=cast<bool>($$);
    class: 6..7 &convert=ASN1Class($$);
  };
};

#- ASN.1 bit string -----------------------------------------------------------
# https://www.obj-sys.com/asn1tutorial/node10.html

type ASN1BitString = unit(len: uint64, constructed: bool) {
  : uint8; # unused bits
  value_bits: bytes &size=(len - 1);

  # TODO - constructed form
  # https://github.com/zeek/spicy/issues/921
  # `bytes` needs << and >> support before we can implement complex bitstrings
  #
};

#- ASN.1 octet string ---------------------------------------------------------
# https://www.obj-sys.com/asn1tutorial/node10.html

type ASN1OctetString = unit(len: uint64, constructed: bool) {
  value: bytes &size = len;

  # TODO - constructed form
};

#- ASN.1 various string types -------------------------------------------------
# https://www.obj-sys.com/asn1tutorial/node124.html

type ASN1String = unit(tag: ASN1Tag, len: uint64) {
  var encoding: spicy::Charset;

  on %init {
    switch ( tag.type_ ) {
      # see "Restricted Character String Types" in
      # "Generic String Encoding Rules (GSER) for ASN.1 Types"
      # (https://datatracker.ietf.org/doc/html/rfc3641#section-3.2)

      case ASN1Type::PrintableString,
           ASN1Type::GeneralizedTime,
           ASN1Type::UTCTime: {
        self.encoding = spicy::Charset::ASCII;
      }

      case ASN1Type::UTF8String,
           ASN1Type::GeneralString,
           ASN1Type::CharacterString,
           ASN1Type::GraphicString,
           ASN1Type::IA5String,
           ASN1Type::NumericString,
           ASN1Type::TeletextString,
           ASN1Type::VideotextString,
           ASN1Type::VisibleString,
           # TODO: RFC3641 mentions special UTF-8 mapping rules for
           # BMPString and UniversalString. This *may* not be correct.
           ASN1Type::BMPString,
           ASN1Type::UniversalString: {
        self.encoding = spicy::Charset::UTF8;
      }
    }
  }

  value: ASN1OctetString(len, tag.constructed) &convert=$$.value.decode(self.encoding);
} &convert=self.value;

#- ASN.1 OID ------------------------------------------------------------------
# https://www.obj-sys.com/asn1tutorial/node124.html

type ASN1ObjectIdentifierNibble = unit {
  data : bitfield(8) {
    num: 0..6;
    more: 7;
  };
} &convert=self.data;

type ASN1ObjectIdentifier = unit(len: uint64) {
  var oidbytes: bytes;
  var temp: uint64;
  var oidstring: string;

  : uint8 if ( len >= 1 ) {
    self.temp = $$ / 40;
    self.oidbytes += ("%d" % (self.temp)).encode();
    self.temp = $$ % 40;
    self.oidbytes += (".%d" % (self.temp)).encode();
    self.temp = 0;
  }

  sublist: ASN1ObjectIdentifierNibble[len - 1] foreach {
    self.temp = ( self.temp<<7 ) | $$.num;
    if ( $$.more != 1 ) {
      self.oidbytes += (".%d" % (self.temp)).encode();
      self.temp = 0;
    }
  }

  on %done {
    self.oidstring = self.oidbytes.decode();
  }
};


#- ASN.1 message header (tag + length information) ----------------------------

public type ASN1Header = unit {
  tag: ASN1Tag;
  len: LengthType;
};

#- ASN.1 message body ---------------------------------------------------------

public type ASN1Body = unit(head: ASN1Header, recursive: bool) {
  switch ( head.tag.type_ ) {

    ASN1Type::Boolean -> bool_value: uint8 &convert=cast<bool>($$) &requires=head.len.len==1;

    ASN1Type::Integer,
    ASN1Type::Enumerated -> num_value: bytes &size=head.len.len
                                             &convert=$$.to_int(spicy::ByteOrder::Big);

    ASN1Type::NullVal -> null_value: bytes &size=0 &requires=head.len.len==0;

    ASN1Type::BitString -> bitstr_value: ASN1BitString(head.len.len, head.tag.constructed);

    ASN1Type::OctetString -> str_value: ASN1OctetString(head.len.len, head.tag.constructed)
                                        &convert=$$.value.decode(spicy::Charset::ASCII);

    ASN1Type::ObjectIdentifier -> str_value: ASN1ObjectIdentifier(head.len.len)
                                             &convert=$$.oidstring;

    ASN1Type::BMPString,
    ASN1Type::CharacterString,
    ASN1Type::GeneralizedTime,
    ASN1Type::GeneralString,
    ASN1Type::GraphicString,
    ASN1Type::IA5String,
    ASN1Type::NumericString,
    ASN1Type::PrintableString,
    ASN1Type::TeletextString,
    ASN1Type::UTCTime,
    ASN1Type::UTF8String,
    ASN1Type::VideotextString,
    ASN1Type::VisibleString,
    ASN1Type::UniversalString -> str_value: ASN1String(head.tag, head.len.len);

    ASN1Type::Sequence, ASN1Type::Set -> seq: ASN1SubMessages(head.len.len) if (recursive);

    # TODO: ASN1Type values not handled yet
    ASN1Type::ObjectDescriptor,
    ASN1Type::InstanceOf,
    ASN1Type::Real,
    ASN1Type::EmbeddedPDV,
    ASN1Type::RelativeOID -> unimplemented_value: bytes &size=head.len.len;

    # unknown (to me) ASN.1 enumeration, skip over silently
    * -> unimplemented_value: bytes &size=head.len.len;
  };
};

#- ASN.1 array of ASN.1 sequence/set sub-messages (up to msgLen bytes) --------

public type ASN1SubMessages = unit(msgLen: uint64) {
  submessages: ASN1Message(True)[] &eod;
} &size=msgLen;

#- ASN.1 message with header and body -----------------------------------------
# Universal or Application/ContextSpecific/Private
# - if Universal, body:ASN1Body is parsed
# - else, application_data:bytes stores data array

public type ASN1Message = unit(recursive: bool) {
  var application_id: int32;

  head: ASN1Header;
  switch ( self.head.tag.class ) {

    ASN1Class::Universal -> body: ASN1Body(self.head, recursive);

    ASN1Class::Application,
    ASN1Class::ContextSpecific,
    ASN1Class::Private -> application_data: bytes &size=self.head.len.len {
      self.application_id = cast<int32>(self.head.tag.type_);
    }

  };
};
