# --
# Copyright 2007 Nominet UK
# 
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# 
#     http://www.apache.org/licenses/LICENSE-2.0
# 
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ++
module Dnsruby
  class RR
    #  (RFC4034, section 3)
    # DNSSEC uses public key cryptography to sign and authenticate DNS
    # resource record sets (RRsets).  Digital signatures are stored in
    # RRSIG resource records and are used in the DNSSEC authentication
    # process described in [RFC4035].  A validator can use these RRSIG RRs
    # to authenticate RRsets from the zone.  The RRSIG RR MUST only be used
    # to carry verification material (digital signatures) used to secure
    # DNS operations.
    # 
    # An RRSIG record contains the signature for an RRset with a particular
    # name, class, and type.  The RRSIG RR specifies a validity interval
    # for the signature and uses the Algorithm, the Signer's Name, and the
    # Key Tag to identify the DNSKEY RR containing the public key that a
    # validator can use to verify the signature.
    class RRSIG < RR
      ClassValue = nil #:nodoc: all
      TypeValue = Types::RRSIG #:nodoc: all

      #  3.1.  RRSIG RDATA Wire Format
      # 
      #    The RDATA for an RRSIG RR consists of a 2 octet Type Covered field, a
      #    1 octet Algorithm field, a 1 octet Labels field, a 4 octet Original
      #    TTL field, a 4 octet Signature Expiration field, a 4 octet Signature
      #    Inception field, a 2 octet Key tag, the Signer's Name field, and the
      #    Signature field.
      # 
      #                         1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3
      #     0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
      #    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      #    |        Type Covered           |  Algorithm    |     Labels    |
      #    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      #    |                         Original TTL                          |
      #    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      #    |                      Signature Expiration                     |
      #    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      #    |                      Signature Inception                      |
      #    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      #    |            Key Tag            |                               /
      #    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+         Signer's Name         /
      #    /                                                               /
      #    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      #    /                                                               /
      #    /                            Signature                          /
      #    /                                                               /
      #    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

      # The type covered by this RRSIG
      attr_reader :type_covered
      # The algorithm used for this RRSIG
      # See Dnsruby::Algorithms for permitted values
      attr_reader :algorithm
      # The number of labels in the original RRSIG RR owner name
      # Can be used to determine if name was synthesised from a wildcard.
      attr_accessor :labels
      # The TTL of the covered RRSet as it appears in the authoritative zone
      attr_accessor :original_ttl
      # The signature expiration
      attr_accessor :expiration
      # The signature inception
      attr_accessor :inception
      # The key tag value of the DNSKEY RR that validates this signature
      attr_accessor :key_tag
      # identifies the owner name of the DNSKEY RR that a validator is
      # supposed to use to validate this signature
      attr_reader :signers_name

      # contains the cryptographic signature that covers
      # the RRSIG RDATA (excluding the Signature field) and the RRset
      # specified by the RRSIG owner name, RRSIG class, and RRSIG Type
      # Covered field
      attr_accessor :signature

      def init_defaults
        @algorithm=Algorithms.RSASHA1
        @type_covered = Types::A
        @original_ttl = 3600
        @inception = Time.now.to_i
        @expiration = Time.now.to_i
        @key_tag = 0
        @labels = 0
        self.signers_name="."
        @signature = "\0"
      end

      def algorithm=(a)
        if (a.instance_of?String)
          if (a.to_i > 0)
            a = a.to_i
          end
        end
        begin
          alg = Algorithms.new(a)
          @algorithm = alg
        rescue ArgumentError => e
          raise DecodeError.new(e)
        end
      end

      def type_covered=(t)
        begin
          type = Types.new(t)
          @type_covered = type
        rescue ArgumentError => e
          raise DecodeError.new(e)
        end
      end

      def signers_name=(s)
        begin
          name = Name.create(s)
          @signers_name = name
        rescue ArgumentError => e
          raise DecodeError.new(e)
        end
      end


      def from_data(data) #:nodoc: all
        type_covered, algorithm, @labels, @original_ttl, expiration, inception,
          @key_tag, signers_name, @signature = data
        @expiration = expiration
        @inception = inception
        self.type_covered=(type_covered)
        self.signers_name=(signers_name)
        self.algorithm=(algorithm)
      end

      def from_string(input)
        if (input.length > 0)
          data = input.split(" ")
          self.type_covered=(data[0])
          self.algorithm=(data[1])
          self.labels=data[2].to_i
          self.original_ttl=data[3].to_i
          self.expiration=get_time(data[4])
          #  Brackets may also be present
          index = 5
          end_index = data.length - 1
          if (data[index]=="(")
            index = 6
            end_index = data.length - 2
          end
          self.inception=get_time(data[index])
          self.key_tag=data[index+1].to_i
          self.signers_name=(data[index+2])
          #  signature can include whitespace - include all text
          #  until we come to " )" at the end, and then gsub
          #  the white space out
          buf=""
          (index+3..end_index).each {|i|
            if (comment_index = data[i].index(";"))
              buf += data[i].slice(0, comment_index)
              #  @TODO@ We lose the comments here - we should really keep them for when we write back to string format?
              break
            else
            buf += data[i]
            end
          }
          buf.gsub!(/\n/, "")
          buf.gsub!(/ /, "")
          # self.signature=Base64.decode64(buf)
          self.signature=buf.unpack("m*")[0]
        end
      end

      def RRSIG.get_time(input)
        if (input.kind_of?Fixnum)
          return input
        end
        #  RFC 4034, section 3.2
        # The Signature Expiration Time and Inception Time field values MUST be
        #    represented either as an unsigned decimal integer indicating seconds
        #    since 1 January 1970 00:00:00 UTC, or in the form YYYYMMDDHHmmSS in
        #    UTC, where:
        # 
        #       YYYY is the year (0001-9999, but see Section 3.1.5);
        #       MM is the month number (01-12);
        #       DD is the day of the month (01-31);
        #       HH is the hour, in 24 hour notation (00-23);
        #       mm is the minute (00-59); and
        #       SS is the second (00-59).
        # 
        #    Note that it is always possible to distinguish between these two
        #    formats because the YYYYMMDDHHmmSS format will always be exactly 14
        #    digits, while the decimal representation of a 32-bit unsigned integer
        #    can never be longer than 10 digits.
        if (input.length == 10)
          return input.to_i
        elsif (input.length == 14)
          year = input[0,4]
          mon=input[4,2]
          day=input[6,2]
          hour=input[8,2]
          min=input[10,2]
          sec=input[12,2]
          #  @TODO@ REPLACE THIS BY LOCAL CODE - Time.gm DOG SLOW!
          return Time.gm(year, mon, day, hour, min, sec).to_i
        else
          raise DecodeError.new("RRSIG : Illegal time value #{input} - see RFC 4034 section 3.2")
        end
      end

      def get_time(input)
        return RRSIG.get_time(input)
      end

      def format_time(time)
        return Time.at(time).gmtime.strftime("%Y%m%d%H%M%S")
      end

      def rdata_to_string #:nodoc: all
        if (@type_covered!=nil)
#          signature = Base64.encode64(@signature) # .gsub(/\n/, "")
          signature = [@signature].pack("m*").gsub(/\n/, "")
          #  @TODO@ Display the expiration and inception as
          return "#{@type_covered.string} #{@algorithm.string} #{@labels} #{@original_ttl} " +
            "#{format_time(@expiration)} ( #{format_time(@inception)} " +
            "#{@key_tag} #{@signers_name.to_s(true)} #{signature} )"
        else
          return ""
        end
      end

      def encode_rdata(msg, canonical=false) #:nodoc: all
        #  2 octets, then 2 sets of 1 octet
        msg.put_pack('ncc', @type_covered.to_i, @algorithm.to_i, @labels)
        msg.put_pack("NNN", @original_ttl, @expiration, @inception)
        msg.put_pack("n", @key_tag)
        msg.put_name(@signers_name, canonical, false)
        msg.put_bytes(@signature)
      end

      def self.decode_rdata(msg) #:nodoc: all
        type_covered, algorithm, labels = msg.get_unpack('ncc')
        original_ttl, expiration, inception = msg.get_unpack('NNN')
        key_tag, = msg.get_unpack('n')
        signers_name = msg.get_name
        signature  = msg.get_bytes
        return self.new(
          [type_covered, algorithm, labels, original_ttl, expiration,
            inception, key_tag, signers_name, signature])
      end

      def sig_data
        # RRSIG_RDATA is the wire format of the RRSIG RDATA fields
        # with the Signer's Name field in canonical form and
        # the Signature field excluded;
        data = MessageEncoder.new { |msg|
          msg.put_pack('ncc', @type_covered.to_i, @algorithm.to_i, @labels)
          msg.put_pack("NNN", @original_ttl, @expiration, @inception)
          msg.put_pack("n", @key_tag)
          msg.put_name(@signers_name, true)
        }.to_s
        return data
      end
    end
  end
end