Browse Source

[master] Merge branch 'trac3432'

Implements TSIG in D2

Fixed Conflicts:
	src/bin/d2/nc_trans.cc
	src/bin/d2/nc_trans.h
	src/bin/d2/tests/nc_test_utils.cc
	src/bin/d2/tests/nc_trans_unittests.cc
Thomas Markwalder 11 years ago
parent
commit
80fea12a53

+ 75 - 34
doc/guide/bind10-guide.xml

@@ -5367,75 +5367,118 @@ corresponding values in the DHCP servers' "dhcp-ddns" configuration section.
 
       <section id="d2-tsig-key-list-config">
         <title>TSIG Key List</title>
-<note>
-<simpara>
-        While this section may be displayed and edited using bindctl, the use
-        of TSIG in actual communications between D2 and DNS servers is not yet
-        implemented.
-</simpara>
-</note>
         <para>
-        DDNS protocol can be conducted with or without TSIG as defined in
-        RFC 2845. This configuration section allows the administrator to
-        define the dictionary of TSIG keys to may be used.  To use TSIG
-        when working with a specific DDNS Domain that key must be defined in
-        the TSIG Key List and referenced by name in that domain's entry in
-        the DDNS catalog.
-        </para>
+        A DDNS protocol exchange can be conducted with or without TSIG
+        (defined in <ulink url="http://tools.ietf/org/html/rfc2845">RFC
+        2845</ulink>). This configuration section allows the administrator
+        to define the set of TSIG keys that may be used in such
+        exchanges.</para>
+
+        <para>To use TSIG when updating entries in a DNS Domain,
+        a key must be defined in the TSIG Key List and referenced by
+        name in that domain's configuration entry.  When D2 matches a
+        change request to a domain, it checks whether the domain has
+        a TSIG key associated with it.  If so, D2 will use that key to
+        sign DNS update messages sent to and verify repsonses received
+        from the domain's DNS server(s). For each TSIG key required by
+        the DNS servers that D2 will be working with there must be a
+        corresponding TSIG key in the TSIG Key list.</para>
+
         <para>
-        As one might gather from its name, this section is a list of
-        TSIG keys. Each key has three parameters:
+        As one might gather from the name, the tsig_key section of the
+        D2 configuration lists the TSIG keys.  Each entry describes a
+        TSIG key used by one or more DNS servers to authenticate requests
+        and sign responses.  Every entry in the list has three parameters:
         <itemizedlist>
           <listitem>
             <simpara>
               <command>name</command> &mdash;
-              is a unique text label used to identify the this key within the
-              list.  It is this value that is used to specify which key (if any)
-              should be used with a specific DNS server. So long as it is
-              unique, its content is arbitrary.  It cannot be blank.
+              a unique text label used to identify this key within the
+              list.  This value is used to specify which key (if any) should be
+              used when updating a specific domain. So long as it is unique its
+              content is arbitrary, although for clarity and ease of maintenance
+              it is recommended that it match the name used on the DNS server(s).
+              It cannot be blank.
             </simpara>
           </listitem>
           <listitem>
             <simpara>
               <command>algorithm</command> &mdash;
               specifies which hashing algorithm should be used with this
-              key.  This value is not currently used.
+              key.  This value must specify the same algorithm used for the
+              key on the DNS server(s). The supported algorithms are listed below:
+              <itemizedlist>
+                <listitem>
+                   <command>HMAC-MD5</command>
+                </listitem>
+                <listitem>
+                    <command>HMAC-SHA1</command>
+                </listitem>
+                <listitem>
+                  <command>HMAC-SHA224</command>
+              </listitem>
+              <listitem>
+                  <command>HMAC-SHA256</command>
+              </listitem>
+              <listitem>
+                  <command>HMAC-SHA384</command>
+                  </listitem>
+              <listitem>
+                  <command>HMAC-SHA512</command>
+              </listitem>
+              </itemizedlist>
+              This value is not case sensitive.
             </simpara>
           </listitem>
           <listitem>
             <simpara>
               <command>secret</command> &mdash;
-              is used to specify the shared secret key code for this key. This
-              value is not currently used.
+              is used to specify the shared secret key code for this key.  This value is
+              case sensitive and must exactly match the value specified on the DNS server(s).
+              It is a base64-encoded text value.
             </simpara>
           </listitem>
         </itemizedlist>
         </para>
         <para>
+        As an example, suppose that a domain D2 will be updating is
+        maintained by a BIND9 DNS server which requires dynamic updates
+        to be secured with TSIG.  Suppose further that the entry for
+        the TSIG key in BIND9's named.conf file looks like this:
+<screen>
+   :
+   key "key.four.example.com." {
+       algorithm hmac-sha224;
+       secret "bZEG7Ow8OgAUPfLWV3aAUQ==";
+   };
+   :
+</screen>
         By default, the TSIG Key list is empty:
 <screen>
 <userinput>> config show DhcpDdns/tsig_keys</userinput>
 DhcpDdns/tsig_keys  []  list  (default)
 </screen>
-        To create a new key in the list, one must first add a new key element:
+        We must first create a new key in the list:
 <screen>
 <userinput>> config add DhcpDdns/tsig_keys</userinput>
 </screen>
-        Displaying the new element, reveals this:
+        Displaying the new element, reveals:
 <screen>
 <userinput>> config show DhcpDdns/tsig_keys[0]</userinput>
 DhcpDdns/tsig_keys[0]/name  ""  string  (default)
-DhcpDdns/tsig_keys[0]/algorithm "hmac_md5"  string  (modified)
+DhcpDdns/tsig_keys[0]/algorithm "HMAC-MD5"  string  (modified)
 DhcpDdns/tsig_keys[0]/secret  ""  string  (default)
 </screen>
-        Populating the key name and secret, while accepting the default value
-        for alogorithm:
+        Now set all three values to match BIND9's key:
 <screen>
-<userinput>> config set DhcpDdns/tsig_keys[0]/name "key1.example.com"</userinput>
-<userinput>> config set DhcpDdns/tsig_keys[0]/secret "123456789"</userinput>
+<userinput>> config set DhcpDdns/tsig_keys[0]/name "key.four.example.com"</userinput>
+<userinput>> config set DhcpDdns/tsig_keys[0]/algorithm "HMAC-SHA224"</userinput>
+<userinput>> config set DhcpDdns/tsig_keys[0]/secret "bZEG7Ow8OgAUPfLWV3aAUQ=="</userinput>
 <userinput>> config commit</userinput>
 </screen>
         </para>
+        These steps would be repeated for each TSIG key needed.  Note that the same TSIG key
+        can be used with more than one domain.
       </section> <!-- "d2-tsig-key-list-config" -->
 
       <section id="d2-forward-ddns-config">
@@ -5453,8 +5496,6 @@ DhcpDdns/forward_ddns/ddns_domains  [] list  (default)
         </para>
         <section id="add-forward-ddns-domain">
           <title>Adding Forward DDNS Domains</title>
-
-
           <para>
           A forward DDNS Domain maps a forward DNS zone to a set of DNS servers
           which maintain the forward DNS data for that zone.  You will need one
@@ -5633,8 +5674,8 @@ DhcpDdns/reverse_ddns/ddns_domains  [] list  (default)
               <simpara>
               <command>key_name</command> &mdash;
               If TSIG should be used with this domain's servers, then this
-              value should be the name of the key from within the TSIG Key List
-              to use.  If the value is blank (the default), TSIG will not be
+              value should be the name of that key from the TSIG Key List.
+              If the value is blank (the default), TSIG will not be
               used in DDNS conversations with this domain's servers.  Currently
               this value is not used as TSIG has not been implemented.
               </simpara>

+ 73 - 16
src/bin/d2/d2_config.cc

@@ -124,15 +124,59 @@ operator<<(std::ostream& os, const D2Params& config) {
 }
 
 // *********************** TSIGKeyInfo  *************************
+// Note these values match correpsonding values for Bind9's
+// dnssec-keygen
+const char* TSIGKeyInfo::HMAC_MD5_STR = "HMAC-MD5";
+const char* TSIGKeyInfo::HMAC_SHA1_STR = "HMAC-SHA1";
+const char* TSIGKeyInfo::HMAC_SHA224_STR = "HMAC-SHA224";
+const char* TSIGKeyInfo::HMAC_SHA256_STR = "HMAC-SHA256";
+const char* TSIGKeyInfo::HMAC_SHA384_STR = "HMAC-SHA384";
+const char* TSIGKeyInfo::HMAC_SHA512_STR = "HMAC-SHA512";
 
 TSIGKeyInfo::TSIGKeyInfo(const std::string& name, const std::string& algorithm,
                          const std::string& secret)
-    :name_(name), algorithm_(algorithm), secret_(secret) {
+    :name_(name), algorithm_(algorithm), secret_(secret), tsig_key_() {
+    remakeKey();
 }
 
 TSIGKeyInfo::~TSIGKeyInfo() {
 }
 
+const dns::Name&
+TSIGKeyInfo::stringToAlgorithmName(const std::string& algorithm_id) {
+    if (boost::iequals(algorithm_id, HMAC_MD5_STR)) {
+        return (dns::TSIGKey::HMACMD5_NAME());
+    } else if (boost::iequals(algorithm_id, HMAC_SHA1_STR)) {
+        return (dns::TSIGKey::HMACSHA1_NAME());
+    } else if (boost::iequals(algorithm_id, HMAC_SHA224_STR)) {
+        return (dns::TSIGKey::HMACSHA224_NAME());
+    } else if (boost::iequals(algorithm_id, HMAC_SHA256_STR)) {
+        return (dns::TSIGKey::HMACSHA256_NAME());
+    } else if (boost::iequals(algorithm_id, HMAC_SHA384_STR)) {
+        return (dns::TSIGKey::HMACSHA384_NAME());
+    } else if (boost::iequals(algorithm_id, HMAC_SHA512_STR)) {
+        return (dns::TSIGKey::HMACSHA512_NAME());
+    }
+
+    isc_throw(BadValue, "Unknown TSIG Key algorithm: " << algorithm_id);
+}
+
+void
+TSIGKeyInfo::remakeKey() {
+    try {
+        // Since our secret value is base64 encoded already, we need to
+        // build the input string for the appropriate TSIGKey constructor.
+        // If secret isn't a valid base64 value, the constructor will throw.
+        std::ostringstream stream;
+        stream << dns::Name(name_).toText() << ":"
+               << secret_ << ":"
+               << stringToAlgorithmName(algorithm_);
+
+        tsig_key_.reset(new dns::TSIGKey(stream.str()));
+    } catch (const std::exception& ex) {
+        isc_throw(D2CfgError, "Cannot make TSIGKey: " << ex.what());
+    }
+}
 
 // *********************** DnsServerInfo  *************************
 
@@ -164,14 +208,25 @@ operator<<(std::ostream& os, const DnsServerInfo& server) {
 
 // *********************** DdnsDomain  *************************
 
-DdnsDomain::DdnsDomain(const std::string& name, const std::string& key_name,
-                       DnsServerInfoStoragePtr servers)
-    : name_(name), key_name_(key_name), servers_(servers) {
+DdnsDomain::DdnsDomain(const std::string& name,
+                       DnsServerInfoStoragePtr servers,
+                       const TSIGKeyInfoPtr& tsig_key_info)
+    : name_(name), servers_(servers),
+      tsig_key_info_(tsig_key_info) {
 }
 
 DdnsDomain::~DdnsDomain() {
 }
 
+const std::string
+DdnsDomain::getKeyName() const {
+    if (tsig_key_info_) {
+        return (tsig_key_info_->getName());
+    }
+
+    return ("");
+}
+
 // *********************** DdnsDomainLstMgr  *************************
 
 const char* DdnsDomainListMgr::wildcard_domain_name_ = "*";
@@ -308,9 +363,6 @@ TSIGKeyInfoParser::build(isc::data::ConstElementPtr key_config) {
     local_scalars_.getParam("algorithm", algorithm);
     local_scalars_.getParam("secret", secret);
 
-    // @todo Validation here is very superficial. This will expand as TSIG
-    // Key use is more fully implemented.
-
     // Name cannot be blank.
     if (name.empty()) {
         isc_throw(D2CfgError, "TSIG Key Info must specify name");
@@ -361,9 +413,6 @@ TSIGKeyInfoParser::createConfigParser(const std::string& config_id) {
 
 void
 TSIGKeyInfoParser::commit() {
-    /// @todo if at some point  TSIG keys need some form of runtime resource
-    /// initialization, such as creating some sort of hash instance in
-    /// crytpolib.  Once TSIG is fully implemented under Trac #3432 we'll know.
 }
 
 // *********************** TSIGKeyInfoListParser  *************************
@@ -606,18 +655,26 @@ DdnsDomainParser::build(isc::data::ConstElementPtr domain_config) {
         isc_throw(D2CfgError, "Duplicate domain specified:" << name);
     }
 
-    // Key name is optional. If it is not blank, then validate it against
-    // the defined list of keys.
+    // Key name is optional. If it is not blank, then find the key in the
+    /// list of defined keys.
+    TSIGKeyInfoPtr tsig_key_info;
     local_scalars_.getParam("key_name", key_name, DCfgContextBase::OPTIONAL);
     if (!key_name.empty()) {
-        if ((!keys_) || (keys_->find(key_name) == keys_->end())) {
-            isc_throw(D2CfgError, "DdnsDomain :" << name <<
-                     " specifies and undefined key:" << key_name);
+        if (keys_) {
+            TSIGKeyInfoMap::iterator kit = keys_->find(key_name);
+            if (kit != keys_->end()) {
+                tsig_key_info = kit->second;
+            }
+        }
+
+        if (!tsig_key_info) {
+            isc_throw(D2CfgError, "DdnsDomain " << name <<
+                     " specifies an undefined key: " << key_name);
         }
     }
 
     // Instantiate the new domain and add it to domain storage.
-    DdnsDomainPtr domain(new DdnsDomain(name, key_name, local_servers_));
+    DdnsDomainPtr domain(new DdnsDomain(name, local_servers_, tsig_key_info));
 
     // Add the new domain to the domain storage.
     (*domains_)[name] = domain;

+ 103 - 24
src/bin/d2/d2_config.h

@@ -19,6 +19,7 @@
 #include <d2/d2_asio.h>
 #include <d2/d_cfg_mgr.h>
 #include <dhcpsrv/dhcp_parsers.h>
+#include <dns/tsig.h>
 #include <exceptions/exceptions.h>
 
 #include <boost/foreach.hpp>
@@ -264,22 +265,53 @@ typedef boost::shared_ptr<D2Params> D2ParamsPtr;
 
 /// @brief Represents a TSIG Key.
 ///
-/// Currently, this is simple storage class containing the basic attributes of
-/// a TSIG Key.  It is intended primarily as a reference for working with
-/// actual keys and may eventually be replaced by isc::dns::TSIGKey.  TSIG Key
-/// functionality at this stage is strictly limited to configuration parsing.
-/// @todo full functionality for using TSIG during DNS updates will be added
-/// in a future release.
+/// Acts as both a storage class containing the basic attributes which
+/// describe a TSIG Key, as well as owning and providing access to an
+/// instance of the actual key (@ref isc::dns::TSIGKey) that can be used
+/// by the IO layer for signing and verifying messages.
+///
 class TSIGKeyInfo {
 public:
+    /// @brief Defines string values for the supported TSIG algorithms
+    //@{
+    static const char* HMAC_MD5_STR;
+    static const char* HMAC_SHA1_STR;
+    static const char* HMAC_SHA256_STR;
+    static const char* HMAC_SHA224_STR;
+    static const char* HMAC_SHA384_STR;
+    static const char* HMAC_SHA512_STR;
+    //}@
 
     /// @brief Constructor
     ///
     /// @param name the unique label used to identify this key
-    /// @param algorithm the name of the encryption alogirthm this key uses.
-    /// (@todo This will be a fixed list of choices)
-    ///
-    /// @param secret the secret component of this key
+    /// @param algorithm the id of the encryption alogirthm this key uses.
+    /// Currently supported values are (case insensitive):
+    /// -# "HMAC-MD5"
+    /// -# "HMAC-SHA1"
+    /// -# "HMAC-SHA224"
+    /// -# "HMAC-SHA256"
+    /// -# "HMAC-SHA384"
+    /// -# "HMAC-SHA512"
+    ///
+    /// @param secret  The base-64 encoded secret component for this key.
+    /// (A suitable string for use here could be obtained by running the
+    /// BIND 9 dnssec-keygen program; the contents of resulting key file
+    /// will look similar to:
+    /// @code
+    ///   Private-key-format: v1.3
+    ///   Algorithm: 157 (HMAC_MD5)
+    ///   Key: LSWXnfkKZjdPJI5QxlpnfQ==
+    ///   Bits: AAA=
+    ///   Created: 20140515143700
+    ///   Publish: 20140515143700
+    ///   Activate: 20140515143700
+    /// @endcode
+    /// where the value the "Key:" entry is the secret component of the key.)
+    ///
+    /// @throw D2CfgError if values supplied are invalid:
+    /// name cannot be blank, algorithm must be a supported value,
+    /// secret must be a non-blank, base64 encoded string.
     TSIGKeyInfo(const std::string& name, const std::string& algorithm,
                 const std::string& secret);
 
@@ -293,7 +325,7 @@ public:
         return (name_);
     }
 
-    /// @brief Getter which returns the key's algorithm.
+    /// @brief Getter which returns the key's algorithm string ID
     ///
     /// @return returns the algorithm as as std::string.
     const std::string getAlgorithm() const {
@@ -307,18 +339,55 @@ public:
         return (secret_);
     }
 
+    /// @brief Getter which returns the TSIG key used to sign and verify
+    /// messages
+    ///
+    /// @return const pointer reference to dns::TSIGKey.
+    const dns::TSIGKeyPtr& getTSIGKey() const {
+        return (tsig_key_);
+    }
+
+    /// @brief Converts algorithm id to dns::TSIGKey algorithm dns::Name
+    ///
+    /// @param algorithm_id string value to translate into an algorithm name.
+    /// Currently supported values are (case insensitive):
+    /// -# "HMAC-MD5"
+    /// -# "HMAC-SHA1"
+    /// -# "HMAC-SHA224"
+    /// -# "HMAC-SHA256"
+    /// -# "HMAC-SHA384"
+    /// -# "HMAC-SHA512"
+    ///
+    /// @return const reference to a dns::Name containing the algorithm name
+    /// @throw BadValue if ID isn't recognized.
+    static const dns::Name& stringToAlgorithmName(const std::string&
+                                                  algorithm_id);
+
 private:
+    /// @brief Creates the actual TSIG key instance member
+    ///
+    /// Replaces this tsig_key member with a key newly created using the key
+    /// name, algorithm id, and secret.
+    /// This method is currently only called by the constructor, however it
+    /// could be called post-construction should keys ever support expiration.
+    ///
+    /// @throw D2CfgError with an explanation if the key could not be created.
+    void remakeKey();
+
     /// @brief The name of the key.
     ///
     /// This value is the unique identifier that domains use to
     /// to specify which TSIG key they need.
     std::string name_;
 
-    /// @brief The algorithm that should be used for this key.
+    /// @brief The string ID of the algorithm that should be used for this key.
     std::string algorithm_;
 
-    /// @brief The secret value component of this key.
+    /// @brief The base64 encoded string secret value component of this key.
     std::string secret_;
+
+    /// @brief The actual TSIG key.
+    dns::TSIGKeyPtr tsig_key_;
 };
 
 /// @brief Defines a pointer for TSIGKeyInfo instances.
@@ -454,10 +523,12 @@ public:
     /// @brief Constructor
     ///
     /// @param name is the domain name of the domain.
-    /// @param key_name is the TSIG key name for use with this domain.
     /// @param servers is the list of server(s) supporting this domain.
-    DdnsDomain(const std::string& name, const std::string& key_name,
-               DnsServerInfoStoragePtr servers);
+    /// @param tsig_key_info pointer to the TSIGKeyInfo for the dommain's key
+    /// It defaults to an empty pointer, signifying the domain has no key.
+    DdnsDomain(const std::string& name,
+               DnsServerInfoStoragePtr servers,
+               const TSIGKeyInfoPtr& tsig_key_info = TSIGKeyInfoPtr());
 
     /// @brief Destructor
     virtual ~DdnsDomain();
@@ -469,12 +540,11 @@ public:
         return (name_);
     }
 
-    /// @brief Getter which returns the domain's TSIG key name.
+    /// @brief Convenience method which returns the domain's TSIG key name.
     ///
-    /// @return returns the key name in an std::string.
-    const std::string getKeyName() const {
-        return (key_name_);
-    }
+    /// @return returns the key name in an std::string. If domain has no
+    /// TSIG key, the string will empty.
+    const std::string getKeyName() const;
 
     /// @brief Getter which returns the domain's list of servers.
     ///
@@ -483,15 +553,24 @@ public:
         return (servers_);
     }
 
+    /// @brief Getter which returns the domain's TSIGKey info
+    ///
+    /// @return returns the pointer to the server storage.  If the domain
+    /// is not configured to use TSIG the pointer will be empty.
+    const TSIGKeyInfoPtr& getTSIGKeyInfo() {
+        return (tsig_key_info_);
+    }
+
 private:
     /// @brief The domain name of the domain.
     std::string name_;
 
-    /// @brief The name of the TSIG key for use with this domain.
-    std::string key_name_;
-
     /// @brief The list of server(s) supporting this domain.
     DnsServerInfoStoragePtr servers_;
+
+    /// @brief Pointer to domain's the TSIGKeyInfo.
+    /// Value is empty if the domain is not configured for TSIG.
+    TSIGKeyInfoPtr tsig_key_info_;
 };
 
 /// @brief Defines a pointer for DdnsDomain instances.

+ 19 - 5
src/bin/d2/d2_update_message.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2013  Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2013-2014 Internet Systems Consortium, Inc. ("ISC")
 //
 // Permission to use, copy, modify, and/or distribute this software for any
 // purpose with or without fee is hereby granted, provided that the above
@@ -108,7 +108,8 @@ D2UpdateMessage::addRRset(const UpdateMsgSection section,
 }
 
 void
-D2UpdateMessage::toWire(AbstractMessageRenderer& renderer) {
+D2UpdateMessage::toWire(AbstractMessageRenderer& renderer,
+                        TSIGContext* const tsig_context) {
     // We are preparing the wire format of the message, meaning
     // that this message will be sent as a request to the DNS.
     // Therefore, we expect that this message is a REQUEST.
@@ -122,16 +123,29 @@ D2UpdateMessage::toWire(AbstractMessageRenderer& renderer) {
         isc_throw(InvalidZoneSection, "Zone section of the DNS Update message"
                   " must comprise exactly one record (RFC2136, section 2.3)");
     }
-    message_.toWire(renderer);
+    message_.toWire(renderer, tsig_context);
 }
 
 void
-D2UpdateMessage::fromWire(isc::util::InputBuffer& buffer) {
+D2UpdateMessage::fromWire(const void* received_data, size_t bytes_received,
+                          dns::TSIGContext* const tsig_context) {
     // First, use the underlying dns::Message implementation to get the
     // contents of the DNS response message. Note that it may or may
     // not be the message that we are interested in, but needs to be
     // parsed so as we can check its ID, Opcode etc.
-    message_.fromWire(buffer);
+    isc::util::InputBuffer received_data_buffer(received_data, bytes_received);
+    message_.fromWire(received_data_buffer);
+
+    // If tsig_contex is not NULL, then we need to verify the message.
+    if (tsig_context) {
+        TSIGError error = tsig_context->verify(message_.getTSIGRecord(),
+                                               received_data, bytes_received);
+        if (error != TSIGError::NOERROR()) {
+            isc_throw(TSIGVerifyError, "TSIG verification failed: "
+                      << error.toText());
+        }
+    }
+
     // This class exposes the getZone() function. This function will return
     // pointer to the D2Zone object if non-empty Zone section exists in the
     // received message. It will return NULL pointer if it doesn't exist.

+ 35 - 11
src/bin/d2/d2_update_message.h

@@ -1,4 +1,4 @@
-// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2013-2014 Internet Systems Consortium, Inc. ("ISC")
 //
 // Permission to use, copy, modify, and/or distribute this software for any
 // purpose with or without fee is hereby granted, provided that the above
@@ -21,6 +21,7 @@
 #include <dns/rcode.h>
 #include <dns/rrclass.h>
 #include <dns/rrset.h>
+#include <dns/tsig.h>
 
 #include <map>
 
@@ -63,6 +64,17 @@ public:
         isc::Exception(file, line, what) {}
 };
 
+/// @brief Exception indicating that a signed, inbound message failed to verfiy
+///
+/// This exception is thrown when TSIG verification of a DNS server's response
+/// fails.
+class TSIGVerifyError : public Exception {
+public:
+    TSIGVerifyError(const char* file, size_t line, const char* what) :
+        isc::Exception(file, line, what) {}
+};
+
+
 class D2UpdateMessage;
 
 /// @brief Pointer to the DNS Update Message.
@@ -250,6 +262,9 @@ public:
     /// RRs respectively. The ZOCOUNT must be equal to 1 because RFC2136
     /// requires that the message comprises exactly one Zone record.
     ///
+    /// If given a TSIG context, this method will pass the context down into
+    /// dns::Message.toWire() method which signs the message using the context.
+    ///
     /// This function does not guarantee exception safety. However, exceptions
     /// should be rare because @c D2UpdateMessage class API prevents invalid
     /// use of the class. The typical case, when this function may throw an
@@ -260,18 +275,23 @@ public:
     ///
     /// @param renderer A renderer object used to generate the message wire
     /// format.
-    void toWire(dns::AbstractMessageRenderer& renderer);
+    /// @param tsig_ctx A TSIG context that is to be used for signing the
+    /// message. If NULL the message will not be signed.
+    void toWire(dns::AbstractMessageRenderer& renderer,
+                dns::TSIGContext* const tsig_ctx = NULL);
 
     /// @brief Decode incoming message from the wire format.
     ///
     /// This function decodes the DNS Update message stored in the buffer
-    /// specified by the function argument. In the first turn, this function
-    /// parses message header and extracts the section counters: ZOCOUNT,
-    /// PRCOUNT, UPCOUNT and ADCOUNT. Using these counters, function identifies
-    /// message sections, which follow message header. These sections can be
-    /// later accessed using: @c D2UpdateMessage::getZone,
-    /// @c D2UpdateMessage::beginSection and @c D2UpdateMessage::endSection
-    /// functions.
+    /// specified by the function argument.  If given a TSIG context, then
+    /// the function will first attempt to use that context to verify the
+    /// message signature.  If verification fails a TSIGVefiryError exception
+    /// will be thrown. The function then parses message header and extracts
+    /// the section counters: ZOCOUNT, PRCOUNT, UPCOUNT and ADCOUNT. Using
+    /// these counters, function identifies message sections, which follow
+    /// message header. These sections can be later accessed using:
+    /// @c D2UpdateMessage::getZone, @c D2UpdateMessage::beginSection and
+    /// @c D2UpdateMessage::endSection functions.
     ///
     /// This function is NOT exception safe. It signals message decoding errors
     /// through exceptions. Message decoding error may occur if the received
@@ -282,8 +302,12 @@ public:
     /// message is the server response.
     /// - The number of records in the Zone section is greater than 1.
     ///
-    /// @param buffer input buffer, holding DNS Update message to be parsed.
-    void fromWire(isc::util::InputBuffer& buffer);
+    /// @param received_data buffer holding DNS Update message to be parsed.
+    /// @param bytes_received the number of bytes in received_data
+    /// @param tsig_context A TSIG context that is to be used to verify the
+    /// message. If NULL TSIG verification will not be attempted.
+    void fromWire(const void* received_data, size_t bytes_received,
+                  dns::TSIGContext* const tsig_context = NULL);
     //@}
 
 private:

+ 1 - 1
src/bin/d2/dhcp-ddns.spec

@@ -44,7 +44,7 @@
             "item_name": "tsig_key",
             "item_type": "map",
             "item_optional": false,
-            "item_default": {"algorithm" : "hmac_md5"},
+            "item_default": {"algorithm" : "HMAC-MD5"},
             "map_item_spec": [ 
             {
                 "item_name": "name",

+ 37 - 34
src/bin/d2/dns_client.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2013-2014 Internet Systems Consortium, Inc. ("ISC")
 //
 // Permission to use, copy, modify, and/or distribute this software for any
 // purpose with or without fee is hereby granted, provided that the above
@@ -60,6 +60,8 @@ public:
     DNSClient::Callback* callback_;
     // A Transport Layer protocol used to communicate with a DNS.
     DNSClient::Protocol proto_;
+    // TSIG context used to sign outbound and verify inbound messages.
+    dns::TSIGContextPtr tsig_context_;
 
     // Constructor and Destructor
     DNSClientImpl(D2UpdateMessagePtr& response_placeholder,
@@ -73,12 +75,13 @@ public:
     // type, representing a response from the server is set.
     virtual void operator()(asiodns::IOFetch::Result result);
 
-    // Starts asynchronous DNS Update.
+    // Starts asynchronous DNS Update using TSIG.
     void doUpdate(asiolink::IOService& io_service,
                   const asiolink::IOAddress& ns_addr,
                   const uint16_t ns_port,
                   D2UpdateMessage& update,
-                  const unsigned int wait);
+                  const unsigned int wait,
+                  const dns::TSIGKeyPtr& tsig_key);
 
     // This function maps the IO error to the DNSClient error.
     DNSClient::Status getStatus(const asiodns::IOFetch::Result);
@@ -130,7 +133,6 @@ DNSClientImpl::operator()(asiodns::IOFetch::Result result) {
     // and pass the status code.
     DNSClient::Status status = getStatus(result);
     if (status == DNSClient::SUCCESS) {
-        InputBuffer response_buf(in_buf_->getData(), in_buf_->getLength());
         // Allocate a new response message. (Note that Message::fromWire
         // may only be run once per message, so we need to start fresh
         // each time.)
@@ -140,14 +142,19 @@ DNSClientImpl::operator()(asiodns::IOFetch::Result result) {
         // throw an exception. We want to catch this exception to return
         // appropriate status code to the caller and log this event.
         try {
-            response_->fromWire(response_buf);
-
+            response_->fromWire(in_buf_->getData(), in_buf_->getLength(),
+                                tsig_context_.get());
         } catch (const isc::Exception& ex) {
             status = DNSClient::INVALID_RESPONSE;
             LOG_DEBUG(dctl_logger, DBGLVL_TRACE_DETAIL,
                       DHCP_DDNS_INVALID_RESPONSE).arg(ex.what());
 
         }
+
+        if (tsig_context_) {
+            // Context is a one-shot deal, get rid of it.
+            tsig_context_.reset();
+        }
     }
 
     // Once we are done with internal business, let's call a callback supplied
@@ -174,13 +181,31 @@ DNSClientImpl::getStatus(const asiodns::IOFetch::Result result) {
     }
     return (DNSClient::OTHER);
 }
-
 void
 DNSClientImpl::doUpdate(asiolink::IOService& io_service,
                         const IOAddress& ns_addr,
                         const uint16_t ns_port,
                         D2UpdateMessage& update,
-                        const unsigned int wait) {
+                        const unsigned int wait,
+                        const dns::TSIGKeyPtr& tsig_key) {
+    // The underlying implementation which we use to send DNS Updates uses
+    // signed integers for timeout. If we want to avoid overflows we need to
+    // respect this limitation here.
+    if (wait > DNSClient::getMaxTimeout()) {
+        isc_throw(isc::BadValue, "A timeout value for DNS Update request must"
+                  " not exceed " << DNSClient::getMaxTimeout()
+                  << ". Provided timeout value is '" << wait << "'");
+    }
+
+    // Create a TSIG context if we have a key, otherwise clear the context
+    // pointer.  Message marshalling uses non-null context is the indicator
+    // that TSIG should be used.
+    if (tsig_key) {
+        tsig_context_.reset(new TSIGContext(*tsig_key));
+    } else {
+        tsig_context_.reset();
+    }
+
     // A renderer is used by the toWire function which creates the on-wire data
     // from the DNS Update message. A renderer has its internal buffer where it
     // renders data by default. However, this buffer can't be directly accessed.
@@ -193,7 +218,7 @@ DNSClientImpl::doUpdate(asiolink::IOService& io_service,
 
     // Render DNS Update message. This may throw a bunch of exceptions if
     // invalid message object is given.
-    update.toWire(renderer);
+    update.toWire(renderer, tsig_context_.get());
 
     // IOFetch has all the mechanisms that we need to perform asynchronous
     // communication with the DNS server. The last but one argument points to
@@ -211,7 +236,6 @@ DNSClientImpl::doUpdate(asiolink::IOService& io_service,
     io_service.post(io_fetch);
 }
 
-
 DNSClient::DNSClient(D2UpdateMessagePtr& response_placeholder,
                      Callback* callback, const DNSClient::Protocol proto)
     : impl_(new DNSClientImpl(response_placeholder, callback, proto)) {
@@ -228,35 +252,14 @@ DNSClient::getMaxTimeout() {
 }
 
 void
-DNSClient::doUpdate(asiolink::IOService&,
-                    const IOAddress&,
-                    const uint16_t,
-                    D2UpdateMessage&,
-                    const unsigned int,
-                    const dns::TSIGKey&) {
-    isc_throw(isc::NotImplemented, "TSIG is currently not supported for"
-              "DNS Update message");
-}
-
-void
 DNSClient::doUpdate(asiolink::IOService& io_service,
                     const IOAddress& ns_addr,
                     const uint16_t ns_port,
                     D2UpdateMessage& update,
-                    const unsigned int wait) {
-    // The underlying implementation which we use to send DNS Updates uses
-    // signed integers for timeout. If we want to avoid overflows we need to
-    // respect this limitation here.
-    if (wait > getMaxTimeout()) {
-        isc_throw(isc::BadValue, "A timeout value for DNS Update request must"
-                  " not exceed " << getMaxTimeout()
-                  << ". Provided timeout value is '" << wait << "'");
-    }
-    impl_->doUpdate(io_service, ns_addr, ns_port, update, wait);
+                    const unsigned int wait,
+                    const dns::TSIGKeyPtr& tsig_key) {
+    impl_->doUpdate(io_service, ns_addr, ns_port, update, wait, tsig_key);
 }
 
-
-
 } // namespace d2
 } // namespace isc
-

+ 4 - 30
src/bin/d2/dns_client.h

@@ -146,41 +146,15 @@ public:
     /// @param wait A timeout (in milliseconds) for the response. If a response
     /// is not received within the timeout, exchange is interrupted. This value
     /// must not exceed maximal value for 'int' data type.
-    /// @param tsig_key An @c isc::dns::TSIGKey object representing TSIG
-    /// context which will be used to render the DNS Update message.
-    ///
-    /// @todo Implement TSIG Support. Currently any attempt to call this
-    /// function will result in exception.
+    /// @param tsig_key A pointer to an @c isc::dns::TSIGKey object that will
+    /// (if not null) be used to sign the DNS Update message and verify the
+    /// response.
     void doUpdate(asiolink::IOService& io_service,
                   const asiolink::IOAddress& ns_addr,
                   const uint16_t ns_port,
                   D2UpdateMessage& update,
                   const unsigned int wait,
-                  const dns::TSIGKey& tsig_key);
-
-    /// @brief Start asynchronous DNS Update without TSIG.
-    ///
-    /// This function starts asynchronous DNS Update and returns. The DNS Update
-    /// will be executed by the specified IO service. Once the message exchange
-    /// with a DNS server is complete, timeout occurs or IO operation is
-    /// interrupted, the caller-supplied callback function will be invoked.
-    ///
-    /// An address and port of the DNS server is specified through the function
-    /// arguments so as the same instance of the @c DNSClient can be used to
-    /// initiate multiple message exchanges.
-    ///
-    /// @param io_service IO service to be used to run the message exchange.
-    /// @param ns_addr DNS server address.
-    /// @param ns_port DNS server port.
-    /// @param update A DNS Update message to be sent to the server.
-    /// @param wait A timeout (in milliseconds) for the response. If a response
-    /// is not received within the timeout, exchange is interrupted. This value
-    /// must not exceed maximal value for 'int' data type.
-    void doUpdate(asiolink::IOService& io_service,
-                  const asiolink::IOAddress& ns_addr,
-                  const uint16_t ns_port,
-                  D2UpdateMessage& update,
-                  const unsigned int wait);
+                  const dns::TSIGKeyPtr& tsig_key = dns::TSIGKeyPtr());
 
 private:
     DNSClientImpl* impl_;  ///< Pointer to DNSClient implementation.

+ 12 - 5
src/bin/d2/nc_trans.cc

@@ -54,7 +54,7 @@ NameChangeTransaction(IOServicePtr& io_service,
      dns_update_status_(DNSClient::OTHER), dns_update_response_(),
      forward_change_completed_(false), reverse_change_completed_(false),
      current_server_list_(), current_server_(), next_server_pos_(0),
-     update_attempts_(0), cfg_mgr_(cfg_mgr) {
+     update_attempts_(0), cfg_mgr_(cfg_mgr), tsig_key_() {
     /// @todo if io_service is NULL we are multi-threading and should
     /// instantiate our own
     if (!io_service_) {
@@ -168,8 +168,7 @@ NameChangeTransaction::transactionOutcomeString() const {
 
 
 void
-NameChangeTransaction::sendUpdate(const std::string& comment,
-                                  bool /* use_tsig_ */) {
+NameChangeTransaction::sendUpdate(const std::string& comment) {
     try {
         ++update_attempts_;
         // @todo add logic to add/replace TSIG key info in request if
@@ -179,8 +178,7 @@ NameChangeTransaction::sendUpdate(const std::string& comment,
         D2ParamsPtr d2_params = cfg_mgr_->getD2Params();
         dns_client_->doUpdate(*io_service_, current_server_->getIpAddress(),
                               current_server_->getPort(), *dns_update_request_,
-                              d2_params->getDnsServerTimeout());
-
+                              d2_params->getDnsServerTimeout(), tsig_key_);
         // Message is on its way, so the next event should be NOP_EVT.
         postNextEvent(NOP_EVT);
         LOG_DEBUG(dctl_logger, DBGLVL_TRACE_DETAIL,
@@ -424,6 +422,15 @@ NameChangeTransaction::initServerSelection(const DdnsDomainPtr& domain) {
         isc_throw(NameChangeTransactionError,
                   "initServerSelection called with an empty domain");
     }
+
+    // Set the tsig_key to that of the DdnsDomain.
+    TSIGKeyInfoPtr tsig_key_info = domain->getTSIGKeyInfo();
+    if (tsig_key_info) {
+        tsig_key_ = tsig_key_info->getTSIGKey();
+    } else {
+        tsig_key_.reset();
+    }
+
     current_server_list_ = domain->getServers();
     next_server_pos_ = 0;
     current_server_.reset();

+ 9 - 4
src/bin/d2/nc_trans.h

@@ -23,6 +23,7 @@
 #include <d2/dns_client.h>
 #include <d2/state_model.h>
 #include <dhcp_ddns/ncr_msg.h>
+#include <dns/tsig.h>
 
 #include <boost/shared_ptr.hpp>
 #include <map>
@@ -209,14 +210,15 @@ protected:
     /// currently selected server.  Since the send is asynchronous, the method
     /// posts NOP_EVT as the next event and then returns.
     ///
+    /// If tsig_key_ is not NULL, then the update will be conducted using
+    /// the key to sign the request and verify the response, otherwise it
+    /// will be conducted without TSIG.
+    ///
     /// @param comment text to include in log detail
-    /// @param use_tsig True if the update should be include a TSIG key. This
-    /// is not yet implemented.
     ///
     /// If an exception occurs it will be logged and and the transaction will
     /// be failed.
-    virtual void sendUpdate(const std::string& comment = "",
-                            bool use_tsig = false);
+    virtual void sendUpdate(const std::string& comment = "");
 
     /// @brief Adds events defined by NameChangeTransaction to the event set.
     ///
@@ -575,6 +577,9 @@ private:
 
     /// @brief Pointer to the configuration manager.
     D2CfgMgrPtr cfg_mgr_;
+
+    /// @brief Pointer to the TSIG key which should be used (if any).
+    dns::TSIGKeyPtr tsig_key_;
 };
 
 /// @brief Defines a pointer to a NameChangeTransaction.

+ 122 - 71
src/bin/d2/tests/d2_cfg_mgr_unittests.cc

@@ -17,6 +17,7 @@
 #include <d2/d2_cfg_mgr.h>
 #include <d_test_stubs.h>
 #include <test_data_files_config.h>
+#include <util/encode/base64.h>
 
 #include <boost/foreach.hpp>
 #include <gtest/gtest.h>
@@ -170,47 +171,24 @@ bool checkServer(DnsServerInfoPtr server, const char* hostname,
 }
 
 /// @brief Convenience function which compares the contents of the given
-/// TSIGKeyInfo against the given set of values.
+/// TSIGKeyInfo against the given set of values, and that the TSIGKey
+/// member points to a key.
 ///
-/// It is structured in such a way that each value is checked, and output
-/// is generate for all that do not match.
-///
-/// @param key is a pointer to the key to check against.
+/// @param key is a pointer to the TSIGKeyInfo instance to verify
 /// @param name is the value to compare against key's name_.
 /// @param algorithm is the string value to compare against key's algorithm.
 /// @param secret is the value to compare against key's secret.
 ///
 /// @return returns true if there is a match across the board, otherwise it
 /// returns false.
-bool checkKey(TSIGKeyInfoPtr key, const char* name,
-                 const char *algorithm, const char* secret)
-{
+bool checkKey(TSIGKeyInfoPtr key, const std::string& name,
+                 const std::string& algorithm, const std::string& secret) {
     // Return value, assume its a match.
-    bool result = true;
-    if (!key) {
-        EXPECT_TRUE(key);
-        return false;
-    }
-
-    // Check name.
-    if (key->getName() != name) {
-        EXPECT_EQ(name, key->getName());
-        result = false;
-    }
-
-    // Check algorithm.
-    if (key->getAlgorithm() != algorithm) {
-        EXPECT_EQ(algorithm, key->getAlgorithm());
-        result = false;
-    }
-
-    // Check secret.
-    if (key->getSecret() !=  secret) {
-        EXPECT_EQ (secret, key->getSecret());
-        result = false;
-    }
-
-    return (result);
+    return (((key) &&
+        (key->getName() == name) &&
+        (key->getAlgorithm() == algorithm)  &&
+        (key->getSecret() ==  secret)  &&
+        (key->getTSIGKey())));
 }
 
 /// @brief Test fixture class for testing DnsServerInfo parsing.
@@ -459,14 +437,12 @@ TEST_F(D2CfgMgrTest, invalidEntry) {
 /// 1. Name cannot be blank.
 /// 2. Algorithm cannot be blank.
 /// 3. Secret cannot be blank.
-/// @TODO TSIG keys are not fully functional. Only basic validation is
-/// currently supported. This test will need to expand as they evolve.
 TEST_F(TSIGKeyInfoTest, invalidEntry) {
     // Config with a blank name entry.
     std::string config = "{"
                          " \"name\": \"\" , "
-                         " \"algorithm\": \"md5\" , "
-                         " \"secret\": \"0123456789\" "
+                         " \"algorithm\": \"HMAC-MD5\" , "
+                         "   \"secret\": \"LSWXnfkKZjdPJI5QxlpnfQ==\" "
                          "}";
     ASSERT_TRUE(fromJSON(config));
 
@@ -477,7 +453,19 @@ TEST_F(TSIGKeyInfoTest, invalidEntry) {
     config = "{"
                          " \"name\": \"d2_key_one\" , "
                          " \"algorithm\": \"\" , "
-                         " \"secret\": \"0123456789\" "
+                         "   \"secret\": \"LSWXnfkKZjdPJI5QxlpnfQ==\" "
+                         "}";
+
+    ASSERT_TRUE(fromJSON(config));
+
+    // Verify that build fails on blank algorithm.
+    EXPECT_THROW(parser_->build(config_set_), D2CfgError);
+
+    // Config with an invalid algorithm entry.
+    config = "{"
+                         " \"name\": \"d2_key_one\" , "
+                         " \"algorithm\": \"bogus\" , "
+                         "   \"secret\": \"LSWXnfkKZjdPJI5QxlpnfQ==\" "
                          "}";
 
     ASSERT_TRUE(fromJSON(config));
@@ -488,7 +476,7 @@ TEST_F(TSIGKeyInfoTest, invalidEntry) {
     // Config with a blank secret entry.
     config = "{"
                          " \"name\": \"d2_key_one\" , "
-                         " \"algorithm\": \"md5\" , "
+                         " \"algorithm\": \"HMAC-MD5\" , "
                          " \"secret\": \"\" "
                          "}";
 
@@ -496,6 +484,18 @@ TEST_F(TSIGKeyInfoTest, invalidEntry) {
 
     // Verify that build fails blank secret
     EXPECT_THROW(parser_->build(config_set_), D2CfgError);
+
+    // Config with an invalid secret entry.
+    config = "{"
+                         " \"name\": \"d2_key_one\" , "
+                         " \"algorithm\": \"HMAC-MD5\" , "
+                         " \"secret\": \"bogus\" "
+                         "}";
+
+    ASSERT_TRUE(fromJSON(config));
+
+    // Verify that build fails an invalid secret
+    EXPECT_THROW(parser_->build(config_set_), D2CfgError);
 }
 
 /// @brief Verifies that TSIGKeyInfo parsing creates a proper TSIGKeyInfo
@@ -504,13 +504,14 @@ TEST_F(TSIGKeyInfoTest, validEntry) {
     // Valid entries for TSIG key, all items are required.
     std::string config = "{"
                          " \"name\": \"d2_key_one\" , "
-                         " \"algorithm\": \"md5\" , "
-                         " \"secret\": \"0123456789\" "
+                         " \"algorithm\": \"HMAC-MD5\" , "
+                         " \"secret\": \"dGhpcyBrZXkgd2lsbCBtYXRjaA==\" "
                          "}";
     ASSERT_TRUE(fromJSON(config));
 
     // Verify that it builds and commits without throwing.
-    ASSERT_NO_THROW(parser_->build(config_set_));
+    //ASSERT_NO_THROW(parser_->build(config_set_));
+    (parser_->build(config_set_));
     ASSERT_NO_THROW(parser_->commit());
 
     // Verify the correct number of keys are present
@@ -523,7 +524,8 @@ TEST_F(TSIGKeyInfoTest, validEntry) {
     TSIGKeyInfoPtr& key = gotit->second;
 
     // Verify the key contents.
-    EXPECT_TRUE(checkKey(key, "d2_key_one", "md5", "0123456789"));
+    EXPECT_TRUE(checkKey(key, "d2_key_one", "HMAC-MD5",
+                         "dGhpcyBrZXkgd2lsbCBtYXRjaA=="));
 }
 
 /// @brief Verifies that attempting to parse an invalid list of TSIGKeyInfo
@@ -533,16 +535,17 @@ TEST_F(TSIGKeyInfoTest, invalidTSIGKeyList) {
     std::string config = "["
 
                          " { \"name\": \"key1\" , "
-                         "   \"algorithm\": \"algo1\" ,"
-                         "   \"secret\": \"secret11\" "
+                         "   \"algorithm\": \"HMAC-MD5\" ,"
+                         "   \"secret\": \"GWG/Xfbju4O2iXGqkSu4PQ==\" "
                          " },"
+                         // this entry has an invalid algorithm
                          " { \"name\": \"key2\" , "
                          "   \"algorithm\": \"\" ,"
-                         "   \"secret\": \"secret12\" "
+                         "   \"secret\": \"GWG/Xfbju4O2iXGqkSu4PQ==\" "
                          " },"
                          " { \"name\": \"key3\" , "
-                         "   \"algorithm\": \"algo3\" ,"
-                         "   \"secret\": \"secret13\" "
+                         "   \"algorithm\": \"HMAC-MD5\" ,"
+                         "   \"secret\": \"GWG/Xfbju4O2iXGqkSu4PQ==\" "
                          " }"
                          " ]";
 
@@ -563,16 +566,16 @@ TEST_F(TSIGKeyInfoTest, duplicateTSIGKey) {
     std::string config = "["
 
                          " { \"name\": \"key1\" , "
-                         "   \"algorithm\": \"algo1\" ,"
-                         "   \"secret\": \"secret11\" "
+                         "   \"algorithm\": \"HMAC-MD5\" ,"
+                         "   \"secret\": \"GWG/Xfbju4O2iXGqkSu4PQ==\" "
                          " },"
                          " { \"name\": \"key2\" , "
-                         "   \"algorithm\": \"algo2\" ,"
-                         "   \"secret\": \"secret12\" "
+                         "   \"algorithm\": \"HMAC-MD5\" ,"
+                         "   \"secret\": \"GWG/Xfbju4O2iXGqkSu4PQ==\" "
                          " },"
                          " { \"name\": \"key1\" , "
-                         "   \"algorithm\": \"algo3\" ,"
-                         "   \"secret\": \"secret13\" "
+                         "   \"algorithm\": \"HMAC-MD5\" ,"
+                         "   \"secret\": \"GWG/Xfbju4O2iXGqkSu4PQ==\" "
                          " }"
                          " ]";
 
@@ -587,21 +590,34 @@ TEST_F(TSIGKeyInfoTest, duplicateTSIGKey) {
 }
 
 /// @brief Verifies a valid list of TSIG Keys parses correctly.
+/// Also verifies that all of the supported algorithm names work.
 TEST_F(TSIGKeyInfoTest, validTSIGKeyList) {
     // Construct a valid list of keys.
     std::string config = "["
 
                          " { \"name\": \"key1\" , "
-                         "   \"algorithm\": \"algo1\" ,"
-                         "   \"secret\": \"secret1\" "
+                         "   \"algorithm\": \"HMAC-MD5\" ,"
+                         "  \"secret\": \"dGhpcyBrZXkgd2lsbCBtYXRjaA==\" "
                          " },"
                          " { \"name\": \"key2\" , "
-                         "   \"algorithm\": \"algo2\" ,"
-                         "   \"secret\": \"secret2\" "
+                         "   \"algorithm\": \"HMAC-SHA1\" ,"
+                         "  \"secret\": \"dGhpcyBrZXkgd2lsbCBtYXRjaA==\" "
                          " },"
                          " { \"name\": \"key3\" , "
-                         "   \"algorithm\": \"algo3\" ,"
-                         "   \"secret\": \"secret3\" "
+                         "   \"algorithm\": \"HMAC-SHA256\" ,"
+                         "  \"secret\": \"dGhpcyBrZXkgd2lsbCBtYXRjaA==\" "
+                         " },"
+                         " { \"name\": \"key4\" , "
+                         "   \"algorithm\": \"HMAC-SHA224\" ,"
+                         "  \"secret\": \"dGhpcyBrZXkgd2lsbCBtYXRjaA==\" "
+                         " },"
+                         " { \"name\": \"key5\" , "
+                         "   \"algorithm\": \"HMAC-SHA384\" ,"
+                         "  \"secret\": \"dGhpcyBrZXkgd2lsbCBtYXRjaA==\" "
+                         " },"
+                         " { \"name\": \"key6\" , "
+                         "   \"algorithm\": \"HMAC-SHA512\" ,"
+                         "   \"secret\": \"dGhpcyBrZXkgd2lsbCBtYXRjaA==\" "
                          " }"
                          " ]";
 
@@ -614,9 +630,10 @@ TEST_F(TSIGKeyInfoTest, validTSIGKeyList) {
     ASSERT_NO_THROW(parser->build(config_set_));
     ASSERT_NO_THROW(parser->commit());
 
+    std::string ref_secret = "dGhpcyBrZXkgd2lsbCBtYXRjaA==";
     // Verify the correct number of keys are present
     int count =  keys_->size();
-    ASSERT_EQ(3, count);
+    ASSERT_EQ(6, count);
 
     // Find the 1st key and retrieve it.
     TSIGKeyInfoMap::iterator gotit = keys_->find("key1");
@@ -624,7 +641,7 @@ TEST_F(TSIGKeyInfoTest, validTSIGKeyList) {
     TSIGKeyInfoPtr& key = gotit->second;
 
     // Verify the key contents.
-    EXPECT_TRUE(checkKey(key, "key1", "algo1", "secret1"));
+    EXPECT_TRUE(checkKey(key, "key1", TSIGKeyInfo::HMAC_MD5_STR, ref_secret));
 
     // Find the 2nd key and retrieve it.
     gotit = keys_->find("key2");
@@ -632,7 +649,7 @@ TEST_F(TSIGKeyInfoTest, validTSIGKeyList) {
     key = gotit->second;
 
     // Verify the key contents.
-    EXPECT_TRUE(checkKey(key, "key2", "algo2", "secret2"));
+    EXPECT_TRUE(checkKey(key, "key2", TSIGKeyInfo::HMAC_SHA1_STR, ref_secret));
 
     // Find the 3rd key and retrieve it.
     gotit = keys_->find("key3");
@@ -640,7 +657,35 @@ TEST_F(TSIGKeyInfoTest, validTSIGKeyList) {
     key = gotit->second;
 
     // Verify the key contents.
-    EXPECT_TRUE(checkKey(key, "key3", "algo3", "secret3"));
+    EXPECT_TRUE(checkKey(key, "key3", TSIGKeyInfo::HMAC_SHA256_STR,
+                         ref_secret));
+
+    // Find the 4th key and retrieve it.
+    gotit = keys_->find("key4");
+    ASSERT_TRUE(gotit != keys_->end());
+    key = gotit->second;
+
+    // Verify the key contents.
+    EXPECT_TRUE(checkKey(key, "key4", TSIGKeyInfo::HMAC_SHA224_STR,
+                         ref_secret));
+
+    // Find the 5th key and retrieve it.
+    gotit = keys_->find("key5");
+    ASSERT_TRUE(gotit != keys_->end());
+    key = gotit->second;
+
+    // Verify the key contents.
+    EXPECT_TRUE(checkKey(key, "key5", TSIGKeyInfo::HMAC_SHA384_STR,
+                         ref_secret));
+
+    // Find the 6th key and retrieve it.
+    gotit = keys_->find("key6");
+    ASSERT_TRUE(gotit != keys_->end());
+    key = gotit->second;
+
+    // Verify the key contents.
+    EXPECT_TRUE(checkKey(key, "key6", TSIGKeyInfo::HMAC_SHA512_STR,
+                         ref_secret));
 }
 
 /// @brief Tests the enforcement of data validation when parsing DnsServerInfos.
@@ -876,7 +921,7 @@ TEST_F(DdnsDomainTest, ddnsDomainParsing) {
     ASSERT_TRUE(fromJSON(config));
 
     // Add a TSIG key to the test key map, so key validation will pass.
-    addKey("d2_key.tmark.org", "md5", "0123456789");
+    addKey("d2_key.tmark.org", "HMAC-MD5", "GWG/Xfbju4O2iXGqkSu4PQ==");
 
     // Verify that the domain configuration builds and commits without error.
     ASSERT_NO_THROW(parser_->build(config_set_));
@@ -895,6 +940,8 @@ TEST_F(DdnsDomainTest, ddnsDomainParsing) {
     // Verify the name and key_name values.
     EXPECT_EQ("tmark.org", domain->getName());
     EXPECT_EQ("d2_key.tmark.org", domain->getKeyName());
+    ASSERT_TRUE(domain->getTSIGKeyInfo());
+    ASSERT_TRUE(domain->getTSIGKeyInfo()->getTSIGKey());
 
     // Verify that the server list exists and contains the correct number of
     // servers.
@@ -952,8 +999,8 @@ TEST_F(DdnsDomainTest, DdnsDomainListParsing) {
     ASSERT_TRUE(fromJSON(config));
 
     // Add keys to key map so key validation passes.
-    addKey("d2_key.tmark.org", "algo1", "secret1");
-    addKey("d2_key.billcat.net", "algo2", "secret2");
+    addKey("d2_key.tmark.org", "HMAC-MD5", "GWG/Xfbju4O2iXGqkSu4PQ==");
+    addKey("d2_key.billcat.net", "HMAC-MD5", "GWG/Xfbju4O2iXGqkSu4PQ==");
 
     // Create the list parser
     isc::dhcp::ParserPtr list_parser;
@@ -976,6 +1023,8 @@ TEST_F(DdnsDomainTest, DdnsDomainListParsing) {
     // Verify the name and key_name values of the first domain.
     EXPECT_EQ("tmark.org", domain->getName());
     EXPECT_EQ("d2_key.tmark.org", domain->getKeyName());
+    ASSERT_TRUE(domain->getTSIGKeyInfo());
+    ASSERT_TRUE(domain->getTSIGKeyInfo()->getTSIGKey());
 
     // Verify the each of the first domain's servers
     DnsServerInfoStoragePtr servers = domain->getServers();
@@ -1003,6 +1052,8 @@ TEST_F(DdnsDomainTest, DdnsDomainListParsing) {
     // Verify the name and key_name values of the second domain.
     EXPECT_EQ("billcat.net", domain->getName());
     EXPECT_EQ("d2_key.billcat.net", domain->getKeyName());
+    ASSERT_TRUE(domain->getTSIGKeyInfo());
+    ASSERT_TRUE(domain->getTSIGKeyInfo()->getTSIGKey());
 
     // Verify the each of second domain's servers
     servers = domain->getServers();
@@ -1090,13 +1141,13 @@ TEST_F(D2CfgMgrTest, fullConfig) {
                         "\"tsig_keys\": ["
                         "{"
                         "  \"name\": \"d2_key.tmark.org\" , "
-                        "  \"algorithm\": \"md5\" , "
-                        "  \"secret\": \"ssh-dont-tell\"  "
+                        "  \"algorithm\": \"hmac-md5\" , "
+                        "   \"secret\": \"LSWXnfkKZjdPJI5QxlpnfQ==\" "
                         "},"
                         "{"
                         "  \"name\": \"d2_key.billcat.net\" , "
-                        "  \"algorithm\": \"md5\" , "
-                        "  \"secret\": \"ollie-ollie-in-free\"  "
+                        "  \"algorithm\": \"hmac-md5\" , "
+                        "   \"secret\": \"LSWXnfkKZjdPJI5QxlpnfQ==\" "
                         "}"
                         "],"
                         "\"forward_ddns\" : {"

+ 3 - 3
src/bin/d2/tests/d2_process_unittests.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2013  Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2013-2014 Internet Systems Consortium, Inc. ("ISC")
 //
 // Permission to use, copy, modify, and/or distribute this software for any
 // purpose with or without fee is hereby granted, provided that the above
@@ -39,8 +39,8 @@ const char* bad_ip_d2_config = "{ "
                         "\"port\" : 5031, "
                         "\"tsig_keys\": ["
                         "{ \"name\": \"d2_key.tmark.org\" , "
-                        "   \"algorithm\": \"md5\" ,"
-                        "   \"secret\": \"0123456989\" "
+                        "   \"algorithm\": \"HMAC-MD5\" ,"
+                        "   \"secret\": \"LSWXnfkKZjdPJI5QxlpnfQ==\" "
                         "} ],"
                         "\"forward_ddns\" : {"
                         "\"ddns_domains\": [ "

+ 125 - 11
src/bin/d2/tests/d2_update_message_unittests.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2013-2014 Internet Systems Consortium, Inc. ("ISC")
 //
 // Permission to use, copy, modify, and/or distribute this software for any
 // purpose with or without fee is hereby granted, provided that the above
@@ -14,6 +14,7 @@
 
 #include <config.h>
 
+#include <d2/d2_config.h>
 #include <d2/d2_update_message.h>
 #include <d2/d2_zone.h>
 #include <dns/messagerenderer.h>
@@ -201,12 +202,12 @@ TEST_F(D2UpdateMessageTest, fromWire) {
         0x20, 0x01, 0x0D, 0xB8, 0x00, 0x01, 0x00, 0x00,
         0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01
     };
-    InputBuffer buf(bin_msg, sizeof(bin_msg));
 
     // Create an object to be used to decode the message from the wire format.
     D2UpdateMessage msg(D2UpdateMessage::INBOUND);
+
     // Decode the message.
-    ASSERT_NO_THROW(msg.fromWire(buf));
+    ASSERT_NO_THROW(msg.fromWire(bin_msg, sizeof(bin_msg)));
 
     // Check that the message header is valid.
     EXPECT_EQ(0x05AF, msg.getId());
@@ -287,14 +288,14 @@ TEST_F(D2UpdateMessageTest, fromWireInvalidOpcode) {
         0x0, 0x0,   // UPCOUNT=0
         0x0, 0x0    // ADCOUNT=0
     };
-    InputBuffer buf(bin_msg, sizeof(bin_msg));
     // The 'true' argument passed to the constructor turns the
     // message into the parse mode in which the fromWire function
     // can be used to decode the binary mesasage data.
     D2UpdateMessage msg(D2UpdateMessage::INBOUND);
     // When using invalid Opcode, the fromWire function should
     // throw NotUpdateMessage exception.
-    EXPECT_THROW(msg.fromWire(buf), isc::d2::NotUpdateMessage);
+    EXPECT_THROW(msg.fromWire(bin_msg, sizeof(bin_msg)),
+                 isc::d2::NotUpdateMessage);
 }
 
 // This test verifies that the fromWire function throws appropriate exception
@@ -311,14 +312,14 @@ TEST_F(D2UpdateMessageTest, fromWireInvalidQRFlag) {
         0x0, 0x0,   // UPCOUNT=0
         0x0, 0x0    // ADCOUNT=0
     };
-    InputBuffer buf(bin_msg, sizeof(bin_msg));
     // The 'true' argument passed to the constructor turns the
     // message into the parse mode in which the fromWire function
     // can be used to decode the binary mesasage data.
     D2UpdateMessage msg(D2UpdateMessage::INBOUND);
     // When using invalid QR flag, the fromWire function should
     // throw InvalidQRFlag exception.
-    EXPECT_THROW(msg.fromWire(buf), isc::d2::InvalidQRFlag);
+    EXPECT_THROW(msg.fromWire(bin_msg, sizeof(bin_msg)),
+                              isc::d2::InvalidQRFlag);
 }
 
 // This test verifies that the fromWire function throws appropriate exception
@@ -349,7 +350,6 @@ TEST_F(D2UpdateMessageTest, fromWireTooManyZones) {
         0x0, 0x6, // ZTYPE='SOA'
         0x0, 0x1  // ZCLASS='IN'
     };
-    InputBuffer buf(bin_msg, sizeof(bin_msg));
 
     // The 'true' argument passed to the constructor turns the
     // message into the parse mode in which the fromWire function
@@ -357,7 +357,8 @@ TEST_F(D2UpdateMessageTest, fromWireTooManyZones) {
     D2UpdateMessage msg(D2UpdateMessage::INBOUND);
     // When parsing a message with more than one Zone record,
     // exception should be thrown.
-    EXPECT_THROW(msg.fromWire(buf), isc::d2::InvalidZoneSection);
+    EXPECT_THROW(msg.fromWire(bin_msg, sizeof(bin_msg)),
+                 isc::d2::InvalidZoneSection);
 }
 
 // This test verifies that the wire format of the message is produced
@@ -571,12 +572,11 @@ TEST_F(D2UpdateMessageTest, toWireInvalidQRFlag) {
         0x0, 0x0    // ADCOUNT=0
     };
 
-    InputBuffer buf(bin_msg, sizeof(bin_msg));
     // The 'true' argument passed to the constructor turns the
     // message into the parse mode in which the fromWire function
     // can be used to decode the binary mesasage data.
     D2UpdateMessage msg(D2UpdateMessage::INBOUND);
-    ASSERT_NO_THROW(msg.fromWire(buf));
+    ASSERT_NO_THROW(msg.fromWire(bin_msg, sizeof(bin_msg)));
 
     // The message is parsed. The QR Flag should now indicate that
     // it is a Response message.
@@ -588,4 +588,118 @@ TEST_F(D2UpdateMessageTest, toWireInvalidQRFlag) {
     EXPECT_THROW(msg.toWire(renderer), isc::d2::InvalidQRFlag);
 }
 
+// TSIG test
+TEST_F(D2UpdateMessageTest, validTSIG) {
+    // Create a TSIG Key and context
+    std::string secret("this key will match");
+    TSIGKeyPtr right_key;
+    ASSERT_NO_THROW(right_key.reset(new
+                                    TSIGKey(Name("right.com"),
+                                            TSIGKey::HMACMD5_NAME(),
+                                            secret.c_str(), secret.size())));
+
+    TSIGKeyPtr wrong_key;
+    secret = "this key will not match";
+    ASSERT_NO_THROW(wrong_key.reset(new
+                                    TSIGKey(Name("wrong.com"),
+                                            TSIGKey::HMACMD5_NAME(),
+                                            secret.c_str(), secret.size())));
+
+
+    // Build a request message
+    D2UpdateMessage msg;
+    msg.setId(0x1234);
+    msg.setRcode(Rcode(Rcode::NOERROR_CODE));
+    msg.setZone(Name("example.com"), RRClass::IN());
+    RRsetPtr prereq1(new RRset(Name("foo.example.com"), RRClass::NONE(),
+                               RRType::ANY(), RRTTL(0)));
+    msg.addRRset(D2UpdateMessage::SECTION_PREREQUISITE, prereq1);
+    RRsetPtr prereq2(new RRset(Name("bar.example.com"), RRClass::ANY(),
+                               RRType::ANY(), RRTTL(0)));
+    msg.addRRset(D2UpdateMessage::SECTION_PREREQUISITE, prereq2);
+    RRsetPtr updaterr1(new RRset(Name("foo.example.com"), RRClass::IN(),
+                                 RRType::A(), RRTTL(10)));
+    char rdata1[] = {
+        0xA, 0xA , 0x1, 0x1
+    };
+    InputBuffer buf_rdata1(rdata1, 4);
+    updaterr1->addRdata(createRdata(RRType::A(), RRClass::IN(), buf_rdata1,
+                                    buf_rdata1.getLength()));
+    msg.addRRset(D2UpdateMessage::SECTION_UPDATE, updaterr1);
+
+    // Make a context to send the message with and use it to render
+    // the message into the wire format.
+    TSIGContextPtr context;
+    ASSERT_NO_THROW(context.reset(new TSIGContext(*right_key)));
+    MessageRenderer renderer;
+    ASSERT_NO_THROW(msg.toWire(renderer, context.get()));
+
+    // Grab the wire data from the signed message.
+    const void* wire_data = renderer.getData();
+    const size_t wire_size = renderer.getLength();
+
+    // Make a context with the wrong key and use it to convert the wired data.
+    // Verification should fail.
+    D2UpdateMessage msg2(D2UpdateMessage::INBOUND);
+    ASSERT_NO_THROW(context.reset(new TSIGContext(*wrong_key)));
+    ASSERT_THROW(msg2.fromWire(wire_data, wire_size, context.get()),
+                 TSIGVerifyError);
+
+    // Now make a context with the correct key and try again.
+    // If the message passes TSIG verification, then the QR Flag test in
+    // the subsequent call to D2UpdateMessage::validateResponse should
+    // fail because this isn't really received message.
+    ASSERT_NO_THROW(context.reset(new TSIGContext(*right_key)));
+    ASSERT_THROW(msg2.fromWire(wire_data, wire_size, context.get()),
+                 InvalidQRFlag);
+}
+
+// Tests message signing and verification for all supported algorithms.
+TEST_F(D2UpdateMessageTest, allValidTSIG) {
+    std::vector<std::string>algorithms;
+    algorithms.push_back(TSIGKeyInfo::HMAC_MD5_STR);
+    algorithms.push_back(TSIGKeyInfo::HMAC_SHA1_STR);
+    algorithms.push_back(TSIGKeyInfo::HMAC_SHA224_STR);
+    algorithms.push_back(TSIGKeyInfo::HMAC_SHA256_STR);
+    algorithms.push_back(TSIGKeyInfo::HMAC_SHA384_STR);
+    algorithms.push_back(TSIGKeyInfo::HMAC_SHA512_STR);
+
+    dns::Name key_name("test_key");
+    std::string secret("random text for secret");
+    for (int i = 0; i < algorithms.size(); ++i) {
+        dns::TSIGKey key(key_name,
+                         TSIGKeyInfo::stringToAlgorithmName(algorithms[i]),
+                         secret.c_str(), secret.size());
+
+        // Build a request message
+        D2UpdateMessage msg;
+        msg.setId(0x1234);
+        msg.setRcode(Rcode(Rcode::NOERROR_CODE));
+        msg.setZone(Name("example.com"), RRClass::IN());
+
+        // Make a context to send the message with and use it to render
+        // the message into the wire format.
+        TSIGContextPtr context;
+        ASSERT_NO_THROW(context.reset(new TSIGContext(key)));
+        MessageRenderer renderer;
+        ASSERT_NO_THROW(msg.toWire(renderer, context.get()));
+
+        // Grab the wire data from the signed message.
+        const void* wire_data = renderer.getData();
+        const size_t wire_size = renderer.getLength();
+
+        // Create a fresh context to "receive" the message. (We can't use the
+        // one we signed it with, as its expecting a signed response to its
+        // request. Here we are acting like the server).
+        // If the message passes TSIG verification, then the QR Flag test in
+        // the subsequent call to D2UpdateMessage::validateResponse should
+        // fail because this isn't really received message.
+        ASSERT_NO_THROW(context.reset(new TSIGContext(key)));
+        D2UpdateMessage msg2(D2UpdateMessage::INBOUND);
+        ASSERT_THROW(msg2.fromWire(wire_data, wire_size, context.get()),
+                                   InvalidQRFlag);
+    }
+}
+
+
 } // End of anonymous namespace

+ 2 - 2
src/bin/d2/tests/d_test_stubs.cc

@@ -26,8 +26,8 @@ const char* valid_d2_config = "{ "
                         "\"port\" : 5031, "
                         "\"tsig_keys\": ["
                         "{ \"name\": \"d2_key.tmark.org\" , "
-                        "   \"algorithm\": \"md5\" ,"
-                        "   \"secret\": \"0123456989\" "
+                        "   \"algorithm\": \"HMAC-MD5\" ,"
+                        "   \"secret\": \"LSWXnfkKZjdPJI5QxlpnfQ==\" "
                         "} ],"
                         "\"forward_ddns\" : {"
                         "\"ddns_domains\": [ "

+ 160 - 26
src/bin/d2/tests/dns_client_unittests.cc

@@ -14,12 +14,11 @@
 
 #include <config.h>
 #include <d2/dns_client.h>
+#include <dns/opcode.h>
 #include <asiodns/io_fetch.h>
 #include <asiodns/logger.h>
 #include <asiolink/interval_timer.h>
-#include <dns/rcode.h>
-#include <dns/rrclass.h>
-#include <dns/tsig.h>
+#include <dns/messagerenderer.h>
 #include <asio/ip/udp.hpp>
 #include <asio/socket_base.hpp>
 #include <boost/bind.hpp>
@@ -189,6 +188,80 @@ public:
                         *remote);
     }
 
+    // @brief Request handler for testing clients using TSIG
+    //
+    // This callback handler is installed when performing async read on a
+    // socket to emulate reception of the DNS Update request with TSIG by a
+    // server.  As a result, this handler will send an appropriate DNS Update
+    // response message back to the address from which the request has come.
+    //
+    // @param socket A pointer to a socket used to receive a query and send a
+    // response.
+    // @param remote A pointer to an object which specifies the host (address
+    // and port) from which a request has come.
+    // @param receive_length A length (in bytes) of the received data.
+    // @param corrupt_response A bool value which indicates that the server's
+    // response should be invalid (true) or valid (false)
+    // @param client_key TSIG key the server should use to verify the inbound
+    // request.  If the pointer is NULL, the server will not attempt to
+    // verify the request.
+    // @param server_key TSIG key the server should use to sign the outbound
+    // request. If the pointer is NULL, the server will not sign the outbound
+    // response.  If the pointer is not NULL and not the same value as the
+    // client_key, the server will use a new context to sign the response then
+    // the one used to verify it.  This allows us to simulate the server
+    // signing with the wrong key.
+    void TSIGReceiveHandler(udp::socket* socket, udp::endpoint* remote,
+                            size_t receive_length,
+                            TSIGKeyPtr client_key,
+                            TSIGKeyPtr server_key) {
+
+        TSIGContextPtr context;
+        if (client_key) {
+            context.reset(new TSIGContext(*client_key));
+        }
+
+        isc::util::InputBuffer received_data_buffer(receive_buffer_,
+                                                    receive_length);
+
+        dns::Message request(Message::PARSE);
+        request.fromWire(received_data_buffer);
+
+        // If contex is not NULL, then we need to verify the message.
+        if (context) {
+            TSIGError error = context->verify(request.getTSIGRecord(),
+                                              receive_buffer_, receive_length);
+            if (error != TSIGError::NOERROR()) {
+                isc_throw(TSIGVerifyError, "TSIG verification failed: "
+                          << error.toText());
+            }
+        }
+
+        dns::Message response(Message::RENDER);
+        response.setOpcode(Opcode(Opcode::UPDATE_CODE));
+        response.setHeaderFlag(dns::Message::HEADERFLAG_QR, true);
+        response.setQid(request.getQid());
+        response.setRcode(Rcode::NOERROR());
+        dns::Question question(Name("example.com."),
+                                    RRClass::IN(), RRType::SOA());
+        response.addQuestion(question);
+
+        MessageRenderer renderer;
+
+        if (!server_key) {
+            // don't sign the response.
+            context.reset();
+        } else if (server_key != client_key) {
+            // use a different key to sign the response.
+            context.reset(new TSIGContext(*server_key));
+        }  // otherwise use the context based on client_key.
+
+        response.toWire(renderer, context.get());
+        // A response message is now ready to send. Send it!
+        socket->send_to(asio::buffer(renderer.getData(), renderer.getLength()),
+                        *remote);
+    }
+
     // This test verifies that when invalid response placeholder object is
     // passed to a constructor, constructor throws the appropriate exception.
     // It also verifies that the constructor will not throw if the supplied
@@ -229,26 +302,6 @@ public:
                      isc::BadValue);
     }
 
-    // This test verifies that isc::NotImplemented exception is thrown when
-    // attempt to send DNS Update message with TSIG is attempted.
-    void runTSIGTest() {
-        // Create outgoing message. Simply set the required message fields:
-        // error code and Zone section. This is enough to create on-wire format
-        // of this message and send it.
-        D2UpdateMessage message(D2UpdateMessage::OUTBOUND);
-        ASSERT_NO_THROW(message.setRcode(Rcode(Rcode::NOERROR_CODE)));
-        ASSERT_NO_THROW(message.setZone(Name("example.com"), RRClass::IN()));
-
-        const int timeout = 0;
-        // Try to send DNS Update with TSIG key. Currently TSIG is not supported
-        // and therefore we expect an exception.
-        TSIGKey tsig_key("key.example:MSG6Ng==");
-        EXPECT_THROW(dns_client_->doUpdate(service_, IOAddress(TEST_ADDRESS),
-                                           TEST_PORT, message, timeout,
-                                           tsig_key),
-                     isc::NotImplemented);
-    }
-
     // This test verifies the DNSClient behavior when a server does not respond
     // do the DNS Update message. In such case, the callback function is
     // expected to be called and the TIME_OUT error code should be returned.
@@ -359,6 +412,59 @@ public:
         // run_one() to work.
         service_.get_io_service().reset();
     }
+
+    // Performs a single request-response exchange with or without TSIG
+    //
+    // @param client_key TSIG passed to dns_client and also used by the
+    // ""server" to verify the request.
+    // request.
+    // @param server_key TSIG key the "server" should use to sign the response.
+    // If this is NULL, then client_key is used.
+    // @param should_pass indicates if the test should pass.
+    void runTSIGTest(TSIGKeyPtr client_key, TSIGKeyPtr server_key,
+                     bool should_pass = true) {
+        // Tell operator() method if we expect an invalid response.
+        corrupt_response_ = !should_pass;
+
+        // Create a request DNS Update message.
+        D2UpdateMessage message(D2UpdateMessage::OUTBOUND);
+        ASSERT_NO_THROW(message.setRcode(Rcode(Rcode::NOERROR_CODE)));
+        ASSERT_NO_THROW(message.setZone(Name("example.com"), RRClass::IN()));
+
+        // Setup our "loopback" server.
+        udp::socket udp_socket(service_.get_io_service(), asio::ip::udp::v4());
+        udp_socket.set_option(socket_base::reuse_address(true));
+        udp_socket.bind(udp::endpoint(address::from_string(TEST_ADDRESS),
+                                      TEST_PORT));
+        udp::endpoint remote;
+        udp_socket.async_receive_from(asio::buffer(receive_buffer_,
+                                                   sizeof(receive_buffer_)),
+                                      remote,
+                                      boost::bind(&DNSClientTest::
+                                                  TSIGReceiveHandler, this,
+                                                  &udp_socket, &remote, _2,
+                                                  client_key, server_key));
+
+        // The socket is now ready to receive the data. Let's post some request
+        // message then. Set timeout to some reasonable value to make sure that
+        // there is sufficient amount of time for the test to generate a
+        // response.
+        const int timeout = 500;
+        expected_++;
+        dns_client_->doUpdate(service_, IOAddress(TEST_ADDRESS), TEST_PORT,
+                              message, timeout, client_key);
+
+        // Kick of the message exchange by actually running the scheduled
+        // "send" and "receive" operations.
+        service_.run();
+
+        udp_socket.close();
+
+        // Since the callback, operator(), calls stop() on the io_service,
+        // we must reset it in order for subsequent calls to run() or
+        // run_one() to work.
+        service_.get_io_service().reset();
+    }
 };
 
 // Verify that the DNSClient object can be created if provided parameters are
@@ -384,10 +490,38 @@ TEST_F(DNSClientTest, invalidTimeout) {
     runInvalidTimeoutTest();
 }
 
-// Verify that exception is thrown when an attempt to send DNS Update with TSIG
-// is made. This test will be removed/changed once TSIG support is added.
+// Verifies that TSIG can be used to sign requests and verify responses.
 TEST_F(DNSClientTest, runTSIGTest) {
-    runTSIGTest();
+    std::string secret ("key number one");
+    TSIGKeyPtr key_one;
+    ASSERT_NO_THROW(key_one.reset(new
+                                    TSIGKey(Name("one.com"),
+                                            TSIGKey::HMACMD5_NAME(),
+                                            secret.c_str(), secret.size())));
+    secret = "key number two";
+    TSIGKeyPtr key_two;
+    ASSERT_NO_THROW(key_two.reset(new
+                                    TSIGKey(Name("two.com"),
+                                            TSIGKey::HMACMD5_NAME(),
+                                            secret.c_str(), secret.size())));
+    TSIGKeyPtr nokey;
+
+    // Should be able to send and receive with no keys.
+    // Neither client nor server will attempt to sign or verify.
+    runTSIGTest(nokey, nokey);
+
+    // Client signs the request, server verfies but doesn't sign.
+    runTSIGTest(key_one, nokey, false);
+
+    // Client and server use the same key to sign and verify.
+    runTSIGTest(key_one, key_one);
+
+    // Server uses different key to sign the response.
+    runTSIGTest(key_one, key_two, false);
+
+    // Client neither signs nor verifies, server responds with a signed answer
+    // Since we are "liberal" in what we accept this should be ok.
+    runTSIGTest(nokey, key_two);
 }
 
 // Verify that the DNSClient receives the response from DNS and the received

+ 3 - 5
src/bin/d2/tests/nc_add_unittests.cc

@@ -57,10 +57,8 @@ public:
     /// the simulate_send_exception_ flag is true.
     ///
     /// @param comment Parameter is unused, but present in base class method.
-    /// @param use_tsig_ Parameter is unused, but present in base class method.
     ///
-    virtual void sendUpdate(const std::string& /*comment*/,
-                            bool /* use_tsig_ = false */) {
+    virtual void sendUpdate(const std::string& /*comment*/) {
         if (simulate_send_exception_) {
             // Make the flag a one-shot by resetting it.
             simulate_send_exception_ = false;
@@ -290,8 +288,8 @@ TEST(NameAddTransaction, construction) {
     DdnsDomainPtr empty_domain;
 
     ASSERT_NO_THROW(ncr = dhcp_ddns::NameChangeRequest::fromJSON(msg_str));
-    ASSERT_NO_THROW(forward_domain.reset(new DdnsDomain("*", "", servers)));
-    ASSERT_NO_THROW(reverse_domain.reset(new DdnsDomain("*", "", servers)));
+    ASSERT_NO_THROW(forward_domain.reset(new DdnsDomain("*", servers)));
+    ASSERT_NO_THROW(reverse_domain.reset(new DdnsDomain("*", servers)));
 
     // Verify that construction with wrong change type fails.
     EXPECT_THROW(NameAddTransaction(io_service, ncr,

+ 3 - 5
src/bin/d2/tests/nc_remove_unittests.cc

@@ -57,10 +57,8 @@ public:
     /// the simulate_send_exception_ flag is true.
     ///
     /// @param comment Parameter is unused, but present in base class method
-    /// @param use_tsig Parameter is unused, but present in base class method.
     ///
-    virtual void sendUpdate(const std::string& /* comment */,
-                            bool /* use_tsig = false */) {
+    virtual void sendUpdate(const std::string& /* comment */) {
         if (simulate_send_exception_) {
             // Make the flag a one-shot by resetting it.
             simulate_send_exception_ = false;
@@ -292,8 +290,8 @@ TEST(NameRemoveTransaction, construction) {
     DdnsDomainPtr empty_domain;
 
     ASSERT_NO_THROW(ncr = dhcp_ddns::NameChangeRequest::fromJSON(msg_str));
-    ASSERT_NO_THROW(forward_domain.reset(new DdnsDomain("*", "", servers)));
-    ASSERT_NO_THROW(reverse_domain.reset(new DdnsDomain("*", "", servers)));
+    ASSERT_NO_THROW(forward_domain.reset(new DdnsDomain("*", servers)));
+    ASSERT_NO_THROW(reverse_domain.reset(new DdnsDomain("*", servers)));
 
     // Verify that construction with wrong change type fails.
     EXPECT_THROW(NameRemoveTransaction(io_service, ncr,

+ 95 - 14
src/bin/d2/tests/nc_test_utils.cc

@@ -18,6 +18,7 @@
 #include <nc_test_utils.h>
 #include <asio.hpp>
 #include <asiolink/udp_endpoint.h>
+#include <util/encode/base64.h>
 
 #include <gtest/gtest.h>
 
@@ -39,7 +40,8 @@ const bool NO_RDATA = false;
 FauxServer::FauxServer(asiolink::IOService& io_service,
                        asiolink::IOAddress& address, size_t port)
     :io_service_(io_service), address_(address), port_(port),
-     server_socket_(), receive_pending_(false), perpetual_receive_(true) {
+     server_socket_(), receive_pending_(false), perpetual_receive_(true),
+     tsig_key_() {
 
     server_socket_.reset(new asio::ip::udp::socket(io_service_.get_io_service(),
                                                    asio::ip::udp::v4()));
@@ -53,7 +55,7 @@ FauxServer::FauxServer(asiolink::IOService& io_service,
                        DnsServerInfo& server)
     :io_service_(io_service), address_(server.getIpAddress()),
      port_(server.getPort()), server_socket_(), receive_pending_(false),
-     perpetual_receive_(true) {
+     perpetual_receive_(true), tsig_key_() {
     server_socket_.reset(new asio::ip::udp::socket(io_service_.get_io_service(),
                                                    asio::ip::udp::v4()));
     server_socket_->set_option(asio::socket_base::reuse_address(true));
@@ -87,14 +89,21 @@ FauxServer::requestHandler(const asio::error_code& error,
                            std::size_t bytes_recvd,
                            const ResponseMode& response_mode,
                            const dns::Rcode& response_rcode) {
+    receive_pending_ = false;
     // If we encountered an error or received no data then fail.
     // We expect the client to send good requests.
     if (error.value() != 0 || bytes_recvd < 1) {
-        ADD_FAILURE() << "FauxServer receive failed" << error.message();
-        receive_pending_ = false;
+        ADD_FAILURE() << "FauxServer receive failed: " << error.message();
         return;
     }
 
+    // If TSIG key isn't NULL, create a context and use to verify the
+    // request and sign the response.
+    dns::TSIGContextPtr context;
+    if (tsig_key_) {
+        context.reset(new dns::TSIGContext(*tsig_key_));
+    }
+
     // We have a successfully received data. We need to turn it into
     // a request in order to build a proper response.
     // Note D2UpdateMessage is geared towards making requests and
@@ -104,11 +113,21 @@ FauxServer::requestHandler(const asio::error_code& error,
     util::InputBuffer request_buf(receive_buffer_, bytes_recvd);
     try {
         request.fromWire(request_buf);
+
+        // If contex is not NULL, then we need to verify the message.
+        if (context) {
+            dns::TSIGError error = context->verify(request.getTSIGRecord(),
+                                                   receive_buffer_,
+                                                   bytes_recvd);
+            if (error != dns::TSIGError::NOERROR()) {
+                isc_throw(TSIGVerifyError, "TSIG verification failed: "
+                          << error.toText());
+            }
+        }
     } catch (const std::exception& ex) {
         // If the request cannot be parsed, then fail the test.
         // We expect the client to send good requests.
         ADD_FAILURE() << "FauxServer request is corrupt:" << ex.what();
-        receive_pending_ = false;
         return;
     }
 
@@ -131,7 +150,19 @@ FauxServer::requestHandler(const asio::error_code& error,
     dns::MessageRenderer renderer;
     util::OutputBuffer response_buf(TEST_MSG_MAX);
     renderer.setBuffer(&response_buf);
-    response.toWire(renderer);
+
+    if (response_mode == INVALID_TSIG) {
+        // Create a different key to sign the response.
+        std::string secret ("key that doesn't match");
+        dns::TSIGKeyPtr key;
+        ASSERT_NO_THROW(key.reset(new
+                                  dns::TSIGKey(dns::Name("badkey"),
+                                               dns::TSIGKey::HMACMD5_NAME(),
+                                               secret.c_str(), secret.size())));
+        context.reset(new dns::TSIGContext(*key));
+    }
+
+    response.toWire(renderer, context.get());
 
     // If mode is to ship garbage, then stomp on part of the rendered
     // message.
@@ -154,7 +185,6 @@ FauxServer::requestHandler(const asio::error_code& error,
         ADD_FAILURE() << "FauxServer send failed: " << ex.what();
     }
 
-    receive_pending_ = false;
     if (perpetual_receive_) {
         // Schedule the next receive
         receive (response_mode, response_rcode);
@@ -162,6 +192,7 @@ FauxServer::requestHandler(const asio::error_code& error,
 }
 
 
+
 //********************** TimedIO class ***********************
 
 TimedIO::TimedIO()
@@ -205,7 +236,8 @@ TransactionTest::~TransactionTest() {
 
 void
 TransactionTest::setupForIPv4Transaction(dhcp_ddns::NameChangeType chg_type,
-                                         int change_mask) {
+                                         int change_mask,
+                                         const TSIGKeyInfoPtr& tsig_key_info) {
     const char* msg_str =
         "{"
         " \"change_type\" : 0 , "
@@ -231,7 +263,7 @@ TransactionTest::setupForIPv4Transaction(dhcp_ddns::NameChangeType chg_type,
         forward_domain_.reset();
     } else {
         // Create the forward domain and then its servers.
-        forward_domain_ = makeDomain("example.com.");
+        forward_domain_ = makeDomain("example.com.", tsig_key_info);
         addDomainServer(forward_domain_, "forward.example.com",
                         "127.0.0.1", 5301);
         addDomainServer(forward_domain_, "forward2.example.com",
@@ -245,7 +277,7 @@ TransactionTest::setupForIPv4Transaction(dhcp_ddns::NameChangeType chg_type,
         reverse_domain_.reset();
     } else {
         // Create the reverse domain and its server.
-        reverse_domain_ = makeDomain("2.168.192.in.addr.arpa.");
+        reverse_domain_ = makeDomain("2.168.192.in.addr.arpa.", tsig_key_info);
         addDomainServer(reverse_domain_, "reverse.example.com",
                         "127.0.0.1", 5301);
         addDomainServer(reverse_domain_, "reverse2.example.com",
@@ -254,8 +286,16 @@ TransactionTest::setupForIPv4Transaction(dhcp_ddns::NameChangeType chg_type,
 }
 
 void
+TransactionTest::setupForIPv4Transaction(dhcp_ddns::NameChangeType chg_type,
+                                         int change_mask,
+                                         const std::string& key_name) {
+    setupForIPv4Transaction(chg_type, change_mask, makeTSIGKeyInfo(key_name));
+}
+
+void
 TransactionTest::setupForIPv6Transaction(dhcp_ddns::NameChangeType chg_type,
-                                         int change_mask) {
+                                         int change_mask,
+                                         const TSIGKeyInfoPtr& tsig_key_info) {
     const char* msg_str =
         "{"
         " \"change_type\" : 0 , "
@@ -281,7 +321,7 @@ TransactionTest::setupForIPv6Transaction(dhcp_ddns::NameChangeType chg_type,
         forward_domain_.reset();
     } else {
         // Create the forward domain and then its servers.
-        forward_domain_ = makeDomain("example.com.");
+        forward_domain_ = makeDomain("example.com.", tsig_key_info);
         addDomainServer(forward_domain_, "fwd6-server.example.com",
                         "::1", 5301);
         addDomainServer(forward_domain_, "fwd6-server2.example.com",
@@ -295,7 +335,7 @@ TransactionTest::setupForIPv6Transaction(dhcp_ddns::NameChangeType chg_type,
         reverse_domain_.reset();
     } else {
         // Create the reverse domain and its server.
-        reverse_domain_ = makeDomain("1.2001.ip6.arpa.");
+        reverse_domain_ = makeDomain("1.2001.ip6.arpa.", tsig_key_info);
         addDomainServer(reverse_domain_, "rev6-server.example.com",
                         "::1", 5301);
         addDomainServer(reverse_domain_, "rev6-server2.example.com",
@@ -303,6 +343,13 @@ TransactionTest::setupForIPv6Transaction(dhcp_ddns::NameChangeType chg_type,
     }
 }
 
+void
+TransactionTest::setupForIPv6Transaction(dhcp_ddns::NameChangeType chg_type,
+                                         int change_mask,
+                                         const std::string& key_name) {
+    setupForIPv6Transaction(chg_type, change_mask, makeTSIGKeyInfo(key_name));
+}
+
 
 //********************** Functions ****************************
 
@@ -385,12 +432,46 @@ dhcp_ddns::NameChangeRequestPtr makeNcrFromString(const std::string& ncr_str) {
 
 DdnsDomainPtr makeDomain(const std::string& zone_name,
                          const std::string& key_name) {
+    DnsServerInfoStoragePtr servers(new DnsServerInfoStorage());
+    DdnsDomainPtr domain(new DdnsDomain(zone_name, servers,
+                         makeTSIGKeyInfo(key_name)));
+    return (domain);
+}
+
+DdnsDomainPtr makeDomain(const std::string& zone_name,
+                         const TSIGKeyInfoPtr &tsig_key_info) {
     DdnsDomainPtr domain;
     DnsServerInfoStoragePtr servers(new DnsServerInfoStorage());
-    domain.reset(new DdnsDomain(zone_name, key_name, servers));
+    domain.reset(new DdnsDomain(zone_name, servers, tsig_key_info));
     return (domain);
 }
 
+TSIGKeyInfoPtr makeTSIGKeyInfo(const std::string& key_name,
+                           const std::string& secret,
+                           const std::string& algorithm) {
+    TSIGKeyInfoPtr key_info;
+    if (!key_name.empty()) {
+        if (!secret.empty()) {
+            key_info.reset(new TSIGKeyInfo(key_name, algorithm, secret));
+        } else {
+            // Since secret was left blank, we'll convert key_name into a
+            // base64 encoded string and use that.
+            const uint8_t* bytes = reinterpret_cast<const uint8_t*>
+                                                   (key_name.c_str());
+            size_t len = key_name.size();
+            const vector<uint8_t> key_name_v(bytes, bytes + len);
+            std::string key_name64
+                = isc::util::encode::encodeBase64(key_name_v);
+
+            // Now, make the TSIGKeyInfo with a real base64 secret.
+            key_info.reset(new TSIGKeyInfo(key_name, algorithm, key_name64));
+        }
+    }
+
+    return (key_info);
+
+}
+
 void addDomainServer(DdnsDomainPtr& domain, const std::string& name,
                      const std::string& ip, const size_t port) {
     DnsServerInfoPtr server(new DnsServerInfo(name, asiolink::IOAddress(ip),

+ 89 - 7
src/bin/d2/tests/nc_test_utils.h

@@ -40,8 +40,9 @@ typedef boost::shared_ptr<asio::ip::udp::socket> SocketPtr;
 class FauxServer {
 public:
     enum  ResponseMode {
-        USE_RCODE,   // Generate a response with a given RCODE
-        CORRUPT_RESP  // Generate a corrupt response
+        USE_RCODE,    // Generate a response with a given RCODE
+        CORRUPT_RESP, // Generate a corrupt response
+        INVALID_TSIG  // Generate a repsonse with the wrong TSIG key
     };
 
     // Reference to IOService to use for IO processing.
@@ -63,6 +64,9 @@ public:
     // a receive has been completed, a new one will be automatically
     // initiated.
     bool perpetual_receive_;
+    // TSIG Key to use to verify requests and sign responses.  If its
+    // NULL TSIG is not used.
+    dns::TSIGKeyPtr tsig_key_;
 
     /// @brief Constructor
     ///
@@ -96,7 +100,9 @@ public:
     /// @brief Socket IO Completion callback
     ///
     /// This method servers as the Server's UDP socket receive callback handler.
-    /// When the receive completes the handler is invoked with the
+    /// When the receive completes the handler is invoked with the parameters
+    /// listed.
+    ///
     /// @param error result code of the receive (determined by asio layer)
     /// @param bytes_recvd number of bytes received, if any
     /// @param response_mode type of response the handler should produce
@@ -111,6 +117,14 @@ public:
     bool isReceivePending() {
         return receive_pending_;
     }
+
+    /// @brief Sets the TSIG key to the given value.
+    ///
+    /// @param tsig_key Pointer to the TSIG key to use.  If the pointer is
+    /// empty, TSIG will not be used.
+    void setTSIGKey (const dns::TSIGKeyPtr& tsig_key) {
+        tsig_key_ = tsig_key;
+    }
 };
 
 /// @brief Provides a means to process IOService IO for a finite amount of time.
@@ -187,8 +201,28 @@ public:
     /// CHG_REMOVE.
     /// @param change_mask determines which change directions are requested
     /// FORWARD_CHG, REVERSE_CHG, or FWD_AND_REV_CHG.
+    /// @param tsig_key_info pointer to the TSIGKeyInfo to assign to both
+    /// domains in the transaction.  This will cause the transaction to
+    /// use TSIG.  If the pointer is empty, TSIG will not be used.
     void setupForIPv4Transaction(dhcp_ddns::NameChangeType change_type,
-                                 int change_mask);
+                                 int change_mask,
+                                 const TSIGKeyInfoPtr& tsig_key_info =
+                                 TSIGKeyInfoPtr());
+
+    /// @brief Creates a transaction which requests an IPv4 DNS update.
+    ///
+    /// Convenience wrapper around the above method which accepts a string
+    /// key_name from which the TSIGKeyInfo is constructed.  Note the string
+    /// may not be blank.
+    ///
+    /// @param change_type selects the type of change requested, CHG_ADD or
+    /// CHG_REMOVE.
+    /// @param change_mask determines which change directions are requested
+    /// FORWARD_CHG, REVERSE_CHG, or FWD_AND_REV_CHG.
+    /// @param key_name value to use to create TSIG key. The value may not
+    /// be blank.
+    void setupForIPv4Transaction(dhcp_ddns::NameChangeType change_type,
+                                 int change_mask, const std::string& key_name);
 
     /// @brief Creates a transaction which requests an IPv6 DNS update.
     ///
@@ -201,8 +235,29 @@ public:
     /// CHG_REMOVE.
     /// @param change_mask determines which change directions are requested
     /// FORWARD_CHG, REVERSE_CHG, or FWD_AND_REV_CHG.
+    /// @param tsig_key_info pointer to the TSIGKeyInfo to assign to both
+    /// domains in the transaction.  This will cause the transaction to
+    /// use TSIG.  If the pointer is empty, TSIG will not be used.
+    void setupForIPv6Transaction(dhcp_ddns::NameChangeType change_type,
+                                 int change_mask,
+                                 const TSIGKeyInfoPtr& tsig_key_info =
+                                 TSIGKeyInfoPtr());
+
+    /// @brief Creates a transaction which requests an IPv6 DNS update.
+    ///
+    /// Convenience wrapper around the above method which accepts a string
+    /// key_name from which the TSIGKeyInfo is constructed.  Note the string
+    /// may not be blank.
+    ///
+    /// @param change_type selects the type of change requested, CHG_ADD or
+    /// CHG_REMOVE.
+    /// @param change_mask determines which change directions are requested
+    /// FORWARD_CHG, REVERSE_CHG, or FWD_AND_REV_CHG.
+    /// @param key_name value to use to create TSIG key, if blank TSIG will not
+    /// be used.
     void setupForIPv6Transaction(dhcp_ddns::NameChangeType change_type,
-                                 int change_mask);
+                                 int change_mask, const std::string& key_name);
+
 };
 
 
@@ -336,11 +391,38 @@ dhcp_ddns::NameChangeRequestPtr makeNcrFromString(const std::string& ncr_str);
 /// @brief Creates a DdnsDomain with the one server.
 ///
 /// @param zone_name zone name of the domain
-/// @param key_name TSIG key name of the TSIG key for this domain
+/// @param key_name TSIG key name of the TSIG key for this domain. It will
+/// create a TSIGKeyInfo based on the key_name and assign it to the domain.
 ///
 /// @throw Underlying methods may throw.
 extern DdnsDomainPtr makeDomain(const std::string& zone_name,
-                                const std::string& key_name = "");
+                                const std::string& key_name);
+
+/// @brief Creates a DdnsDomain with the one server.
+///
+/// @param zone_name zone name of the domain
+/// @param tsig_key_info pointer to the TSIGInfog key for this domain.
+/// Defaults to an empty pointer, meaning this domain has no key.
+///
+/// @throw Underlying methods may throw.
+extern DdnsDomainPtr makeDomain(const std::string& zone_name,
+                                const TSIGKeyInfoPtr&
+                                tsig_key_info = TSIGKeyInfoPtr());
+
+/// @brief Creates a TSIGKeyInfo
+///
+/// @param key_name name of the key
+/// @param secret key secret data as a base64 encoded string. If blank,
+/// then the secret value will be generated from key_name.
+/// @param algorithm algorithm to use. Defaults to MD5.
+/// @return a TSIGKeyInfoPtr for the newly created key.  If key_name is blank
+/// the pointer will be empty.
+/// @throw Underlying methods may throw.
+extern
+TSIGKeyInfoPtr makeTSIGKeyInfo(const std::string& key_name,
+                               const std::string& secret = "",
+                               const std::string& algorithm
+                               = TSIGKeyInfo::HMAC_MD5_STR);
 
 /// @brief Creates a DnsServerInfo and adds it to the given DdnsDomain.
 ///

+ 162 - 9
src/bin/d2/tests/nc_trans_unittests.cc

@@ -78,15 +78,11 @@ public:
     /// sendUpdate without incorporating exectution of the state model
     /// into the test.
     /// It sets the DNS status update and posts IO_COMPLETED_EVT as does
-    /// the normal callback, but rather than invoking runModel it stops
-    /// the IO service.  This allows tests to be constructed that consisted
-    /// of generating a DNS request and invoking sendUpdate to post it and
-    /// wait for response.
+    /// the normal callback.
     virtual void operator()(DNSClient::Status status) {
         if (use_stub_callback_) {
             setDnsUpdateStatus(status);
             postNextEvent(IO_COMPLETED_EVT);
-            getIOService()->stop();
         } else {
             // For tests which need to use the real callback.
             NameChangeTransaction::operator()(status);
@@ -288,13 +284,28 @@ public:
     virtual ~NameChangeTransactionTest() {
     }
 
+
     /// @brief  Instantiates a NameChangeStub test transaction
     /// The transaction is constructed around a predefined (i.e "canned")
     /// NameChangeRequest. The request has both forward and reverse DNS
     /// changes requested, and both forward and reverse domains are populated.
-    NameChangeStubPtr makeCannedTransaction() {
+    /// @param key_name value to use to create TSIG key, if blank TSIG will not
+    /// be used.
+    NameChangeStubPtr makeCannedTransaction(const TSIGKeyInfoPtr&
+                                            tsig_key_info = TSIGKeyInfoPtr()) {
+        // Creates IPv4 remove request, forward, and reverse domains.
+        setupForIPv4Transaction(dhcp_ddns::CHG_ADD, FWD_AND_REV_CHG,
+                                tsig_key_info);
+
+        // Now create the test transaction as would occur in update manager.
+        // Instantiate the transaction as would be done by update manager.
+        return (NameChangeStubPtr(new NameChangeStub(io_service_, ncr_,
+                                  forward_domain_, reverse_domain_)));
+    }
+
+    NameChangeStubPtr makeCannedTransaction(const std::string& key_name) {
         // Creates IPv4 remove request, forward, and reverse domains.
-        setupForIPv4Transaction(dhcp_ddns::CHG_ADD, FWD_AND_REV_CHG);
+        setupForIPv4Transaction(dhcp_ddns::CHG_ADD, FWD_AND_REV_CHG, key_name);
 
         // Now create the test transaction as would occur in update manager.
         // Instantiate the transaction as would be done by update manager.
@@ -374,8 +385,8 @@ TEST(NameChangeTransaction, construction) {
     DdnsDomainPtr empty_domain;
 
     ASSERT_NO_THROW(ncr = dhcp_ddns::NameChangeRequest::fromJSON(msg_str));
-    ASSERT_NO_THROW(forward_domain.reset(new DdnsDomain("*", "", servers)));
-    ASSERT_NO_THROW(reverse_domain.reset(new DdnsDomain("*", "", servers)));
+    ASSERT_NO_THROW(forward_domain.reset(new DdnsDomain("*", servers)));
+    ASSERT_NO_THROW(reverse_domain.reset(new DdnsDomain("*", servers)));
 
     // Verify that construction with a null IOServicePtr fails.
     // @todo Subject to change if multi-threading is implemented.
@@ -1021,6 +1032,148 @@ TEST_F(NameChangeTransactionTest, sendUpdate) {
     EXPECT_EQ("response.example.com.", zone->getName().toText());
 }
 
+/// @brief Tests that an unsigned response to a signed request is an error
+TEST_F(NameChangeTransactionTest, tsigUnsignedResponse) {
+    NameChangeStubPtr name_change;
+    ASSERT_NO_THROW(name_change = makeCannedTransaction("key_one"));
+    ASSERT_NO_THROW(name_change->initDictionaries());
+    ASSERT_TRUE(name_change->selectFwdServer());
+
+    // Create a server and start it listening.
+    FauxServer server(*io_service_, *(name_change->getCurrentServer()));
+    server.receive (FauxServer::USE_RCODE, dns::Rcode::NOERROR());
+
+    // Do the udpate.
+    ASSERT_NO_FATAL_FAILURE(doOneExchange(name_change));
+
+    // Verify that next event is IO_COMPLETED_EVT and DNS status is
+    // INVALID_RESPONSE.
+    ASSERT_EQ(NameChangeTransaction::IO_COMPLETED_EVT,
+              name_change->getNextEvent());
+
+    ASSERT_EQ(DNSClient::INVALID_RESPONSE, name_change->getDnsUpdateStatus());
+
+    // When TSIG errors occur, only the message header (including Rcode) is
+    // unpacked.  In this case, it should be NOERROR but have no other
+    // information.
+    D2UpdateMessagePtr response = name_change->getDnsUpdateResponse();
+    ASSERT_TRUE(response);
+    ASSERT_EQ(dns::Rcode::NOERROR().getCode(), response->getRcode().getCode());
+    EXPECT_FALSE(response->getZone());
+}
+
+/// @brief Tests that a response signed with the wrong key is an error
+TEST_F(NameChangeTransactionTest, tsigInvalidResponse) {
+    NameChangeStubPtr name_change;
+    ASSERT_NO_THROW(name_change = makeCannedTransaction("key_one"));
+    ASSERT_NO_THROW(name_change->initDictionaries());
+    ASSERT_TRUE(name_change->selectFwdServer());
+
+    // Create a server, tell it to sign responses with a "random" key,
+    // then start it listening.
+    FauxServer server(*io_service_, *(name_change->getCurrentServer()));
+    server.receive (FauxServer::INVALID_TSIG, dns::Rcode::NOERROR());
+
+    // Do the udpate.
+    ASSERT_NO_FATAL_FAILURE(doOneExchange(name_change));
+
+    // Verify that next event is IO_COMPLETED_EVT and DNS status is
+    // INVALID_RESPONSE.
+    ASSERT_EQ(NameChangeTransaction::IO_COMPLETED_EVT,
+              name_change->getNextEvent());
+
+    ASSERT_EQ(DNSClient::INVALID_RESPONSE, name_change->getDnsUpdateStatus());
+
+    // When TSIG errors occur, only the message header (including Rcode) is
+    // unpacked.  In this case, it should be NOERROR but have no other
+    // information.
+    D2UpdateMessagePtr response = name_change->getDnsUpdateResponse();
+    ASSERT_TRUE(response);
+    ASSERT_EQ(dns::Rcode::NOERROR().getCode(), response->getRcode().getCode());
+    EXPECT_FALSE(response->getZone());
+}
+
+/// @brief Tests that a signed response to an unsigned request is ok.
+/// Currently our policy is to accept a signed response to an unsigned request
+/// even though the spec says a server MUST not do that.
+TEST_F(NameChangeTransactionTest, tsigUnexpectedSignedResponse) {
+    NameChangeStubPtr name_change;
+    ASSERT_NO_THROW(name_change = makeCannedTransaction());
+    ASSERT_NO_THROW(name_change->initDictionaries());
+    ASSERT_TRUE(name_change->selectFwdServer());
+
+    // Create a server, tell it to sign responses with a "random" key,
+    // then start it listening.
+    FauxServer server(*io_service_, *(name_change->getCurrentServer()));
+    server.receive (FauxServer::INVALID_TSIG, dns::Rcode::NOERROR());
+
+    // Perform an update without TSIG.
+    ASSERT_NO_FATAL_FAILURE(doOneExchange(name_change));
+
+    // Verify that next event is IO_COMPLETED_EVT and DNS status is SUCCESS.
+    ASSERT_EQ(NameChangeTransaction::IO_COMPLETED_EVT,
+              name_change->getNextEvent());
+
+    ASSERT_EQ(DNSClient::SUCCESS, name_change->getDnsUpdateStatus());
+
+    D2UpdateMessagePtr response = name_change->getDnsUpdateResponse();
+    ASSERT_TRUE(response);
+    ASSERT_EQ(dns::Rcode::NOERROR().getCode(), response->getRcode().getCode());
+    D2ZonePtr zone = response->getZone();
+    EXPECT_TRUE(zone);
+    EXPECT_EQ("response.example.com.", zone->getName().toText());
+}
+
+/// @brief Tests that a TSIG udpate succeeds when client and server both use
+/// the right key.  Runs the test for all supported algorithms.
+TEST_F(NameChangeTransactionTest, tsigAllValid) {
+    std::vector<std::string>algorithms;
+    algorithms.push_back(TSIGKeyInfo::HMAC_MD5_STR);
+    algorithms.push_back(TSIGKeyInfo::HMAC_SHA1_STR);
+    algorithms.push_back(TSIGKeyInfo::HMAC_SHA224_STR);
+    algorithms.push_back(TSIGKeyInfo::HMAC_SHA256_STR);
+    algorithms.push_back(TSIGKeyInfo::HMAC_SHA384_STR);
+    algorithms.push_back(TSIGKeyInfo::HMAC_SHA512_STR);
+
+    for (int i = 0; i < algorithms.size(); ++i) {
+        SCOPED_TRACE (algorithms[i]);
+        TSIGKeyInfoPtr key;
+        ASSERT_NO_THROW(key.reset(new TSIGKeyInfo("test_key",
+                                                  algorithms[i],
+                                                  "GWG/Xfbju4O2iXGqkSu4PQ==")));
+        NameChangeStubPtr name_change;
+        ASSERT_NO_THROW(name_change = makeCannedTransaction(key));
+        ASSERT_NO_THROW(name_change->initDictionaries());
+        ASSERT_TRUE(name_change->selectFwdServer());
+
+        // Create a server, set its TSIG key, and then start it listening.
+        FauxServer server(*io_service_, *(name_change->getCurrentServer()));
+        // Since we create a new server instance each time we need to tell
+        // it not reschedule receives automatically.
+        server.perpetual_receive_ = false;
+        server.setTSIGKey(key->getTSIGKey());
+        server.receive (FauxServer::USE_RCODE, dns::Rcode::NOERROR());
+
+        // Do the update.
+        ASSERT_NO_FATAL_FAILURE(doOneExchange(name_change));
+
+        // Verify that next event is IO_COMPLETED_EVT and DNS status is SUCCESS.
+        ASSERT_EQ(NameChangeTransaction::IO_COMPLETED_EVT,
+                  name_change->getNextEvent());
+
+        ASSERT_EQ(DNSClient::SUCCESS, name_change->getDnsUpdateStatus());
+
+        D2UpdateMessagePtr response = name_change->getDnsUpdateResponse();
+        ASSERT_TRUE(response);
+        ASSERT_EQ(dns::Rcode::NOERROR().getCode(),
+                  response->getRcode().getCode());
+        D2ZonePtr zone = response->getZone();
+        EXPECT_TRUE(zone);
+        EXPECT_EQ("response.example.com.", zone->getName().toText());
+    }
+}
+
+
 /// @brief Tests the prepNewRequest method
 TEST_F(NameChangeTransactionTest, prepNewRequest) {
     NameChangeStubPtr name_change;

+ 5 - 0
src/lib/dns/tsig.h

@@ -16,6 +16,7 @@
 #define TSIG_H 1
 
 #include <boost/noncopyable.hpp>
+#include <boost/shared_ptr.hpp>
 
 #include <exceptions/exceptions.h>
 
@@ -430,6 +431,10 @@ private:
     struct TSIGContextImpl;
     TSIGContextImpl* impl_;
 };
+
+typedef boost::shared_ptr<TSIGContext> TSIGContextPtr;
+typedef boost::shared_ptr<TSIGKey> TSIGKeyPtr;
+
 }
 }