RRSIG validation back to the root

This commit is contained in:
Benjamin Fry
2016-03-09 22:50:11 -08:00
parent 901701afc8
commit 357c555f20
8 changed files with 206 additions and 81 deletions

BIN
Kjqmt7v.csr Normal file

Binary file not shown.

View File

@@ -20,16 +20,28 @@ ground up.
# Status:
WARNING!!! Under active development!
The client now supports timeouts (thanks mio!). Currently hardcoded to 5 seconds,
I'll make this configurable if people ask for that, but this allows me to move on.
Using the client should be safe. The client is currently hardcoded to a 5 second,
timeout. I'll make this configurable if people ask for that, but this allows me
to move on. Please send feedback! It currently does not cache responses, if
this is a feature you'd like earlier rather than later, post a request. The
validation of DNSSec is complete, but negative responses are not yet.
The plan is to support NSEC3 primarily, but allow NSEC records to be validated.
The server code is complete, the daemon supports IPv4 and IPv6, UDP and TCP.
There currently is no way to limit TCP and AXFR operations, so it is still not
recommended to put into production as TCP can be used to DOS the service.
Master file parsing is complete and supported. There is currently no forking
option, and the server is not yet threaded.
option, and the server is not yet threaded. There is still a lot of work to do
before a server can be trusted with this externally. Running it behind a firewall
on a private network would be safe.
## DNSSec status
Currently the root key is hardcoded into the system. This gives validation of
DNSKEY and DS records back to the root. NSEC and NSEC3 are not yet implemented.
Because caching is not yet enabled, it has been noticed that some DNS servers
appear to rate limit the connections, validating RRSIG records back to the root
can require a significant number of additional queries for those records.
## RFC's implemented
@@ -42,6 +54,15 @@ option, and the server is not yet threaded.
### Update operations
- [RFC 2136](https://tools.ietf.org/html/rfc2136): Dynamic Update
### Secure DNS operations
- [RFC 3007](https://tools.ietf.org/html/rfc3007): Secure Dynamic Update
- [RFC 4034](https://tools.ietf.org/html/rfc4034): DNSSEC Resource Records
- [RFC 4035](https://tools.ietf.org/html/rfc4035): Protocol Modifications for DNSSEC
- [RFC 4509](https://tools.ietf.org/html/rfc4509): SHA-256 in DNSSEC Delegation Signer
- [RFC 5702](https://tools.ietf.org/html/rfc5702): SHA-2 Algorithms with RSA in DNSKEY and RRSIG for DNSSEC
- [RFC 6840](https://tools.ietf.org/html/rfc6840): Clarifications and Implementation Notes for DNSSEC
- [RFC 6944](https://tools.ietf.org/html/rfc6944): DNSKEY Algorithm Implementation Status
## RFC's in progress or not yet implemented
### Basic operations
@@ -55,14 +76,7 @@ option, and the server is not yet threaded.
- [Long-Lived Queries](http://tools.ietf.org/html/draft-sekar-dns-llq-01): Notify with bells
### Secure DNS operations
- [RFC 3007](https://tools.ietf.org/html/rfc3007): Secure Dynamic Update
- [RFC 4034](https://tools.ietf.org/html/rfc4034): DNSSEC Resource Records
- [RFC 4035](https://tools.ietf.org/html/rfc4035): Protocol Modifications for DNSSEC
- [RFC 4509](https://tools.ietf.org/html/rfc4509): SHA-256 in DNSSEC Delegation Signer
- [RFC 5155](https://tools.ietf.org/html/rfc5155): DNSSEC Hashed Authenticated Denial of Existence
- [RFC 5702](https://tools.ietf.org/html/rfc5702): SHA-2 Algorithms with RSA in DNSKEY and RRSIG for DNSSEC
- [RFC 6840](https://tools.ietf.org/html/rfc6840): Clarifications and Implementation Notes for DNSSEC
- [RFC 6944](https://tools.ietf.org/html/rfc6944): DNSKEY Algorithm Implementation Status
- [RFC 6975](https://tools.ietf.org/html/rfc6975): Signaling Cryptographic Algorithm Understanding
- [DNSCrypt](https://dnscrypt.org): Trusted DNS queries
- [S/MIME](https://tools.ietf.org/html/draft-ietf-dane-smime-09): Domain Names For S/MIME

3
root_key.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/sh
openssl req -text -noout -verify -in Kjqmt7v.csr -inform DER -pubkey -out src/rr/dnssec/Kjqmt7v.pem

54
src/rr/dnssec/Kjqmt7v.pem Normal file
View File

@@ -0,0 +1,54 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqAAgqVVmukLohruATNqE
5H71bb167GEmFVUs7JBtIRbQ7yBwKMUVVBRN/q/nx8uPAF3RgjQTOsBxCoEYLOH9
FK0ig7yDQ1+d8vYxMlGTGhdt8NpR5U9C5gSGDfs1lYAlD1WcxUPE/9Ucvj3oz9Bn
GSN/n8R+5ynaBoNfpFLoJemhjrwuy89WNHRlLDPPVqkDO8312XMSF5fsgIkEG24D
obctCnNbmE4DaHMJMyMk8nwtuoXp2xXoOgFDOC6XSwYhwY5iXs7JB1d9nnut6VJB
qB676KkB1NMnbkCxFMCi5vw40ZwuaqsCZEsoE/V1/CFgHg3uSc2e6WpDED5STWKH
PQIDAQAB
-----END PUBLIC KEY-----
Certificate Request:
Data:
Version: 0 (0x0)
Subject: O=ICANN, OU=IANA, CN=Root Zone KSK 2010-06-16T21:19:24+00:00/1.3.6.1.4.1.1000.53=. IN DS 19036 8 2 49AAC11D7B6F6446702E54A1607371607A1A41855200FD2CE1CDDE32F24E8FB5
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:a8:00:20:a9:55:66:ba:42:e8:86:bb:80:4c:da:
84:e4:7e:f5:6d:bd:7a:ec:61:26:15:55:2c:ec:90:
6d:21:16:d0:ef:20:70:28:c5:15:54:14:4d:fe:af:
e7:c7:cb:8f:00:5d:d1:82:34:13:3a:c0:71:0a:81:
18:2c:e1:fd:14:ad:22:83:bc:83:43:5f:9d:f2:f6:
31:32:51:93:1a:17:6d:f0:da:51:e5:4f:42:e6:04:
86:0d:fb:35:95:80:25:0f:55:9c:c5:43:c4:ff:d5:
1c:be:3d:e8:cf:d0:67:19:23:7f:9f:c4:7e:e7:29:
da:06:83:5f:a4:52:e8:25:e9:a1:8e:bc:2e:cb:cf:
56:34:74:65:2c:33:cf:56:a9:03:3b:cd:f5:d9:73:
12:17:97:ec:80:89:04:1b:6e:03:a1:b7:2d:0a:73:
5b:98:4e:03:68:73:09:33:23:24:f2:7c:2d:ba:85:
e9:db:15:e8:3a:01:43:38:2e:97:4b:06:21:c1:8e:
62:5e:ce:c9:07:57:7d:9e:7b:ad:e9:52:41:a8:1e:
bb:e8:a9:01:d4:d3:27:6e:40:b1:14:c0:a2:e6:fc:
38:d1:9c:2e:6a:ab:02:64:4b:28:13:f5:75:fc:21:
60:1e:0d:ee:49:cd:9e:e9:6a:43:10:3e:52:4d:62:
87:3d
Exponent: 65537 (0x10001)
Attributes:
a0:00
Signature Algorithm: sha256WithRSAEncryption
10:92:6a:79:8f:e9:e4:90:86:8c:d0:51:ac:2d:c9:9f:53:d3:
3a:79:e9:15:e8:ca:26:de:08:bd:d2:cd:83:da:ac:d3:e6:bc:
54:8b:f9:09:d9:27:b1:6a:ec:fa:b4:7a:4a:f6:97:80:a4:25:
80:b6:55:b2:1c:6a:a7:8f:5d:21:a8:57:ad:2e:bd:c9:37:50:
70:b7:8b:e0:1b:60:ed:a4:83:0d:d4:d6:13:0f:c9:cb:0a:8f:
76:80:eb:23:6f:01:0f:ba:09:20:2b:79:57:e1:18:8f:9a:dc:
72:18:6d:76:84:e7:71:72:f1:49:d7:ac:15:2a:51:27:e9:b4:
cd:47:ae:18:36:21:fc:88:d6:78:1e:3d:95:eb:81:d4:ad:24:
f7:cc:87:d0:73:91:41:f2:44:cd:a3:35:20:0a:07:a4:f0:de:
10:04:de:24:b7:9e:1d:1c:24:0a:ee:ca:71:da:ff:2c:f6:4f:
c3:f8:0f:57:6d:33:cb:8b:c1:45:73:8f:bb:f9:4c:71:0f:80:
c9:b8:15:51:51:17:03:fb:9b:b2:ea:ec:ae:31:a3:ce:0e:7b:
c9:d6:ac:65:b9:ec:8e:90:c8:94:e3:37:30:eb:fc:7f:cf:3a:
fd:59:7a:d7:7c:9e:0e:01:79:4e:52:0b:78:00:6f:fd:08:eb:
8c:56:1c:8f

View File

@@ -15,7 +15,6 @@
*/
use openssl::crypto::pkey::{PKey, Role};
use openssl::crypto::rsa::RSA;
use openssl::crypto::hash;
use openssl::bn::BigNum;
use ::rr::dnssec::DigestType;

View File

@@ -18,9 +18,11 @@ mod digest_type;
mod nsec3;
mod signer;
mod supported_algorithm;
mod trust_anchor;
pub use self::algorithm::Algorithm;
pub use self::digest_type::DigestType;
pub use self::nsec3::Nsec3HashAlgorithm;
pub use self::signer::Signer;
pub use self::supported_algorithm::SupportedAlgorithms;
pub use self::trust_anchor::TrustAnchor;

View File

@@ -0,0 +1,40 @@
/*
* Copyright (C) 2015 Benjamin Fry <benjaminfry@me.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
use std::io::Cursor;
use openssl::crypto::pkey::{PKey, Role};
use ::rr::dnssec::Algorithm;
const ROOT_ANCHOR: &'static str = include_str!("Kjqmt7v.pem");
pub struct TrustAnchor {
pkey: Vec<u8>
}
impl TrustAnchor {
pub fn new() -> TrustAnchor {
let mut cursor = Cursor::new(ROOT_ANCHOR);
let pkey = PKey::public_key_from_pem(&mut cursor).expect("Error parsing Kjqmt7v.pem");
let alg = Algorithm::RSASHA256;
TrustAnchor{ pkey: alg.public_key_to_vec(&pkey) }
}
pub fn contains(&self, other_key: &[u8]) -> bool {
self.pkey == other_key
}
}

View File

@@ -26,7 +26,7 @@ use openssl::crypto::pkey::Role;
use ::error::*;
use ::rr::{DNSClass, RecordType, Record, RData};
use ::rr::domain;
use ::rr::dnssec::Signer;
use ::rr::dnssec::{Signer, TrustAnchor};
use ::op::{ Message, MessageType, OpCode, Query, Edns, ResponseCode };
use ::serialize::binary::*;
@@ -72,41 +72,44 @@ impl<A: ToSocketAddrs + Copy> Client<A> {
// the RRSIG is signed by the DNSKEY, the DNSKEY is signed by the DS record in the Parent
// zone. The key_tag is the DS record is assigned to the DNSKEY.
let record_response = try!(self.inner_query(name, query_class, query_type, true));
let rrsigs: Vec<&Record> = record_response.get_answers().iter().filter(|rr| rr.get_rr_type() == RecordType::RRSIG).collect();
{
let rrsigs: Vec<&Record> = record_response.get_answers().iter().filter(|rr| rr.get_rr_type() == RecordType::RRSIG).collect();
if rrsigs.is_empty() {
return Err(ClientError::NoRRSIG);
if rrsigs.is_empty() {
return Err(ClientError::NoRRSIG);
}
// group the record sets by name and type
let mut rrset_types: HashSet<(domain::Name, RecordType)> = HashSet::new();
for rrset in record_response.get_answers().iter()
.filter(|rr| rr.get_rr_type() != RecordType::RRSIG)
.map(|rr| (rr.get_name().clone(), rr.get_rr_type())) {
rrset_types.insert(rrset);
}
// verify all returned rrsets
for (name, rrset_type) in rrset_types {
let rrset: Vec<&Record> = record_response.get_answers().iter().filter(|rr| rr.get_rr_type() == rrset_type && rr.get_name() == &name).collect();
// '. DNSKEY' -> 'com. DS' -> 'com. DNSKEY' -> 'examle.com. DS' -> 'example.com. DNSKEY'
// 'com. DS' is signed by '. DNSKEY' which produces 'com. RRSIG', all are in the same zone, '.'
// the '.' DNSKEY is signed by the well known root certificate.
// TODO fix rrsigs clone()
let proof = try!(self.recursive_query_verify(&name, rrset, rrsigs.clone(), query_type, query_class));
// TODO return this, also make a prettier print
debug!("proved existance through: {:?}", proof);
}
}
// group the record sets by name and type
let mut rrset_types: HashSet<(domain::Name, RecordType)> = HashSet::new();
for rrset in record_response.get_answers().iter()
.filter(|rr| rr.get_rr_type() != RecordType::RRSIG)
.map(|rr| (rr.get_name().clone(), rr.get_rr_type())) {
rrset_types.insert(rrset);
}
// verify all returned rrsets
for (name, rrset_type) in rrset_types {
let rrset: Vec<&Record> = record_response.get_answers().iter().filter(|rr| rr.get_rr_type() == rrset_type && rr.get_name() == &name).collect();
// '. DNSKEY' -> 'com. DS' -> 'com. DNSKEY' -> 'examle.com. DS' -> 'example.com. DNSKEY'
// 'com. DS' is signed by '. DNSKEY' which produces 'com. RRSIG', all are in the same zone, '.'
// the '.' DNSKEY is signed by the well known root certificate.
// TODO fix rrsigs clone()
let proof = try!(self.recursive_query_verify(&name, rrset, rrsigs.clone(), query_type, query_class));
debug!("proved existance through: {:?}", proof);
}
// verify each RRSIG...
unimplemented!()
// getting here means that we looped through all records with validation
Ok(record_response)
}
/// Verifies a record set against the supplied signatures, looking up the DNSKey chain.
/// returns the chain of proof or an error if there is none.
fn recursive_query_verify(&self, name: &domain::Name, rrset: Vec<&Record>, rrsigs: Vec<&Record>,
query_type: RecordType, query_class: DNSClass) -> ClientResult<Vec<Record>> {
debug!("verifying: {} type: {:?}", name, query_type);
// TODO: this is ugly, what reference do I want?
let rrset: Vec<Record> = rrset.iter().map(|rr|rr.clone()).cloned().collect();
@@ -119,6 +122,7 @@ impl<A: ToSocketAddrs + Copy> Client<A> {
// but how do you know which needs to be validated with the DS in the parent zone?
if zone_key && secure_entry_point {
let mut proof = try!(self.verify_dnskey(record));
// TODO: this is verified, it can be cached
proof.push(record.clone());
return Ok(proof);
}
@@ -138,26 +142,26 @@ impl<A: ToSocketAddrs + Copy> Client<A> {
for dnskey in key_rrset.iter() {
if let &RData::DNSKEY{zone_key, algorithm, revoke, ref public_key, ..} = dnskey.get_rdata() {
if revoke { debug!("revoked"); continue } // TODO: does this need to be validated? RFC 5011
if !zone_key { debug!("not a zone_key, can't use"); continue }
if algorithm != sig_alg { debug!("algorithms don't match"); continue }
if revoke { debug!("revoked: {}", dnskey.get_name()); continue } // TODO: does this need to be validated? RFC 5011
if !zone_key { continue }
if algorithm != sig_alg { continue }
let pkey = try!(algorithm.public_key_from_vec(public_key));
if !pkey.can(Role::Verify) { debug!("pkey can't verify"); continue }
if !pkey.can(Role::Verify) { debug!("pkey can't verify, {:?}", dnskey.get_name()); continue }
let signer: Signer = Signer::new(algorithm, pkey, signer_name.clone());
let rrset_hash: Vec<u8> = signer.hash_rrset(rrsig, &rrset);
if signer.verify(&rrset_hash, sig) {
debug!("verified: {} with: {}", name, dnskey.get_name());
if signer_name == name && query_type == RecordType::DNSKEY {
// this is self signed... let's skip to DS validation
let mut proof: Vec<Record> = try!(self.verify_dnskey(dnskey));
// TODO: this is verified, cache it
proof.push((*dnskey).clone());
return Ok(proof);
} else {
let mut proof = try!(self.recursive_query_verify(&signer_name, key_rrset.clone(), key_rrsigs, RecordType::DNSKEY, query_class));
// TODO: this is verified, cache it
proof.push((*dnskey).clone());
return Ok(proof);
}
@@ -179,9 +183,16 @@ impl<A: ToSocketAddrs + Copy> Client<A> {
/// attempts to verify the DNSKey against the DS of the parent.
/// returns the chain of proof or an error if there is none.
fn verify_dnskey(&self, dnskey: &Record) -> ClientResult<Vec<Record>> {
debug!("verifying dnskey");
let name: &domain::Name = dnskey.get_name();
if dnskey.get_name().is_root() {
if let &RData::DNSKEY{ ref public_key, .. } = dnskey.get_rdata() {
if TrustAnchor::new().contains(public_key) {
return Ok(vec![dnskey.clone()])
}
}
}
let ds_response = try!(self.inner_query(&name, dnskey.get_dns_class(), RecordType::DS, true));
let ds_rrset: Vec<&Record> = ds_response.get_answers().iter().filter(|rr| rr.get_rr_type() == RecordType::DS).collect();
let ds_rrsigs: Vec<&Record> = ds_response.get_answers().iter().filter(|rr| rr.get_rr_type() == RecordType::RRSIG).collect();
@@ -217,7 +228,9 @@ impl<A: ToSocketAddrs + Copy> Client<A> {
let hash: Vec<u8> = digest_type.hash(&buf);
if &hash == digest {
// continue to verify the chain...
return self.recursive_query_verify(&name, ds_rrset.clone(), ds_rrsigs, RecordType::DNSKEY, dnskey.get_dns_class());
let mut proof: Vec<Record> = try!(self.recursive_query_verify(&name, ds_rrset.clone(), ds_rrsigs, RecordType::DNSKEY, dnskey.get_dns_class()));
proof.push(dnskey.clone());
return Ok(proof)
}
} else {
panic!("expected DS: {:?}", ds.get_rr_type());
@@ -488,36 +501,36 @@ fn test_secure_query_example() {
}
}
// TODO: use this site for verifying nsec3
#[test]
#[cfg(feature = "ftest")]
fn test_secure_query_sdsmt() {
use std::net::*;
use ::rr::dns_class::DNSClass;
use ::rr::record_type::RecordType;
use ::rr::domain;
use ::rr::record_data::RData;
use ::udp::Client;
let name = domain::Name::with_labels(vec!["www".to_string(), "sdsmt".to_string(), "edu".to_string()]);
let client = Client::new(("8.8.8.8").parse().unwrap()).unwrap();
let response = client.secure_query(&name, DNSClass::IN, RecordType::A);
assert!(response.is_ok(), "query failed: {}", response.unwrap_err());
let response = response.unwrap();
println!("response records: {:?}", response);
let record = &response.get_answers()[0];
assert_eq!(record.get_name(), &name);
assert_eq!(record.get_rr_type(), RecordType::A);
assert_eq!(record.get_dns_class(), DNSClass::IN);
if let &RData::A{ ref address } = record.get_rdata() {
assert_eq!(address, &Ipv4Addr::new(93,184,216,34))
} else {
assert!(false);
}
}
// // TODO: use this site for verifying nsec3
// #[test]
// #[cfg(feature = "ftest")]
// fn test_secure_query_sdsmt() {
// use std::net::*;
//
// use ::rr::dns_class::DNSClass;
// use ::rr::record_type::RecordType;
// use ::rr::domain;
// use ::rr::record_data::RData;
// use ::udp::Client;
//
// let name = domain::Name::with_labels(vec!["www".to_string(), "sdsmt".to_string(), "edu".to_string()]);
// let client = Client::new(("8.8.8.8").parse().unwrap()).unwrap();
//
// let response = client.secure_query(&name, DNSClass::IN, RecordType::A);
// assert!(response.is_ok(), "query failed: {}", response.unwrap_err());
//
// let response = response.unwrap();
//
// println!("response records: {:?}", response);
//
// let record = &response.get_answers()[0];
// assert_eq!(record.get_name(), &name);
// assert_eq!(record.get_rr_type(), RecordType::A);
// assert_eq!(record.get_dns_class(), DNSClass::IN);
//
// if let &RData::A{ ref address } = record.get_rdata() {
// assert_eq!(address, &Ipv4Addr::new(93,184,216,34))
// } else {
// assert!(false);
// }
// }