Commit eef87432 authored by JINMEI Tatuya's avatar JINMEI Tatuya

[trac893] implemented major logic of TSIG verify

parent df8ef3ff
......@@ -40,6 +40,9 @@ BUILT_SOURCES += rdata_tsig_toWire1.wire rdata_tsig_toWire2.wire
BUILT_SOURCES += rdata_tsig_toWire3.wire rdata_tsig_toWire4.wire
BUILT_SOURCES += rdata_tsig_toWire5.wire
BUILT_SOURCES += tsigrecord_toWire1.wire tsigrecord_toWire2.wire
BUILT_SOURCES += tsig_verify1.wire tsig_verify2.wire tsig_verify3.wire
BUILT_SOURCES += tsig_verify4.wire tsig_verify5.wire tsig_verify6.wire
BUILT_SOURCES += tsig_verify7.wire tsig_verify8.wire tsig_verify9.wire
# NOTE: keep this in sync with real file listing
# so is included in tarball
......@@ -108,6 +111,9 @@ EXTRA_DIST += rdata_tsig_toWire1.spec rdata_tsig_toWire2.spec
EXTRA_DIST += rdata_tsig_toWire3.spec rdata_tsig_toWire4.spec
EXTRA_DIST += rdata_tsig_toWire5.spec
EXTRA_DIST += tsigrecord_toWire1.spec tsigrecord_toWire2.spec
EXTRA_DIST += tsig_verify1.spec tsig_verify2.spec tsig_verify3.spec
EXTRA_DIST += tsig_verify4.spec tsig_verify5.spec tsig_verify6.spec
EXTRA_DIST += tsig_verify7.spec tsig_verify8.spec tsig_verify9.spec
.spec.wire:
./gen-wiredata.py -o $@ $<
......@@ -283,9 +283,8 @@ class NS(RR):
f.write('# NS name=%s\n' % (self.nsname))
f.write('%s\n' % nsname_wire)
class SOA:
# this currently doesn't support name compression within the RDATA.
rdlen = -1 # auto-calculate
class SOA(RR):
rdlen = None # auto-calculate
mname = 'ns.example.com'
rname = 'root.example.com'
serial = 2010012601
......@@ -296,11 +295,9 @@ class SOA:
def dump(self, f):
mname_wire = encode_name(self.mname)
rname_wire = encode_name(self.rname)
rdlen = self.rdlen
if rdlen < 0:
rdlen = int(20 + len(mname_wire) / 2 + len(str(rname_wire)) / 2)
f.write('\n# SOA RDATA (RDLEN=%d)\n' % rdlen)
f.write('%04x\n' % rdlen);
if self.rdlen is None:
self.rdlen = int(20 + len(mname_wire) / 2 + len(str(rname_wire)) / 2)
self.dump_header(f, self.rdlen)
f.write('# NNAME=%s RNAME=%s\n' % (self.mname, self.rname))
f.write('%s %s\n' % (mname_wire, rname_wire))
f.write('# SERIAL(%d) REFRESH(%d) RETRY(%d) EXPIRE(%d) MINIMUM(%d)\n' %
......
#
# An example of signed AXFR request
#
[custom]
sections: header:question:tsig
[header]
id: 0x3410
arcount: 1
[question]
rrtype: AXFR
[tsig]
as_rr: True
rr_name: www.example.com
algorithm: hmac-md5
time_signed: 0x4da8e951
mac_size: 16
mac: 0x35b2fd08268781634400c7c8a5533b13
original_id: 0x3410
#
# An example of signed AXFR response
#
[custom]
sections: header:question:soa:tsig
[header]
id: 0x3410
aa: 1
qr: 1
ancount: 1
arcount: 1
[question]
rrtype: AXFR
[soa]
# note that names are compressed in this RR
as_rr: True
rr_name: ptr=12
mname: ns.ptr=12
rname: root.ptr=12
serial: 2011041503
refresh: 7200
retry: 3600
expire: 2592000
[tsig]
as_rr: True
rr_name: www.example.com
algorithm: hmac-md5
time_signed: 0x4da8e951
mac_size: 16
mac: 0xbdd612cd2c7f9e0648bd6dc23713e83c
original_id: 0x3410
#
# An example of signed AXFR response (continued)
#
[custom]
sections: header:ns:tsig
[header]
id: 0x3410
aa: 1
qr: 1
qdcount: 0
ancount: 1
arcount: 1
[ns]
# note that names are compressed in this RR
as_rr: True
rr_name: example.com.
nsname: ns.ptr=12
[tsig]
as_rr: True
rr_name: www.example.com
algorithm: hmac-md5
time_signed: 0x4da8e951
mac_size: 16
mac: 0x102458f7f62ddd7d638d746034130968
original_id: 0x3410
#
# An example of signed DNS response with bogus MAC
#
[custom]
sections: header:question:a:tsig
[header]
id: 0x2d65
aa: 1
qr: 1
rd: 1
ancount: 1
arcount: 1
[question]
name: www.example.com
[a]
as_rr: True
rr_name: ptr=12
[tsig]
as_rr: True
rr_name: www.example.com
algorithm: hmac-md5
time_signed: 0x4da8877a
mac_size: 16
# bogus MAC
mac: 0xdeadbeefdeadbeefdeadbeefdeadbeef
original_id: 0x2d65
#
# An example of signed DNS response
#
[custom]
sections: header:question:a:tsig
[header]
id: 0x2d65
aa: 1
qr: 1
rd: 1
ancount: 1
arcount: 1
[question]
name: www.example.com
[a]
as_rr: True
rr_name: ptr=12
[tsig]
as_rr: True
rr_name: www.example.com
algorithm: hmac-md5
time_signed: 0x4da8877a
mac_size: 16
mac: 0x8fcda66a7cd1a3b9948eb1869d384a9f
original_id: 0x2d65
#
# Forwarded DNS query message with TSIG signed (header ID != orig ID)
#
[custom]
sections: header:question:tsig
[header]
id: 0x1035
rd: 1
arcount: 1
[question]
name: www.example.com
[tsig]
as_rr: True
# TSIG QNAME won't be compressed
rr_name: www.example.com
algorithm: hmac-md5
time_signed: 0x4da8877a
mac_size: 16
mac: 0x227026ad297beee721ce6c6fff1e9ef3
original_id: 0x2d65
#
# DNS query message with TSIG that has empty MAC (invalidly)
#
[custom]
sections: header:question:tsig
[header]
id: 0x2d65
rd: 1
arcount: 1
[question]
name: www.example.com
[tsig]
as_rr: True
# TSIG QNAME won't be compressed
rr_name: www.example.com
algorithm: hmac-md5
time_signed: 0x4da8877a
mac_size: 0
mac: ''
original_id: 0x2d65
#
# DNS query message with TSIG that has empty MAC + BADKEY error
#
[custom]
sections: header:question:tsig
[header]
id: 0x2d65
rd: 1
arcount: 1
[question]
name: www.example.com
[tsig]
as_rr: True
# TSIG QNAME won't be compressed
rr_name: www.example.com
algorithm: hmac-md5
time_signed: 0x4da8877a
mac_size: 0
mac: ''
# 17: BADKEY
error: 17
original_id: 0x2d65
#
# A simple DNS query message with TSIG signed, but TSIG key and algorithm
# names have upper case characters (unusual)
#
[custom]
sections: header:question:tsig
[header]
id: 0x2d65
rd: 1
arcount: 1
[question]
name: www.example.com
[tsig]
as_rr: True
rr_name: WWW.EXAMPLE.COM
algorithm: HMAC-MD5.SIG-ALG.REG.INT
time_signed: 0x4da8877a
mac_size: 16
mac: 0x227026ad297beee721ce6c6fff1e9ef3
original_id: 0x2d65
This diff is collapsed.
......@@ -45,14 +45,15 @@ namespace dns {
namespace {
typedef boost::shared_ptr<HMAC> HMACPtr;
// This singleton key is used when the TSIG context is constructed with no
// matching key. The key name and algorithm won't be used in subsequent
// sign/verify, so the their values don't matter.
const TSIGKey&
getDummyTSIGKey() {
static TSIGKey dummy_key(Name::ROOT_NAME(), TSIGKey::HMACMD5_NAME(), NULL,
0);
return (dummy_key);
// TSIG uses 48-bit unsigned integer to represent time signed.
// Since gettimeWrapper() returns a 64-bit *signed* integer, we
// make sure it's stored in an unsigned 64-bit integer variable and
// represents a value in the expected range. (In reality, however,
// gettimeWrapper() will return a positive integer that will fit
// in 48 bits)
uint64_t
getTSIGTime() {
return (detail::gettimeWrapper() & 0x0000ffffffffffffULL);
}
}
......@@ -61,6 +62,32 @@ struct TSIGContext::TSIGContextImpl {
state_(INIT), key_(key), error_(Rcode::NOERROR()),
previous_timesigned_(0)
{}
TSIGError postVerifyUpdate(TSIGError error, const void* digest,
size_t digest_len)
{
if (state_ == INIT) {
state_ = RECEIVED_REQUEST;
} else if (state_ == WAIT_RESPONSE && error == TSIGError::NOERROR()) {
state_ = VERIFIED_RESPONSE;
}
if (digest != NULL) {
previous_digest_.assign(static_cast<const uint8_t*>(digest),
static_cast<const uint8_t*>(digest) +
digest_len);
}
error_ = error;
return (error);
}
void digestPreviousMAC(OutputBuffer& buffer, HMACPtr hmac) const;
void digestTSIGVariables(OutputBuffer& buffer, HMACPtr hmac,
uint16_t rrclass, uint32_t rrttl,
uint64_t time_signed, uint16_t fudge,
uint16_t error, uint16_t otherlen,
const void* otherdata,
bool time_variables_only) const;
void digestDNSMessage(OutputBuffer& buffer, HMACPtr hmac,
uint16_t qid, const void* data,
size_t data_len) const;
State state_;
const TSIGKey key_;
vector<uint8_t> previous_digest_;
......@@ -68,6 +95,89 @@ struct TSIGContext::TSIGContextImpl {
uint64_t previous_timesigned_; // only meaningful for response with BADTIME
};
void
TSIGContext::TSIGContextImpl::digestPreviousMAC(OutputBuffer& buffer,
HMACPtr hmac) const
{
buffer.clear();
const uint16_t previous_digest_len(previous_digest_.size());
buffer.writeUint16(previous_digest_len);
if (previous_digest_len != 0) {
buffer.writeData(&previous_digest_[0], previous_digest_len);
}
hmac->update(buffer.getData(), buffer.getLength());
}
void
TSIGContext::TSIGContextImpl::digestTSIGVariables(
OutputBuffer& buffer, HMACPtr hmac, uint16_t rrclass, uint32_t rrttl,
uint64_t time_signed, uint16_t fudge, uint16_t error, uint16_t otherlen,
const void* otherdata, bool time_variables_only) const
{
buffer.clear();
if (!time_variables_only) {
key_.getKeyName().toWire(buffer);
buffer.writeUint16(rrclass);
buffer.writeUint32(rrttl);
key_.getAlgorithmName().toWire(buffer);
}
buffer.writeUint16(time_signed >> 32);
buffer.writeUint32(time_signed & 0xffffffff);
buffer.writeUint16(fudge);
hmac->update(buffer.getData(), buffer.getLength());
if (!time_variables_only) {
buffer.clear();
buffer.writeUint16(error);
buffer.writeUint16(otherlen);
hmac->update(buffer.getData(), buffer.getLength());
if (otherlen > 0) {
hmac->update(otherdata, otherlen);
}
}
}
namespace {
// We exploit some minimum knowledge of DNS message format:
// the header section has a fixed length of 12 octets
// the offset in the header section to the ID field is 0 (and the field length
// is 2 octets)
// the offset in the header section to the ARCOUNT field is 10 (and the field
// length is 2 octets)
const size_t MESSAGE_HEADER_LEN = 12;
}
void
TSIGContext::TSIGContextImpl::digestDNSMessage(OutputBuffer& buffer,
HMACPtr hmac,
uint16_t qid, const void* data,
size_t data_len) const
{
buffer.clear();
const uint8_t* msgptr = static_cast<const uint8_t*>(data);
// Install the original ID
buffer.writeUint16(qid);
msgptr += sizeof(uint16_t);
// Copy the rest of the header except the ARCOUNT field.
buffer.writeData(msgptr, 8);
msgptr += 8;
// Install the adjusted ARCOUNT (we don't care even if the value is bogus
// and it underflows; it would simply result in verification failure)
InputBuffer b(msgptr, sizeof(uint16_t));
const uint16_t arcount = b.readUint16();
buffer.writeUint16(arcount - 1);
msgptr += 2;
// Digest the header and the rest of the DNS message
hmac->update(buffer.getData(), buffer.getLength());
hmac->update(msgptr, data_len - MESSAGE_HEADER_LEN);
}
TSIGContext::TSIGContext(const TSIGKey& key) : impl_(new TSIGContextImpl(key))
{
}
......@@ -78,7 +188,12 @@ TSIGContext::TSIGContext(const Name& key_name, const Name& algorithm_name,
const TSIGKeyRing::FindResult result(keyring.find(key_name,
algorithm_name));
if (result.code == TSIGKeyRing::NOTFOUND) {
impl_ = new TSIGContextImpl(getDummyTSIGKey());
// If not key is found, create a dummy key with the specified key
// parameters and empty secret. In the common scenario this will
// be used in subsequent response with a TSIG indicating a BADKEY
// error.
impl_ = new TSIGContextImpl(TSIGKey(key_name, algorithm_name,
NULL, 0));
impl_->error_ = TSIGError::BAD_KEY();
} else {
impl_ = new TSIGContextImpl(*result.key);
......@@ -103,21 +218,20 @@ ConstTSIGRecordPtr
TSIGContext::sign(const uint16_t qid, const void* const data,
const size_t data_len)
{
if (impl_->state_ == VERIFIED_RESPONSE) {
isc_throw(TSIGContextError,
"TSIG sign attempt after verifying a response");
}
if (data == NULL || data_len == 0) {
isc_throw(InvalidParameter, "TSIG sign error: empty data is given");
}
TSIGError error(TSIGError::NOERROR());
// TSIG uses 48-bit unsigned integer to represent time signed.
// Since gettimeofdayWrapper() returns a 64-bit *signed* integer, we
// make sure it's stored in an unsigned 64-bit integer variable and
// represents a value in the expected range. (In reality, however,
// gettimeofdayWrapper() will return a positive integer that will fit
// in 48 bits)
const uint64_t now = (detail::gettimeWrapper() & 0x0000ffffffffffffULL);
const uint64_t now = getTSIGTime();
// For responses adjust the error code.
if (impl_->state_ == CHECKED) {
if (impl_->state_ == RECEIVED_REQUEST) {
error = impl_->error_;
}
......@@ -130,7 +244,7 @@ TSIGContext::sign(const uint16_t qid, const void* const data,
now, DEFAULT_FUDGE, 0, NULL,
qid, error.getCode(), 0, NULL)));
impl_->previous_digest_.clear();
impl_->state_ = SIGNED;
impl_->state_ = SENT_RESPONSE;
return (tsig);
}
......@@ -144,53 +258,35 @@ TSIGContext::sign(const uint16_t qid, const void* const data,
// If the context has previous MAC (either the Request MAC or its own
// previous MAC), digest it.
if (impl_->state_ != INIT) {
const uint16_t previous_digest_len(impl_->previous_digest_.size());
variables.writeUint16(previous_digest_len);
if (previous_digest_len != 0) {
variables.writeData(&impl_->previous_digest_[0],
previous_digest_len);
}
hmac->update(variables.getData(), variables.getLength());
impl_->digestPreviousMAC(variables, hmac);
}
// Digest the message (without TSIG)
hmac->update(data, data_len);
//
// Digest TSIG variables. If state_ is SIGNED we skip digesting them
// except for time related variables (RFC2845 4.4).
//
variables.clear();
if (impl_->state_ != SIGNED) {
impl_->key_.getKeyName().toWire(variables);
TSIGRecord::getClass().toWire(variables);
variables.writeUint32(TSIGRecord::TSIG_TTL);
impl_->key_.getAlgorithmName().toWire(variables);
}
// Digest TSIG variables.
// First, prepare some non constant variables.
const uint64_t time_signed = (error == TSIGError::BAD_TIME()) ?
impl_->previous_timesigned_ : now;
variables.writeUint16(time_signed >> 32);
variables.writeUint32(time_signed & 0xffffffff);
variables.writeUint16(DEFAULT_FUDGE);
hmac->update(variables.getData(), variables.getLength());
variables.clear();
if (impl_->state_ != SIGNED) {
variables.writeUint16(error.getCode());
// For BADTIME error, digest 6 bytes of other data.
// (6 bytes = size of time signed value)
variables.writeUint16((error == TSIGError::BAD_TIME()) ? 6 : 0);
hmac->update(variables.getData(), variables.getLength());
variables.clear();
if (error == TSIGError::BAD_TIME()) {
variables.writeUint16(now >> 32);
variables.writeUint32(now & 0xffffffff);
hmac->update(variables.getData(), variables.getLength());
}
// For BADTIME error, we include 6 bytes of other data.
// (6 bytes = size of time signed value)
const uint16_t otherlen = (error == TSIGError::BAD_TIME()) ? 6 : 0;
OutputBuffer otherdatabuf(otherlen);
if (error == TSIGError::BAD_TIME()) {
otherdatabuf.writeUint16(now >> 32);
otherdatabuf.writeUint32(now & 0xffffffff);
}
const uint16_t otherlen = variables.getLength();
const void* const otherdata =
(otherlen == 0) ? NULL : otherdatabuf.getData();
// Then calculate the digest. If state_ is SENT_RESPONSE we are sending
// a continued message in the same TCP stream so skip digesting
// variables except for time related variables (RFC2845 4.4).
impl_->digestTSIGVariables(variables, hmac,
TSIGRecord::getClass().getCode(),
TSIGRecord::TSIG_TTL, time_signed,
DEFAULT_FUDGE, error.getCode(),
otherlen, otherdata,
impl_->state_ == SENT_RESPONSE);
// Get the final digest, update internal state, then finish.
vector<uint8_t> digest = hmac->sign();
......@@ -200,31 +296,131 @@ TSIGContext::sign(const uint16_t qid, const void* const data,
time_signed, DEFAULT_FUDGE,
digest.size(), &digest[0],
qid, error.getCode(), otherlen,
otherlen == 0 ?
NULL : variables.getData())));
otherdata)));
// Exception free from now on.
impl_->previous_digest_.swap(digest);
impl_->state_ = SIGNED;
impl_->state_ = (impl_->state_ == INIT) ? WAIT_RESPONSE : SENT_RESPONSE;
return (tsig);
}
void
TSIGContext::verifyTentative(ConstTSIGRecordPtr tsig, TSIGError error) {
const any::TSIG tsig_rdata = tsig->getRdata();
TSIGError
TSIGContext::verify(const TSIGRecord* const record, const void* const data,
const size_t data_len)
{
if (impl_->state_ == SENT_RESPONSE) {
isc_throw(TSIGContextError,
"TSIG verify attempt after sending a response");
}
impl_->error_ = error;
if (error == TSIGError::BAD_TIME()) {
impl_->previous_timesigned_ = tsig_rdata.getTimeSigned();
// This case happens when we sent a signed request and have received an
// unsigned response. According to RFC2845 Section 4.6 this case should be
// considered a "format error" (although the specific error code
// wouldn't matter much for the caller).
if (record == NULL) {
return (impl_->postVerifyUpdate(TSIGError::FORMERR(), NULL, 0));
}
// For simplicity we assume non empty digests.
assert(tsig_rdata.getMACSize() != 0);
impl_->previous_digest_.assign(
static_cast<const uint8_t*>(tsig_rdata.getMAC()),
static_cast<const uint8_t*>(tsig_rdata.getMAC()) +
tsig_rdata.getMACSize());
const any::TSIG& tsig_rdata = record->getRdata();
impl_->state_ = CHECKED;
// Reject some obviously invalid data
if (data_len < MESSAGE_HEADER_LEN + record->getLength()) {
isc_throw(InvalidParameter,
"TSIG verify: data length is invalid: " << data_len);
}
if (data == NULL) {
isc_throw(InvalidParameter, "TSIG verify: empty data is invalid");
}
// Check key: whether we first verify it with a known key or we verify
// it using the consistent key in the context. If the check fails we are
// done with BADKEY.
if (impl_->state_ == INIT && impl_->error_ == TSIGError::BAD_KEY()) {
return (impl_->postVerifyUpdate(TSIGError::BAD_KEY(), NULL, 0));
}
if (impl_->key_.getKeyName() != record->getName() ||
impl_->key_.getAlgorithmName() != tsig_rdata.getAlgorithm()) {
return (impl_->postVerifyUpdate(TSIGError::BAD_KEY(), NULL, 0));
}
// Check time: the current time must be in the range of
// [time signed - fudge, time signed + fudge]. Otherwise verification
// fails with BADTIME. (RFC2845 Section 4.6.2)
const uint64_t now = getTSIGTime();
if (tsig_rdata.getTimeSigned() + DEFAULT_FUDGE < now ||
tsig_rdata.getTimeSigned() - DEFAULT_FUDGE > now) {
const void* digest = NULL;
size_t digest_len = 0;
if (impl_->state_ == INIT) {
digest = tsig_rdata.getMAC();
digest_len = tsig_rdata.getMACSize();