update TLS docs and others
This commit is contained in:
parent
1e38d34bad
commit
bb75b3bf3a
@ -2,6 +2,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## 0.10.1 (unreleased)
|
||||
### Changed
|
||||
- Fixed TLS documentation, and add more elsewhere; fixes #102
|
||||
|
||||
## 0.10.0
|
||||
### Changed
|
||||
- *Important* Possible breaking API change, the original Client has been renamed
|
||||
|
@ -26,6 +26,7 @@ readme = "../README.md"
|
||||
# This is a small list of keywords used to categorize and search for this
|
||||
# package.
|
||||
keywords = ["DNS", "BIND", "dig", "named", "dnssec"]
|
||||
categories = ["network-programming"]
|
||||
|
||||
# This is a string description of the license for this package. Currently
|
||||
# crates.io will validate the license provided against a whitelist of known
|
||||
@ -36,6 +37,9 @@ license = "MIT/Apache-2.0"
|
||||
# custom build steps
|
||||
build = "build.rs"
|
||||
|
||||
[badges]
|
||||
travis-ci = { repository = "bluejekyll/trust-dns" }
|
||||
|
||||
[features]
|
||||
default = ["openssl", "tls"]
|
||||
tls = ["native-tls", "tokio-tls", "security-framework"]
|
||||
|
@ -19,7 +19,7 @@ use futures::Stream;
|
||||
use tokio_core::reactor::Core;
|
||||
|
||||
use client::{ClientHandle, BasicClientHandle, ClientConnection, ClientFuture, SecureClientHandle};
|
||||
use ::error::*;
|
||||
use error::*;
|
||||
use rr::{domain, DNSClass, IntoRecordSet, RecordType, Record};
|
||||
use rr::dnssec::Signer;
|
||||
#[cfg(any(feature = "openssl", feature = "ring"))]
|
||||
@ -28,7 +28,7 @@ use op::Message;
|
||||
|
||||
/// Client trait which implements basic DNS Client operations.
|
||||
///
|
||||
/// As of 0.9.4, the Client is now a wrapper around the `ClientFuture`, which is a futures-rs
|
||||
/// As of 0.10.0, the Client is now a wrapper around the `ClientFuture`, which is a futures-rs
|
||||
/// and tokio-rs based implementation. This trait implements syncronous functions for ease of use.
|
||||
///
|
||||
/// There was a strong attempt to make it backwards compatible, but making it a drop in replacement
|
||||
@ -58,8 +58,9 @@ pub trait Client<C: ClientHandle> {
|
||||
query_class: DNSClass,
|
||||
query_type: RecordType)
|
||||
-> ClientResult<Message> {
|
||||
self.get_io_loop()
|
||||
.run(self.get_client_handle().query(name.clone(), query_class, query_type))
|
||||
self.get_io_loop().run(self.get_client_handle().query(name.clone(),
|
||||
query_class,
|
||||
query_type))
|
||||
}
|
||||
|
||||
/// Sends a NOTIFY message to the remote system
|
||||
@ -78,8 +79,10 @@ pub trait Client<C: ClientHandle> {
|
||||
-> ClientResult<Message>
|
||||
where R: IntoRecordSet
|
||||
{
|
||||
self.get_io_loop()
|
||||
.run(self.get_client_handle().notify(name, query_class, query_type, rrset))
|
||||
self.get_io_loop().run(self.get_client_handle().notify(name,
|
||||
query_class,
|
||||
query_type,
|
||||
rrset))
|
||||
}
|
||||
|
||||
/// Sends a record to create on the server, this will fail if the record exists (atomicity
|
||||
@ -326,8 +329,9 @@ pub trait Client<C: ClientHandle> {
|
||||
zone_origin: domain::Name,
|
||||
dns_class: DNSClass)
|
||||
-> ClientResult<Message> {
|
||||
self.get_io_loop()
|
||||
.run(self.get_client_handle().delete_all(name_of_records, zone_origin, dns_class))
|
||||
self.get_io_loop().run(self.get_client_handle().delete_all(name_of_records,
|
||||
zone_origin,
|
||||
dns_class))
|
||||
}
|
||||
}
|
||||
|
||||
@ -347,8 +351,8 @@ impl SyncClient {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `client_connection` - the client_connection to use for all communication
|
||||
pub fn new<CC: ClientConnection>(client_connection: CC) -> SyncClient
|
||||
where <CC as ClientConnection>::MessageStream: Stream<Item=Vec<u8>, Error=io::Error> + 'static {
|
||||
pub fn new<CC: ClientConnection>(client_connection: CC) -> SyncClient
|
||||
where <CC as ClientConnection>::MessageStream: Stream<Item=Vec<u8>, Error=io::Error> + 'static{
|
||||
let (io_loop, stream, stream_handle) = client_connection.unwrap();
|
||||
|
||||
let client = ClientFuture::new(stream, stream_handle, io_loop.handle(), None);
|
||||
@ -368,7 +372,7 @@ impl SyncClient {
|
||||
/// * `client_connection` - the client_connection to use for all communication
|
||||
/// * `signer` - signer to use, this needs an associated private key
|
||||
pub fn with_signer<CC: ClientConnection>(client_connection: CC, signer: Signer) -> SyncClient
|
||||
where <CC as ClientConnection>::MessageStream: Stream<Item=Vec<u8>, Error=io::Error> + 'static {
|
||||
where <CC as ClientConnection>::MessageStream: Stream<Item=Vec<u8>, Error=io::Error> + 'static{
|
||||
let (io_loop, stream, stream_handle) = client_connection.unwrap();
|
||||
|
||||
let client = ClientFuture::new(stream, stream_handle, io_loop.handle(), Some(signer));
|
||||
@ -390,6 +394,7 @@ impl Client<BasicClientHandle> for SyncClient {
|
||||
}
|
||||
}
|
||||
|
||||
/// A DNS client which will validate DNSSec records upon receipt
|
||||
#[cfg(any(feature = "openssl", feature = "ring"))]
|
||||
pub struct SecureSyncClient {
|
||||
client_handle: RefCell<SecureClientHandle<BasicClientHandle>>,
|
||||
@ -405,7 +410,7 @@ impl SecureSyncClient {
|
||||
/// * `client_connection` - the client_connection to use for all communication
|
||||
pub fn new<CC>(client_connection: CC) -> SecureSyncClientBuilder<CC>
|
||||
where CC: ClientConnection,
|
||||
<CC as ClientConnection>::MessageStream: Stream<Item=Vec<u8>, Error=io::Error> + 'static {
|
||||
<CC as ClientConnection>::MessageStream: Stream<Item=Vec<u8>, Error=io::Error> + 'static{
|
||||
SecureSyncClientBuilder {
|
||||
client_connection: client_connection,
|
||||
trust_anchor: None,
|
||||
@ -439,8 +444,9 @@ impl SecureSyncClient {
|
||||
query_class: DNSClass,
|
||||
query_type: RecordType)
|
||||
-> ClientResult<Message> {
|
||||
self.get_io_loop()
|
||||
.run(self.get_client_handle().query(query_name.clone(), query_class, query_type))
|
||||
self.get_io_loop().run(self.get_client_handle().query(query_name.clone(),
|
||||
query_class,
|
||||
query_type))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,7 @@ use rand::Rng;
|
||||
use rand;
|
||||
use tokio_core::reactor::{Handle, Timeout};
|
||||
|
||||
use ::error::*;
|
||||
use error::*;
|
||||
use op::{Message, MessageType, OpCode, Query, UpdateMessage};
|
||||
use rr::{domain, DNSClass, IntoRecordSet, RData, Record, RecordType};
|
||||
use rr::dnssec::Signer;
|
||||
@ -31,14 +31,17 @@ const QOS_MAX_RECEIVE_MSGS: usize = 100; // max number of messages to receive fr
|
||||
/// A reference to a Sender of bytes returned from the creation of a UdpClientStream or TcpClientStream
|
||||
pub type StreamHandle = UnboundedSender<Vec<u8>>;
|
||||
|
||||
/// Implementations of Sinks for sending DNS messages
|
||||
pub trait ClientStreamHandle {
|
||||
fn send(&mut self, buffer: Vec<u8>) -> io::Result<()>;
|
||||
}
|
||||
|
||||
impl ClientStreamHandle for StreamHandle {
|
||||
fn send(&mut self, buffer: Vec<u8>) -> io::Result<()> {
|
||||
UnboundedSender::send(self, buffer)
|
||||
.map_err(|_| io::Error::new(io::ErrorKind::Other, "unknown"))
|
||||
UnboundedSender::send(self, buffer).map_err(|_| {
|
||||
io::Error::new(io::ErrorKind::Other,
|
||||
"unknown")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -115,10 +118,10 @@ impl<S: Stream<Item = Vec<u8>, Error = io::Error> + 'static> ClientFuture<S> {
|
||||
signer: signer,
|
||||
}
|
||||
})
|
||||
.flatten()
|
||||
.map_err(|e| {
|
||||
error!("error in Client: {}", e);
|
||||
}));
|
||||
.flatten()
|
||||
.map_err(|e| {
|
||||
error!("error in Client: {}", e);
|
||||
}));
|
||||
|
||||
BasicClientHandle { message_sender: sender }
|
||||
}
|
||||
@ -338,8 +341,8 @@ impl ClientHandle for BasicClientHandle {
|
||||
|
||||
// conver the oneshot into a Box of a Future message and error.
|
||||
Box::new(receiver.map_err(|c| ClientError::from(c))
|
||||
.map(|result| result.into_future())
|
||||
.flatten())
|
||||
.map(|result| result.into_future())
|
||||
.flatten())
|
||||
}
|
||||
}
|
||||
|
||||
@ -544,7 +547,9 @@ pub trait ClientHandle: Clone {
|
||||
|
||||
// for updates, the query section is used for the zone
|
||||
let mut zone: Query = Query::new();
|
||||
zone.set_name(zone_origin).set_query_class(rrset.dns_class()).set_query_type(RecordType::SOA);
|
||||
zone.set_name(zone_origin)
|
||||
.set_query_class(rrset.dns_class())
|
||||
.set_query_type(RecordType::SOA);
|
||||
|
||||
// build the message
|
||||
let mut message: Message = Message::new();
|
||||
@ -615,7 +620,9 @@ pub trait ClientHandle: Clone {
|
||||
|
||||
// for updates, the query section is used for the zone
|
||||
let mut zone: Query = Query::new();
|
||||
zone.set_name(zone_origin).set_query_class(rrset.dns_class()).set_query_type(RecordType::SOA);
|
||||
zone.set_name(zone_origin)
|
||||
.set_query_class(rrset.dns_class())
|
||||
.set_query_type(RecordType::SOA);
|
||||
|
||||
// build the message
|
||||
let mut message: Message = Message::new();
|
||||
@ -626,8 +633,7 @@ pub trait ClientHandle: Clone {
|
||||
message.add_zone(zone);
|
||||
|
||||
if must_exist {
|
||||
let mut prerequisite =
|
||||
Record::with(rrset.name().clone(), rrset.record_type(), 0);
|
||||
let mut prerequisite = Record::with(rrset.name().clone(), rrset.record_type(), 0);
|
||||
prerequisite.set_dns_class(DNSClass::ANY);
|
||||
message.add_pre_requisite(prerequisite);
|
||||
}
|
||||
@ -784,7 +790,9 @@ pub trait ClientHandle: Clone {
|
||||
|
||||
// for updates, the query section is used for the zone
|
||||
let mut zone: Query = Query::new();
|
||||
zone.set_name(zone_origin).set_query_class(rrset.dns_class()).set_query_type(RecordType::SOA);
|
||||
zone.set_name(zone_origin)
|
||||
.set_query_class(rrset.dns_class())
|
||||
.set_query_type(RecordType::SOA);
|
||||
|
||||
// build the message
|
||||
let mut message: Message = Message::new();
|
||||
@ -852,7 +860,9 @@ pub trait ClientHandle: Clone {
|
||||
|
||||
// for updates, the query section is used for the zone
|
||||
let mut zone: Query = Query::new();
|
||||
zone.set_name(zone_origin).set_query_class(record.dns_class()).set_query_type(RecordType::SOA);
|
||||
zone.set_name(zone_origin)
|
||||
.set_query_class(record.dns_class())
|
||||
.set_query_type(RecordType::SOA);
|
||||
|
||||
// build the message
|
||||
let mut message: Message = Message::new();
|
||||
|
@ -12,10 +12,10 @@ use futures::Future;
|
||||
|
||||
use client::ClientHandle;
|
||||
use client::rc_future::{rc_future, RcFuture};
|
||||
use ::error::*;
|
||||
use error::*;
|
||||
use op::{Message, Query};
|
||||
|
||||
/// Will return memoized (cached) responses to queries
|
||||
/// A ClienHandle for memoized (cached) responses to queries.
|
||||
///
|
||||
/// This wraps a ClientHandle, changing the implementation `send()` to store the response against
|
||||
/// the Message.Query that was sent. This should reduce network traffic especially during things
|
||||
@ -45,7 +45,10 @@ impl<H> ClientHandle for MemoizeClientHandle<H>
|
||||
where H: ClientHandle
|
||||
{
|
||||
fn send(&mut self, message: Message) -> Box<Future<Item = Message, Error = ClientError>> {
|
||||
let query = message.queries().first().expect("no query!").clone();
|
||||
let query = message.queries()
|
||||
.first()
|
||||
.expect("no query!")
|
||||
.clone();
|
||||
|
||||
if let Some(rc_future) = self.active_queries.borrow().get(&query) {
|
||||
// FIXME check TTLs?
|
||||
@ -72,10 +75,10 @@ impl<H> ClientHandle for MemoizeClientHandle<H>
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::cell::Cell;
|
||||
use ::client::*;
|
||||
use ::error::*;
|
||||
use ::op::*;
|
||||
use ::rr::*;
|
||||
use client::*;
|
||||
use error::*;
|
||||
use op::*;
|
||||
use rr::*;
|
||||
use futures::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
@ -105,17 +108,29 @@ mod test {
|
||||
let mut test2 = Message::new();
|
||||
test2.add_query(Query::new().set_query_type(RecordType::AAAA).clone());
|
||||
|
||||
let result = client.send(test1.clone()).wait().ok().unwrap();
|
||||
let result = client.send(test1.clone())
|
||||
.wait()
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(result.id(), 0);
|
||||
|
||||
let result = client.send(test2.clone()).wait().ok().unwrap();
|
||||
let result = client.send(test2.clone())
|
||||
.wait()
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(result.id(), 1);
|
||||
|
||||
// should get the same result for each...
|
||||
let result = client.send(test1).wait().ok().unwrap();
|
||||
let result = client.send(test1)
|
||||
.wait()
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(result.id(), 0);
|
||||
|
||||
let result = client.send(test2).wait().ok().unwrap();
|
||||
let result = client.send(test2)
|
||||
.wait()
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(result.id(), 1);
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,7 @@ use std::rc::Rc;
|
||||
use futures::*;
|
||||
|
||||
use client::ClientHandle;
|
||||
use ::error::*;
|
||||
use error::*;
|
||||
use op::{Message, OpCode, Query};
|
||||
use rr::{domain, DNSClass, RData, Record, RecordType};
|
||||
use rr::dnssec::{Algorithm, KeyPair, SupportedAlgorithms, TrustAnchor};
|
||||
@ -96,14 +96,17 @@ impl<H> ClientHandle for SecureClientHandle<H>
|
||||
// backstop, this might need to be configurable at some point
|
||||
if self.request_depth > 20 {
|
||||
return Box::new(failed(ClientErrorKind::Message("exceeded max validation depth")
|
||||
.into()));
|
||||
.into()));
|
||||
}
|
||||
|
||||
// dnssec only matters on queries.
|
||||
if let OpCode::Query = message.op_code() {
|
||||
// This will panic on no queries, that is a very odd type of request, isn't it?
|
||||
// TODO: there should only be one
|
||||
let query = message.queries().first().cloned().unwrap();
|
||||
let query = message.queries()
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap();
|
||||
let client: SecureClientHandle<H> = self.clone_with_context();
|
||||
|
||||
// TODO: cache response of the server about understood algorithms
|
||||
@ -115,7 +118,8 @@ impl<H> ClientHandle for SecureClientHandle<H>
|
||||
|
||||
// send along the algorithms which are supported by this client
|
||||
let mut algorithms = SupportedAlgorithms::new();
|
||||
#[cfg(feature = "openssl")] {
|
||||
#[cfg(feature = "openssl")]
|
||||
{
|
||||
algorithms.set(Algorithm::RSASHA256);
|
||||
algorithms.set(Algorithm::ECDSAP256SHA256);
|
||||
algorithms.set(Algorithm::ECDSAP384SHA384);
|
||||
@ -135,33 +139,33 @@ impl<H> ClientHandle for SecureClientHandle<H>
|
||||
let dns_class = message.queries().first().map_or(DNSClass::IN, |q| q.query_class());
|
||||
|
||||
return Box::new(self.client
|
||||
.send(message)
|
||||
.and_then(move |message_response| {
|
||||
// group the record sets by name and type
|
||||
// each rrset type needs to validated independently
|
||||
debug!("validating message_response: {}", message_response.id());
|
||||
verify_rrsets(client, message_response, dns_class)
|
||||
})
|
||||
.and_then(move |verified_message| {
|
||||
// at this point all of the message is verified.
|
||||
// This is where NSEC (and possibly NSEC3) validation occurs
|
||||
// As of now, only NSEC is supported.
|
||||
if verified_message.answers().is_empty() {
|
||||
let nsecs = verified_message.name_servers()
|
||||
.iter()
|
||||
.filter(|rr| rr.rr_type() == RecordType::NSEC)
|
||||
.collect::<Vec<_>>();
|
||||
.send(message)
|
||||
.and_then(move |message_response| {
|
||||
// group the record sets by name and type
|
||||
// each rrset type needs to validated independently
|
||||
debug!("validating message_response: {}", message_response.id());
|
||||
verify_rrsets(client, message_response, dns_class)
|
||||
})
|
||||
.and_then(move |verified_message| {
|
||||
// at this point all of the message is verified.
|
||||
// This is where NSEC (and possibly NSEC3) validation occurs
|
||||
// As of now, only NSEC is supported.
|
||||
if verified_message.answers().is_empty() {
|
||||
let nsecs = verified_message.name_servers()
|
||||
.iter()
|
||||
.filter(|rr| rr.rr_type() == RecordType::NSEC)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if !verify_nsec(&query, nsecs) {
|
||||
// TODO change this to remove the NSECs, like we do for the others?
|
||||
return Err(ClientErrorKind::Message("could not validate nxdomain \
|
||||
if !verify_nsec(&query, nsecs) {
|
||||
// TODO change this to remove the NSECs, like we do for the others?
|
||||
return Err(ClientErrorKind::Message("could not validate nxdomain \
|
||||
with NSEC")
|
||||
.into());
|
||||
}
|
||||
.into());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(verified_message)
|
||||
}));
|
||||
Ok(verified_message)
|
||||
}));
|
||||
}
|
||||
|
||||
self.client.send(message)
|
||||
@ -233,10 +237,10 @@ fn verify_rrsets<H>(client: SecureClientHandle<H>,
|
||||
.chain(message_result.additionals())
|
||||
.filter(|rr| rr.rr_type() == RecordType::RRSIG)
|
||||
.filter(|rr| if let &RData::SIG(ref rrsig) = rr.rdata() {
|
||||
rrsig.type_covered() == record_type
|
||||
} else {
|
||||
false
|
||||
})
|
||||
rrsig.type_covered() == record_type
|
||||
} else {
|
||||
false
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
@ -262,10 +266,10 @@ fn verify_rrsets<H>(client: SecureClientHandle<H>,
|
||||
|
||||
// return the full Message validator
|
||||
Box::new(VerifyRrsetsFuture {
|
||||
message_result: Some(message_result),
|
||||
rrsets: rrsets_to_verify,
|
||||
verified_rrsets: HashSet::new(),
|
||||
})
|
||||
message_result: Some(message_result),
|
||||
rrsets: rrsets_to_verify,
|
||||
verified_rrsets: HashSet::new(),
|
||||
})
|
||||
}
|
||||
|
||||
impl Future for VerifyRrsetsFuture {
|
||||
@ -317,25 +321,25 @@ impl Future for VerifyRrsetsFuture {
|
||||
let answers = message_result.take_answers()
|
||||
.into_iter()
|
||||
.filter(|record| {
|
||||
self.verified_rrsets
|
||||
.contains(&(record.name().clone(), record.rr_type()))
|
||||
})
|
||||
self.verified_rrsets.contains(&(record.name().clone(),
|
||||
record.rr_type()))
|
||||
})
|
||||
.collect::<Vec<Record>>();
|
||||
|
||||
let name_servers = message_result.take_name_servers()
|
||||
.into_iter()
|
||||
.filter(|record| {
|
||||
self.verified_rrsets
|
||||
.contains(&(record.name().clone(), record.rr_type()))
|
||||
})
|
||||
self.verified_rrsets.contains(&(record.name().clone(),
|
||||
record.rr_type()))
|
||||
})
|
||||
.collect::<Vec<Record>>();
|
||||
|
||||
let additionals = message_result.take_additionals()
|
||||
.into_iter()
|
||||
.filter(|record| {
|
||||
self.verified_rrsets
|
||||
.contains(&(record.name().clone(), record.rr_type()))
|
||||
})
|
||||
self.verified_rrsets.contains(&(record.name().clone(),
|
||||
record.rr_type()))
|
||||
})
|
||||
.collect::<Vec<Record>>();
|
||||
|
||||
// add the filtered records back to the message
|
||||
@ -414,16 +418,16 @@ fn verify_dnskey_rrset<H>(mut client: SecureClientHandle<H>,
|
||||
.enumerate()
|
||||
.filter(|&(_, rr)| rr.rr_type() == RecordType::DNSKEY)
|
||||
.filter_map(|(i, rr)| if let &RData::DNSKEY(ref rdata) = rr.rdata() {
|
||||
Some((i, rdata))
|
||||
} else {
|
||||
None
|
||||
})
|
||||
Some((i, rdata))
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.filter_map(|(i, rdata)| if client.trust_anchor.contains(rdata.public_key()) {
|
||||
debug!("in trust_anchor");
|
||||
Some(i)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
debug!("in trust_anchor");
|
||||
Some(i)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.collect::<Vec<usize>>();
|
||||
|
||||
if !anchored_keys.is_empty() {
|
||||
@ -445,10 +449,10 @@ fn verify_dnskey_rrset<H>(mut client: SecureClientHandle<H>,
|
||||
.enumerate()
|
||||
.filter(|&(_, rr)| rr.rr_type() == RecordType::DNSKEY)
|
||||
.filter_map(|(i, rr)| if let &RData::DNSKEY(ref rdata) = rr.rdata() {
|
||||
Some((i, rdata))
|
||||
} else {
|
||||
None
|
||||
})
|
||||
Some((i, rdata))
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.filter(|&(_, key_rdata)| {
|
||||
ds_message.answers()
|
||||
.iter()
|
||||
@ -555,13 +559,13 @@ fn verify_default_rrset<H>(client: SecureClientHandle<H>,
|
||||
rrset.record_type);
|
||||
|
||||
// Special case for self-signed DNSKEYS, validate with itself...
|
||||
if rrsigs.iter()
|
||||
.filter(|rrsig| rrsig.rr_type() == RecordType::RRSIG)
|
||||
.any(|rrsig| if let &RData::SIG(ref sig) = rrsig.rdata() {
|
||||
if rrsigs.iter().filter(|rrsig| rrsig.rr_type() == RecordType::RRSIG).any(|rrsig| {
|
||||
if let &RData::SIG(ref sig) = rrsig.rdata() {
|
||||
return RecordType::DNSKEY == rrset.record_type && sig.signer_name() == &rrset.name;
|
||||
} else {
|
||||
panic!("expected a SIG here");
|
||||
}) {
|
||||
}
|
||||
}) {
|
||||
// in this case it was looks like a self-signed key, first validate the signature
|
||||
// then return rrset. Like the standard case below, the DNSKEY is validated
|
||||
// after this function. This function is only responsible for validating the signature
|
||||
@ -649,11 +653,12 @@ fn verify_default_rrset<H>(client: SecureClientHandle<H>,
|
||||
validation: {}, {:?}",
|
||||
rrset.name,
|
||||
rrset.record_type))
|
||||
.into()));
|
||||
.into()));
|
||||
}
|
||||
|
||||
// as long as any of the verifcations is good, then the RRSET is valid.
|
||||
let select = select_ok(verifications)
|
||||
let select =
|
||||
select_ok(verifications)
|
||||
// getting here means at least one of the rrsigs succeeded...
|
||||
.map(move |(rrset, rest)| {
|
||||
drop(rest); // drop all others, should free up Rc
|
||||
@ -695,11 +700,11 @@ fn verify_rrset_with_dnskey(dnskey: &DNSKEY, sig: &SIG, rrset: &Rrset) -> Client
|
||||
.and_then(|rrset_hash| {
|
||||
signer.verify(&rrset_hash, sig.sig())
|
||||
.map(|_| {
|
||||
debug!("verified rrset: {}, type: {:?}",
|
||||
rrset.name,
|
||||
rrset.record_type);
|
||||
()
|
||||
})
|
||||
debug!("verified rrset: {}, type: {:?}",
|
||||
rrset.name,
|
||||
rrset.record_type);
|
||||
()
|
||||
})
|
||||
.map_err(|e| e.into())
|
||||
})
|
||||
}
|
||||
@ -782,15 +787,18 @@ fn verify_nsec(query: &Query, nsecs: Vec<&Record>) -> bool {
|
||||
}
|
||||
|
||||
// based on the WTF? above, we will ignore any NSEC records of the same name
|
||||
if nsecs.iter()
|
||||
.filter(|r| query.name() != r.name())
|
||||
.any(|r| query.name() > r.name() && {
|
||||
if let &RData::NSEC(ref rdata) = r.rdata() {
|
||||
query.name() < rdata.next_domain_name()
|
||||
} else {
|
||||
panic!("expected NSEC was {:?}", r.rr_type()) // valid panic, never should happen
|
||||
if nsecs.iter().filter(|r| query.name() != r.name()).any(|r| {
|
||||
query.name() > r.name() &&
|
||||
{
|
||||
if let &RData::NSEC(ref rdata) = r.rdata() {
|
||||
query.name() < rdata.next_domain_name()
|
||||
} else {
|
||||
panic!("expected NSEC was {:?}", r.rr_type()) // valid panic, never should happen
|
||||
}
|
||||
}
|
||||
}) {
|
||||
return true;
|
||||
}
|
||||
}) { return true }
|
||||
|
||||
// TODO: need to validate ANY or *.domain record existance, which doesn't make sense since
|
||||
// that would have been returned in the request
|
||||
|
@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
//! TCP protocol related components for DNS.
|
||||
//! TCP protocol related components for DNS
|
||||
|
||||
mod tcp_client_connection;
|
||||
mod tcp_client_stream;
|
||||
|
@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! TCP based DNS client
|
||||
//! TCP based DNS client connection for Client impls
|
||||
|
||||
use std::net::SocketAddr;
|
||||
use std::io;
|
||||
@ -21,11 +21,13 @@ use futures::Future;
|
||||
use tokio_core::net::TcpStream;
|
||||
use tokio_core::reactor::Core;
|
||||
|
||||
use ::error::*;
|
||||
use error::*;
|
||||
use client::{ClientConnection, ClientStreamHandle};
|
||||
use tcp::TcpClientStream;
|
||||
|
||||
/// TCP based DNS client
|
||||
/// Tcp client connection
|
||||
///
|
||||
/// Use with `trust_dns::client::Client` impls
|
||||
pub struct TcpClientConnection {
|
||||
io_loop: Core,
|
||||
tcp_client_stream: Box<Future<Item = TcpClientStream<TcpStream>, Error = io::Error>>,
|
||||
@ -47,17 +49,17 @@ impl TcpClientConnection {
|
||||
io_loop.handle());
|
||||
|
||||
Ok(TcpClientConnection {
|
||||
io_loop: io_loop,
|
||||
tcp_client_stream: tcp_client_stream,
|
||||
client_stream_handle: handle,
|
||||
})
|
||||
io_loop: io_loop,
|
||||
tcp_client_stream: tcp_client_stream,
|
||||
client_stream_handle: handle,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientConnection for TcpClientConnection {
|
||||
type MessageStream = TcpClientStream<TcpStream>;
|
||||
|
||||
fn unwrap(self) -> (Core, Box<Future<Item=Self::MessageStream, Error=io::Error>>, Box<ClientStreamHandle>) {
|
||||
fn unwrap(self) -> (Core, Box<Future<Item=Self::MessageStream, Error=io::Error>>, Box<ClientStreamHandle>){
|
||||
(self.io_loop, self.tcp_client_stream, self.client_stream_handle)
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,9 @@ use BufClientStreamHandle;
|
||||
use tcp::TcpStream;
|
||||
use client::ClientStreamHandle;
|
||||
|
||||
/// Tcp client stream
|
||||
///
|
||||
/// Use with `trust_dns::client::ClientFuture` impls
|
||||
#[must_use = "futures do nothing unless polled"]
|
||||
pub struct TcpClientStream<S> {
|
||||
tcp_stream: TcpStream<S>,
|
||||
@ -32,17 +35,15 @@ impl TcpClientStream<TokioTcpStream> {
|
||||
Box<ClientStreamHandle>) {
|
||||
let (stream_future, sender) = TcpStream::new(name_server, loop_handle);
|
||||
|
||||
let new_future: Box<Future<Item=TcpClientStream<TokioTcpStream>, Error=io::Error>> =
|
||||
Box::new(stream_future.map(move |tcp_stream| {
|
||||
TcpClientStream {
|
||||
tcp_stream: tcp_stream,
|
||||
}
|
||||
}));
|
||||
let new_future: Box<Future<Item = TcpClientStream<TokioTcpStream>, Error = io::Error>> =
|
||||
Box::new(stream_future.map(move |tcp_stream| {
|
||||
TcpClientStream { tcp_stream: tcp_stream }
|
||||
}));
|
||||
|
||||
let sender = Box::new(BufClientStreamHandle {
|
||||
name_server: name_server,
|
||||
sender: sender,
|
||||
});
|
||||
name_server: name_server,
|
||||
sender: sender,
|
||||
});
|
||||
|
||||
(new_future, sender)
|
||||
}
|
||||
|
@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
//! TCP protocol related components for DNS.
|
||||
//! TLS protocol related components for DNS over TLS
|
||||
|
||||
mod tls_client_connection;
|
||||
mod tls_client_stream;
|
||||
|
@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! TCP based DNS client
|
||||
//! TLS based DNS client connection for Client impls
|
||||
|
||||
use std::net::SocketAddr;
|
||||
use std::io;
|
||||
@ -26,11 +26,13 @@ use openssl::x509::X509 as OpensslX509;
|
||||
use security_framework::certificate::SecCertificate;
|
||||
use tokio_core::reactor::Core;
|
||||
|
||||
use ::error::*;
|
||||
use error::*;
|
||||
use client::{ClientConnection, ClientStreamHandle};
|
||||
use tls::{TlsClientStream, TlsClientStreamBuilder};
|
||||
|
||||
/// TCP based DNS client
|
||||
/// Tls client connection
|
||||
///
|
||||
/// Use with `trust_dns::client::Client` impls
|
||||
pub struct TlsClientConnection {
|
||||
io_loop: Core,
|
||||
tls_client_stream: Box<Future<Item = TlsClientStream, Error = io::Error>>,
|
||||
@ -46,7 +48,7 @@ impl TlsClientConnection {
|
||||
impl ClientConnection for TlsClientConnection {
|
||||
type MessageStream = TlsClientStream;
|
||||
|
||||
fn unwrap(self) -> (Core, Box<Future<Item=Self::MessageStream, Error=io::Error>>, Box<ClientStreamHandle>) {
|
||||
fn unwrap(self) -> (Core, Box<Future<Item=Self::MessageStream, Error=io::Error>>, Box<ClientStreamHandle>){
|
||||
(self.io_loop, self.tls_client_stream, self.client_stream_handle)
|
||||
}
|
||||
}
|
||||
@ -54,11 +56,17 @@ impl ClientConnection for TlsClientConnection {
|
||||
pub struct TlsClientConnectionBuilder(TlsClientStreamBuilder);
|
||||
|
||||
impl TlsClientConnectionBuilder {
|
||||
/// Add a custom trusted peer certificate or certificate auhtority.
|
||||
///
|
||||
/// If this is the 'client' then the 'server' must have it associated as it's `identity`, or have had the `identity` signed by this certificate.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn add_ca(&mut self, ca: SecCertificate) {
|
||||
self.0.add_ca(ca);
|
||||
}
|
||||
|
||||
/// Add a custom trusted peer certificate or certificate auhtority.
|
||||
///
|
||||
/// If this is the 'client' then the 'server' must have it associated as it's `identity`, or have had the `identity` signed by this certificate.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn add_ca(&mut self, ca: OpensslX509) {
|
||||
self.0.add_ca(ca);
|
||||
@ -77,7 +85,9 @@ impl TlsClientConnectionBuilder {
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name_server` - address of the name server to use for queries
|
||||
/// * `name_server` - IP and Port for the remote DNS resolver
|
||||
/// * `subject_name` - The Subject Public Key Info (SPKI) name as associated to a certificate
|
||||
/// * `loop_handle` - The reactor Core handle
|
||||
pub fn build(self,
|
||||
name_server: SocketAddr,
|
||||
subject_name: String)
|
||||
@ -86,9 +96,9 @@ impl TlsClientConnectionBuilder {
|
||||
let (tls_client_stream, handle) = self.0.build(name_server, subject_name, io_loop.handle());
|
||||
|
||||
Ok(TlsClientConnection {
|
||||
io_loop: io_loop,
|
||||
tls_client_stream: tls_client_stream,
|
||||
client_stream_handle: handle,
|
||||
})
|
||||
io_loop: io_loop,
|
||||
tls_client_stream: tls_client_stream,
|
||||
client_stream_handle: handle,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -35,11 +35,17 @@ impl TlsClientStream {
|
||||
pub struct TlsClientStreamBuilder(TlsStreamBuilder);
|
||||
|
||||
impl TlsClientStreamBuilder {
|
||||
/// Add a custom trusted peer certificate or certificate auhtority.
|
||||
///
|
||||
/// If this is the 'client' then the 'server' must have it associated as it's `identity`, or have had the `identity` signed by this certificate.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn add_ca(&mut self, ca: SecCertificate) {
|
||||
self.0.add_ca(ca);
|
||||
}
|
||||
|
||||
/// Add a custom trusted peer certificate or certificate auhtority.
|
||||
///
|
||||
/// If this is the 'client' then the 'server' must have it associated as it's `identity`, or have had the `identity` signed by this certificate.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn add_ca(&mut self, ca: OpensslX509) {
|
||||
self.0.add_ca(ca);
|
||||
@ -47,10 +53,17 @@ impl TlsClientStreamBuilder {
|
||||
|
||||
/// Client side identity for client auth in TLS (aka mutual TLS auth)
|
||||
#[cfg(feature = "mtls")]
|
||||
pub fn identity(&mut self, pkcs12: Pkcs12) {
|
||||
pub fn identity(&mut self, pkcs12: Pkcs12) {
|
||||
self.0.identity(pkcs12);
|
||||
}
|
||||
|
||||
/// Creates a new TlsStream to the specified name_server
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name_server` - IP and Port for the remote DNS resolver
|
||||
/// * `subject_name` - The Subject Public Key Info (SPKI) name as associated to a certificate
|
||||
/// * `loop_handle` - The reactor Core handle
|
||||
pub fn build
|
||||
(self,
|
||||
name_server: SocketAddr,
|
||||
@ -63,9 +76,9 @@ pub fn identity(&mut self, pkcs12: Pkcs12) {
|
||||
Box::new(stream_future.map(move |tls_stream| TcpClientStream::from_stream(tls_stream)));
|
||||
|
||||
let sender = Box::new(BufClientStreamHandle {
|
||||
name_server: name_server,
|
||||
sender: sender,
|
||||
});
|
||||
name_server: name_server,
|
||||
sender: sender,
|
||||
});
|
||||
|
||||
(new_future, sender)
|
||||
}
|
||||
|
@ -78,14 +78,14 @@ impl TlsStream {
|
||||
// if there was a pkcs12 associated, we'll add it to the identity
|
||||
if let Some(pkcs12) = pkcs12 {
|
||||
try!(tls.identity(pkcs12).map_err(|e| {
|
||||
io::Error::new(io::ErrorKind::ConnectionRefused,
|
||||
format!("tls error: {}", e))
|
||||
}));
|
||||
io::Error::new(io::ErrorKind::ConnectionRefused,
|
||||
format!("tls error: {}", e))
|
||||
}));
|
||||
}
|
||||
tls.build().map_err(|e| {
|
||||
io::Error::new(io::ErrorKind::ConnectionRefused,
|
||||
format!("tls error: {}", e))
|
||||
})
|
||||
io::Error::new(io::ErrorKind::ConnectionRefused,
|
||||
format!("tls error: {}", e))
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
@ -107,14 +107,14 @@ impl TlsStream {
|
||||
}));
|
||||
}
|
||||
builder.build().map_err(|e| {
|
||||
io::Error::new(io::ErrorKind::ConnectionRefused,
|
||||
format!("tls error: {}", e))
|
||||
})
|
||||
io::Error::new(io::ErrorKind::ConnectionRefused,
|
||||
format!("tls error: {}", e))
|
||||
})
|
||||
}
|
||||
|
||||
/// Initializes a TcpStream with an existing tokio_core::net::TcpStream.
|
||||
/// Initializes a TlsStream with an existing tokio_tls::TlsStream.
|
||||
///
|
||||
/// This is intended for use with a TcpListener and Incoming.
|
||||
/// This is intended for use with a TlsListener and Incoming connections
|
||||
pub fn from_tls_stream(stream: TokioTlsStream<TokioTcpStream>,
|
||||
peer_addr: SocketAddr)
|
||||
-> (Self, BufStreamHandle) {
|
||||
@ -184,9 +184,6 @@ impl TlsStreamBuilder {
|
||||
/// * `name_server` - IP and Port for the remote DNS resolver
|
||||
/// * `subject_name` - The Subject Public Key Info (SPKI) name as associated to a certificate
|
||||
/// * `loop_handle` - The reactor Core handle
|
||||
/// * `certs` - list of trusted certificate authorities
|
||||
/// * `pkcs12` - optional client identity for client auth (i.e. for mutual TLS authentication)
|
||||
/// TODO: make a builder for the certifiates...
|
||||
pub fn build(self,
|
||||
name_server: SocketAddr,
|
||||
subject_name: String,
|
||||
@ -212,17 +209,19 @@ impl TlsStreamBuilder {
|
||||
Box::new(tcp.and_then(move |tcp_stream| {
|
||||
tls_connector.connect_async(&subject_name, tcp_stream)
|
||||
.map(move |s| {
|
||||
TcpStream::from_stream_with_receiver(s, name_server, outbound_messages)
|
||||
})
|
||||
TcpStream::from_stream_with_receiver(s,
|
||||
name_server,
|
||||
outbound_messages)
|
||||
})
|
||||
.map_err(|e| {
|
||||
io::Error::new(io::ErrorKind::ConnectionRefused,
|
||||
format!("tls error: {}", e))
|
||||
})
|
||||
io::Error::new(io::ErrorKind::ConnectionRefused,
|
||||
format!("tls error: {}", e))
|
||||
})
|
||||
})
|
||||
.map_err(|e| {
|
||||
io::Error::new(io::ErrorKind::ConnectionRefused,
|
||||
format!("tls error: {}", e))
|
||||
}));
|
||||
.map_err(|e| {
|
||||
io::Error::new(io::ErrorKind::ConnectionRefused,
|
||||
format!("tls error: {}", e))
|
||||
}));
|
||||
|
||||
(stream, message_sender)
|
||||
}
|
||||
|
@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
//! UDP protocol related components for DNS.
|
||||
//! UDP protocol related components for DNS
|
||||
|
||||
mod udp_client_connection;
|
||||
mod udp_client_stream;
|
||||
|
@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! UDP based DNS client
|
||||
//! UDP based DNS client connection for Client impls
|
||||
|
||||
use std::io;
|
||||
use std::net::SocketAddr;
|
||||
@ -20,11 +20,13 @@ use std::net::SocketAddr;
|
||||
use futures::Future;
|
||||
use tokio_core::reactor::Core;
|
||||
|
||||
use ::error::*;
|
||||
use error::*;
|
||||
use client::{ClientConnection, ClientStreamHandle};
|
||||
use udp::UdpClientStream;
|
||||
|
||||
/// UDP based DNS client
|
||||
/// UDP based DNS Client connection
|
||||
///
|
||||
/// Use with `trust_dns::client::Client` impls
|
||||
pub struct UdpClientConnection {
|
||||
io_loop: Core,
|
||||
udp_client_stream: Box<Future<Item = UdpClientStream, Error = io::Error>>,
|
||||
@ -45,17 +47,17 @@ impl UdpClientConnection {
|
||||
let (udp_client_stream, handle) = UdpClientStream::new(name_server, io_loop.handle());
|
||||
|
||||
Ok(UdpClientConnection {
|
||||
io_loop: io_loop,
|
||||
udp_client_stream: udp_client_stream,
|
||||
client_stream_handle: handle,
|
||||
})
|
||||
io_loop: io_loop,
|
||||
udp_client_stream: udp_client_stream,
|
||||
client_stream_handle: handle,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientConnection for UdpClientConnection {
|
||||
type MessageStream = UdpClientStream;
|
||||
|
||||
fn unwrap(self) -> (Core, Box<Future<Item=Self::MessageStream, Error=io::Error>>, Box<ClientStreamHandle>) {
|
||||
fn unwrap(self) -> (Core, Box<Future<Item=Self::MessageStream, Error=io::Error>>, Box<ClientStreamHandle>){
|
||||
(self.io_loop, self.udp_client_stream, self.client_stream_handle)
|
||||
}
|
||||
}
|
||||
|
@ -99,7 +99,11 @@ fn root_ca() -> (PKey, X509Name, X509) {
|
||||
x509_build.set_pubkey(&pkey).unwrap();
|
||||
x509_build.set_serial_number(&serial).unwrap();
|
||||
|
||||
let basic_constraints = BasicConstraints::new().critical().ca().build().unwrap();
|
||||
let basic_constraints = BasicConstraints::new()
|
||||
.critical()
|
||||
.ca()
|
||||
.build()
|
||||
.unwrap();
|
||||
x509_build.append_extension(basic_constraints).unwrap();
|
||||
|
||||
let subject_alternative_name = SubjectAlternativeName::new()
|
||||
@ -134,15 +138,11 @@ fn cert(subject_name: &str, ca_pkey: &PKey, ca_name: &X509Name, _: &X509) -> (PK
|
||||
x509_build.set_pubkey(&pkey).unwrap();
|
||||
x509_build.set_serial_number(&serial).unwrap();
|
||||
|
||||
let ext_key_usage = ExtendedKeyUsage::new()
|
||||
.server_auth()
|
||||
.build()
|
||||
.unwrap();
|
||||
let ext_key_usage = ExtendedKeyUsage::new().server_auth().build().unwrap();
|
||||
x509_build.append_extension(ext_key_usage).unwrap();
|
||||
|
||||
let subject_key_identifier = SubjectKeyIdentifier::new()
|
||||
.build(&x509_build.x509v3_context(None, None))
|
||||
.unwrap();
|
||||
let subject_key_identifier =
|
||||
SubjectKeyIdentifier::new().build(&x509_build.x509v3_context(None, None)).unwrap();
|
||||
x509_build.append_extension(subject_key_identifier).unwrap();
|
||||
|
||||
let authority_key_identifier = AuthorityKeyIdentifier::new()
|
||||
|
@ -27,6 +27,7 @@ readme = "../README.md"
|
||||
# This is a small list of keywords used to categorize and search for this
|
||||
# package.
|
||||
keywords = ["DNS", "BIND", "dig", "named", "dnssec"]
|
||||
categories = ["network-programming"]
|
||||
|
||||
# This is a string description of the license for this package. Currently
|
||||
# crates.io will validate the license provided against a whitelist of known
|
||||
@ -37,6 +38,9 @@ license = "MIT/Apache-2.0"
|
||||
# custom build steps
|
||||
build = "build.rs"
|
||||
|
||||
[badges]
|
||||
travis-ci = { repository = "bluejekyll/trust-dns" }
|
||||
|
||||
[features]
|
||||
default = ["trust-dns/tls"]
|
||||
ring = ["trust-dns/ring"]
|
||||
|
@ -372,30 +372,24 @@ fn generate_cert(subject_name: &str,
|
||||
try!(x509_build.set_pubkey(&pkey));
|
||||
try!(x509_build.set_serial_number(&serial));
|
||||
|
||||
let ext_key_usage = try!(ExtendedKeyUsage::new()
|
||||
.server_auth()
|
||||
.client_auth()
|
||||
.build());
|
||||
let ext_key_usage = try!(ExtendedKeyUsage::new().server_auth().client_auth().build());
|
||||
try!(x509_build.append_extension(ext_key_usage));
|
||||
|
||||
let subject_key_identifier =
|
||||
try!(SubjectKeyIdentifier::new().build(&x509_build.x509v3_context(None, None)));
|
||||
let subject_key_identifier = try!(SubjectKeyIdentifier::new()
|
||||
.build(&x509_build.x509v3_context(None, None)));
|
||||
try!(x509_build.append_extension(subject_key_identifier));
|
||||
|
||||
let authority_key_identifier = try!(AuthorityKeyIdentifier::new()
|
||||
.keyid(true)
|
||||
.build(&x509_build.x509v3_context(None, None)));
|
||||
.keyid(true)
|
||||
.build(&x509_build.x509v3_context(None, None)));
|
||||
try!(x509_build.append_extension(authority_key_identifier));
|
||||
|
||||
let subject_alternative_name = try!(SubjectAlternativeName::new()
|
||||
.dns(subject_name)
|
||||
.build(&x509_build.x509v3_context(None, None)));
|
||||
.dns(subject_name)
|
||||
.build(&x509_build.x509v3_context(None, None)));
|
||||
try!(x509_build.append_extension(subject_alternative_name));
|
||||
|
||||
let basic_constraints = try!(BasicConstraints::new()
|
||||
.critical()
|
||||
.ca()
|
||||
.build());
|
||||
let basic_constraints = try!(BasicConstraints::new().critical().ca().build());
|
||||
try!(x509_build.append_extension(basic_constraints));
|
||||
|
||||
try!(x509_build.sign(&pkey, hash::MessageDigest::sha256()));
|
||||
|
Loading…
Reference in New Issue
Block a user