Browse Source

[3432] Added support for TSIG to D2UpdateMessage and DNSClient

Change D2UpdateMessage to support TSIG signing and verification via its
toWire and fromWire methods.  Both now accept a pointer to a TSIGContext,
which they should use, if its not NULL.

Implemented DNSCLient::doUpdate variant that accepts a TSIGKey. It will
use the key to create a TSIGContext that will then be used to sign the
outbound request and to verify the response in the operator() method.

Added appropriate unit tests.
Thomas Markwalder 11 years ago
parent
commit
dd0024cf1e

+ 18 - 4
src/bin/d2/d2_update_message.cc

@@ -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.

+ 34 - 10
src/bin/d2/d2_update_message.h

@@ -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_ctx A TSIG context that is to be used to verify the
+    /// message. If NULL TSIG verification will not be attempted.
+    void fromWire(const void* data, size_t datalen,
+                  dns::TSIGContext* const tsig_context = NULL);
     //@}
 
 private:

+ 44 - 22
src/bin/d2/dns_client.cc

@@ -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,6 +75,14 @@ public:
     // type, representing a response from the server is set.
     virtual void operator()(asiodns::IOFetch::Result result);
 
+    // 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 dns::TSIGKey& tsig_key);
+
     // Starts asynchronous DNS Update.
     void doUpdate(asiolink::IOService& io_service,
                   const asiolink::IOAddress& ns_addr,
@@ -130,7 +140,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 +149,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,6 +188,16 @@ 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 dns::TSIGKey& tsig_key) {
+    tsig_context_.reset(new TSIGContext(tsig_key));
+    doUpdate(io_service, ns_addr, ns_port, update, wait);
+}
 
 void
 DNSClientImpl::doUpdate(asiolink::IOService& io_service,
@@ -181,6 +205,15 @@ DNSClientImpl::doUpdate(asiolink::IOService& io_service,
                         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 > DNSClient::getMaxTimeout()) {
+        isc_throw(isc::BadValue, "A timeout value for DNS Update request must"
+                  " not exceed " << DNSClient::getMaxTimeout()
+                  << ". Provided timeout value is '" << wait << "'");
+    }
+
     // 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 +226,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
@@ -228,14 +261,13 @@ 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");
+DNSClient::doUpdate(asiolink::IOService& io_service,
+                    const IOAddress& ns_addr,
+                    const uint16_t ns_port,
+                    D2UpdateMessage& update,
+                    const unsigned int wait,
+                    const dns::TSIGKey& tsig_key) {
+    impl_->doUpdate(io_service, ns_addr, ns_port, update, wait, tsig_key);
 }
 
 void
@@ -244,19 +276,9 @@ DNSClient::doUpdate(asiolink::IOService& io_service,
                     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);
 }
 
-
-
 } // namespace d2
 } // namespace isc
 

+ 74 - 10
src/bin/d2/tests/d2_update_message_unittests.cc

@@ -201,12 +201,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 +287,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 +311,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 +349,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 +356,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 +571,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 +587,69 @@ 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);
+}
+
 } // End of anonymous namespace

+ 142 - 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,57 @@ public:
                         *remote);
     }
 
+    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 +279,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 +389,64 @@ 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_++;
+        if (client_key) {
+            dns_client_->doUpdate(service_, IOAddress(TEST_ADDRESS), TEST_PORT,
+                                  message, timeout, *client_key);
+        } else {
+            dns_client_->doUpdate(service_, IOAddress(TEST_ADDRESS), TEST_PORT,
+                                  message, timeout);
+        }
+
+        // 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 +472,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

+ 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;
+
 }
 }