[secDNS] EPP <create> command (#20)

This commit is contained in:
kmkaplan 2023-04-18 15:57:57 +02:00 committed by GitHub
parent c0e58dec04
commit 976fd2c002
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 611 additions and 0 deletions

506
src/extensions/secdns.rs Normal file
View File

@ -0,0 +1,506 @@
//! DNS security extensions mapping
//!
//! As described in [RFC 5910](https://www.rfc-editor.org/rfc/rfc5910)
use instant_xml::{Error, Id, Serializer, ToXml};
use std::borrow::Cow;
use std::fmt::Write;
use std::time::Duration;
use crate::request::{Extension, Transaction};
pub const XMLNS: &str = "urn:ietf:params:xml:ns:secDNS-1.1";
impl<'a> Transaction<CreateData<'a>> for crate::domain::create::DomainCreate<'a> {}
impl<'a> Extension for CreateData<'a> {
type Response = ();
}
#[derive(Debug, ToXml)]
#[xml(rename = "create", ns(XMLNS))]
pub struct CreateData<'a> {
data: DsOrKeyType<'a>,
}
impl<'a> From<&'a [DsDataType<'a>]> for CreateData<'a> {
fn from(s: &'a [DsDataType<'a>]) -> Self {
Self {
data: DsOrKeyType {
maximum_signature_lifetime: None,
data: DsOrKeyData::DsData(s),
},
}
}
}
impl<'a> From<&'a [KeyDataType<'a>]> for CreateData<'a> {
fn from(s: &'a [KeyDataType<'a>]) -> Self {
Self {
data: DsOrKeyType {
maximum_signature_lifetime: None,
data: DsOrKeyData::KeyData(s),
},
}
}
}
impl<'a> From<(Duration, &'a [DsDataType<'a>])> for CreateData<'a> {
fn from((maximum_signature_lifetime, data): (Duration, &'a [DsDataType<'a>])) -> Self {
Self {
data: DsOrKeyType {
maximum_signature_lifetime: Some(maximum_signature_lifetime),
data: DsOrKeyData::DsData(data),
},
}
}
}
impl<'a> From<(Duration, &'a [KeyDataType<'a>])> for CreateData<'a> {
fn from((maximum_signature_lifetime, data): (Duration, &'a [KeyDataType<'a>])) -> Self {
Self {
data: DsOrKeyType {
maximum_signature_lifetime: Some(maximum_signature_lifetime),
data: DsOrKeyData::KeyData(data),
},
}
}
}
/// Struct supporting either the `dsData` or the `keyData` interface.
#[derive(Debug)]
pub struct DsOrKeyType<'a> {
maximum_signature_lifetime: Option<Duration>,
data: DsOrKeyData<'a>,
}
impl ToXml for DsOrKeyType<'_> {
fn serialize<W: Write + ?Sized>(
&self,
_: Option<Id<'_>>,
serializer: &mut Serializer<'_, W>,
) -> Result<(), Error> {
if let Some(maximum_signature_lifetime) = self.maximum_signature_lifetime {
let nc_name = "maxSigLife";
let prefix = serializer.write_start(nc_name, XMLNS)?;
serializer.end_start()?;
maximum_signature_lifetime
.as_secs()
.serialize(None, serializer)?;
serializer.write_close(prefix, nc_name)?;
}
match &self.data {
DsOrKeyData::DsData(data) => data.serialize(None, serializer)?,
DsOrKeyData::KeyData(data) => data.serialize(None, serializer)?,
}
Ok(())
}
}
#[derive(Debug, ToXml)]
#[xml(forward)]
pub enum DsOrKeyData<'a> {
DsData(&'a [DsDataType<'a>]),
KeyData(&'a [KeyDataType<'a>]),
}
#[derive(Debug, ToXml)]
#[xml(rename = "dsData", ns(XMLNS))]
pub struct DsDataType<'a> {
#[xml(rename = "keyTag")]
key_tag: u16,
#[xml(rename = "alg")]
algorithm: Algorithm,
#[xml(rename = "digestType")]
digest_type: DigestAlgorithm,
digest: Cow<'a, str>,
#[xml(rename = "keyData")]
key_data: Option<KeyDataType<'a>>,
}
impl<'a> DsDataType<'a> {
pub fn new(
key_tag: u16,
algorithm: Algorithm,
digest_type: DigestAlgorithm,
digest: &'a str,
key_data: Option<KeyDataType<'a>>,
) -> Self {
Self {
key_tag,
algorithm,
digest_type,
digest: digest.into(),
key_data,
}
}
}
/// DigestAlgorithm identifies the algorithm used to construct the digest
/// https://www.iana.org/assignments/ds-rr-types/ds-rr-types.xhtml
#[derive(Clone, Copy, Debug)]
// XXX Do NOT derive PartialEq, Hash or Ord because the variant
// Other(u8) could clash with one of the other variants. They have to
// be hand coded.
pub enum DigestAlgorithm {
Sha1,
Sha256,
Gost,
Sha384,
Other(u8),
}
impl From<DigestAlgorithm> for u8 {
fn from(s: DigestAlgorithm) -> Self {
match s {
DigestAlgorithm::Sha1 => 1,
DigestAlgorithm::Sha256 => 2,
DigestAlgorithm::Gost => 3,
DigestAlgorithm::Sha384 => 4,
DigestAlgorithm::Other(n) => n,
}
}
}
impl ToXml for DigestAlgorithm {
fn serialize<W: Write + ?Sized>(
&self,
id: Option<Id<'_>>,
serializer: &mut Serializer<'_, W>,
) -> Result<(), Error> {
u8::from(*self).serialize(id, serializer)
}
}
/// Algorithm identifies the public key's cryptographic algorithm
/// https://www.iana.org/assignments/dns-sec-alg-numbers/dns-sec-alg-numbers.xhtml#dns-sec-alg-numbers-1
#[derive(Clone, Copy, Debug)]
// XXX Do NOT derive PartialEq, Hash or Ord because the variant
// Other(u8) could clash with one of the other variants. They have to
// be hand coded.
pub enum Algorithm {
// Delete DS
Delete,
/// RSA/MD5
RsaMd5,
/// Diffie-Hellman
Dh,
/// DSA/SHA-1
Dsa,
/// Elliptic Curve
Ecc,
/// RSA/SHA-1
RsaSha1,
/// DSA-NSEC3-SHA1
DsaNsec3Sha1,
/// RSASHA1-NSEC3-SHA1
RsaSha1Nsec3Sha1,
/// RSA/SHA-256
RsaSha256,
/// RSA/SHA-512
RsaSha512,
/// GOST R 34.10-2001
EccGost,
/// ECDSA Curve P-256 with SHA-256
EcdsaP256Sha256,
/// ECDSA Curve P-384 with SHA-384
EcdsaP384Sha384,
/// Ed25519
Ed25519,
/// Ed448
Ed448,
/// Indirect
Indirect,
/// Private
PrivateDns,
/// Private
PrivateOid,
Other(u8),
}
impl From<Algorithm> for u8 {
fn from(s: Algorithm) -> Self {
match s {
Algorithm::Delete => 0,
Algorithm::RsaMd5 => 1,
Algorithm::Dh => 2,
Algorithm::Dsa => 3,
// RFC 4034
Algorithm::Ecc => 4,
Algorithm::RsaSha1 => 5,
Algorithm::DsaNsec3Sha1 => 6,
Algorithm::RsaSha1Nsec3Sha1 => 7,
Algorithm::RsaSha256 => 8,
Algorithm::RsaSha512 => 10,
Algorithm::EccGost => 12,
Algorithm::EcdsaP256Sha256 => 13,
Algorithm::EcdsaP384Sha384 => 14,
Algorithm::Ed25519 => 15,
Algorithm::Ed448 => 16,
Algorithm::Indirect => 252,
Algorithm::PrivateDns => 253,
Algorithm::PrivateOid => 254,
Algorithm::Other(n) => n,
}
}
}
impl ToXml for Algorithm {
fn serialize<W: Write + ?Sized>(
&self,
id: Option<Id<'_>>,
serializer: &mut Serializer<'_, W>,
) -> Result<(), Error> {
u8::from(*self).serialize(id, serializer)
}
}
#[derive(Debug, ToXml)]
#[xml(rename = "keyData", ns(XMLNS))]
pub struct KeyDataType<'a> {
flags: Flags,
protocol: Protocol,
#[xml(rename = "alg")]
algorithm: Algorithm,
#[xml(rename = "pubKey")]
public_key: Cow<'a, str>,
}
impl<'a> KeyDataType<'a> {
pub fn new(
flags: Flags,
protocol: Protocol,
algorithm: Algorithm,
public_key: &'a str,
) -> Self {
Self {
flags,
protocol,
algorithm,
public_key: public_key.into(),
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct Flags {
/// Zone Key flag. If `true` then the DNSKEY record holds a DNS
/// zone key. If `false` then the DNSKEY record holds some other
/// type of DNS public key.
zone_key: bool,
/// Secure Entry Point. If `true` then the DNSKEY record holds a
/// key intended for use as a secure entry point.
secure_entry_point: bool,
}
impl From<Flags> for u16 {
fn from(flags: Flags) -> Self {
let mut res = 0;
if flags.zone_key {
res |= 0b1_0000_0000;
}
if flags.secure_entry_point {
res |= 0x1;
}
res
}
}
impl ToXml for Flags {
fn serialize<W: Write + ?Sized>(
&self,
id: Option<Id<'_>>,
serializer: &mut Serializer<'_, W>,
) -> Result<(), Error> {
u16::from(*self).serialize(id, serializer)
}
}
/// `Flags` for a zone signing key.
pub const FLAGS_DNS_ZONE_KEY: Flags = Flags {
zone_key: true,
secure_entry_point: false,
};
/// `Flags` for a key signing key.
pub const FLAGS_DNS_ZONE_KEY_SEP: Flags = Flags {
zone_key: true,
secure_entry_point: true,
};
#[derive(Clone, Copy, Debug)]
// XXX Do NOT derive PartialEq, Hash or Ord because the variant
// Other(u8) could clash with one of the other variants. They have to
// be hand coded.
pub enum Protocol {
/// RFC 2535, reserved
Tls,
/// RFC 2535, reserved
Email,
/// RFC 5034 DNSSEC
Dnssec,
/// RFC 2535, reserved
Ipsec,
/// RFC 2535
All,
Other(u8),
}
impl From<Protocol> for u8 {
fn from(s: Protocol) -> Self {
match s {
Protocol::Tls => 1,
Protocol::Email => 2,
Protocol::Dnssec => 3,
Protocol::Ipsec => 4,
Protocol::All => 255,
Protocol::Other(n) => n,
}
}
}
impl ToXml for Protocol {
fn serialize<W: Write + ?Sized>(
&self,
id: Option<Id<'_>>,
serializer: &mut Serializer<'_, W>,
) -> Result<(), Error> {
u8::from(*self).serialize(id, serializer)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain;
use crate::tests::assert_serialized;
#[test]
fn create_ds_data_interface() {
let ds_data = [DsDataType::new(
12345,
Algorithm::Dsa,
DigestAlgorithm::Sha1,
"49FD46E6C4B45C55D4AC",
None,
)];
let extension = CreateData::from((Duration::from_secs(604800), ds_data.as_ref()));
let ns = [
domain::HostInfo::Obj(domain::HostObj {
name: "ns1.example.com".into(),
}),
domain::HostInfo::Obj(domain::HostObj {
name: "ns2.example.com".into(),
}),
];
let contact = [
domain::DomainContact {
contact_type: "admin".into(),
id: "sh8013".into(),
},
domain::DomainContact {
contact_type: "tech".into(),
id: "sh8013".into(),
},
];
let object = domain::DomainCreate::new(
"example.com",
domain::Period::years(2).unwrap(),
Some(&ns),
Some("jd1234"),
"2fooBAR",
Some(&contact),
);
assert_serialized(
"request/extensions/secdns_create_ds.xml",
(&object, &extension),
);
}
#[test]
fn create_ds_and_key_data_interface() {
let key_data = KeyDataType::new(
FLAGS_DNS_ZONE_KEY_SEP,
Protocol::Dnssec,
Algorithm::Dsa,
"AQPJ////4Q==",
);
let ds_data = [DsDataType::new(
12345,
Algorithm::Dsa,
DigestAlgorithm::Sha1,
"49FD46E6C4B45C55D4AC",
Some(key_data),
)];
let extension = CreateData::from((Duration::from_secs(604800), ds_data.as_ref()));
let ns = [
domain::HostInfo::Obj(domain::HostObj {
name: "ns1.example.com".into(),
}),
domain::HostInfo::Obj(domain::HostObj {
name: "ns2.example.com".into(),
}),
];
let contact = [
domain::DomainContact {
contact_type: "admin".into(),
id: "sh8013".into(),
},
domain::DomainContact {
contact_type: "tech".into(),
id: "sh8013".into(),
},
];
let object = domain::DomainCreate::new(
"example.com",
domain::Period::years(2).unwrap(),
Some(&ns),
Some("jd1234"),
"2fooBAR",
Some(&contact),
);
assert_serialized(
"request/extensions/secdns_create_ds_key.xml",
(&object, &extension),
);
}
#[test]
fn create_key_data_interface() {
let key_data = [KeyDataType::new(
FLAGS_DNS_ZONE_KEY_SEP,
Protocol::Dnssec,
Algorithm::RsaMd5,
"AQPJ////4Q==",
)];
let extension = CreateData::from(key_data.as_ref());
let ns = [
domain::HostInfo::Obj(domain::HostObj {
name: "ns1.example.com".into(),
}),
domain::HostInfo::Obj(domain::HostObj {
name: "ns2.example.com".into(),
}),
];
let contact = [
domain::DomainContact {
contact_type: "admin".into(),
id: "sh8013".into(),
},
domain::DomainContact {
contact_type: "tech".into(),
id: "sh8013".into(),
},
];
let object = domain::DomainCreate::new(
"example.com",
domain::Period::years(2).unwrap(),
Some(&ns),
Some("jd1234"),
"2fooBAR",
Some(&contact),
);
assert_serialized(
"request/extensions/secdns_create_key.xml",
(&object, &extension),
);
}
}

View File

@ -64,6 +64,7 @@ pub mod extensions {
pub const XMLNS: &str = "urn:ietf:params:xml:ns:rgp-1.0";
}
pub mod secdns;
pub mod frnic;
}

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
<create>
<create xmlns="urn:ietf:params:xml:ns:domain-1.0">
<name>example.com</name>
<period unit="y">2</period>
<ns>
<hostObj>ns1.example.com</hostObj>
<hostObj>ns2.example.com</hostObj>
</ns>
<registrant>jd1234</registrant>
<contact type="admin">sh8013</contact>
<contact type="tech">sh8013</contact>
<authInfo>
<pw>2fooBAR</pw>
</authInfo>
</create>
</create>
<extension>
<create xmlns="urn:ietf:params:xml:ns:secDNS-1.1">
<maxSigLife>604800</maxSigLife>
<dsData>
<keyTag>12345</keyTag>
<alg>3</alg>
<digestType>1</digestType>
<digest>49FD46E6C4B45C55D4AC</digest>
</dsData>
</create>
</extension>
<clTRID>cltrid:1626454866</clTRID>
</command>
</epp>

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
<create>
<create xmlns="urn:ietf:params:xml:ns:domain-1.0">
<name>example.com</name>
<period unit="y">2</period>
<ns>
<hostObj>ns1.example.com</hostObj>
<hostObj>ns2.example.com</hostObj>
</ns>
<registrant>jd1234</registrant>
<contact type="admin">sh8013</contact>
<contact type="tech">sh8013</contact>
<authInfo>
<pw>2fooBAR</pw>
</authInfo>
</create>
</create>
<extension>
<create xmlns="urn:ietf:params:xml:ns:secDNS-1.1">
<maxSigLife>604800</maxSigLife>
<dsData>
<keyTag>12345</keyTag>
<alg>3</alg>
<digestType>1</digestType>
<digest>49FD46E6C4B45C55D4AC</digest>
<keyData>
<flags>257</flags>
<protocol>3</protocol>
<alg>3</alg>
<pubKey>AQPJ////4Q==</pubKey>
</keyData>
</dsData>
</create>
</extension>
<clTRID>cltrid:1626454866</clTRID>
</command>
</epp>

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
<create>
<create xmlns="urn:ietf:params:xml:ns:domain-1.0">
<name>example.com</name>
<period unit="y">2</period>
<ns>
<hostObj>ns1.example.com</hostObj>
<hostObj>ns2.example.com</hostObj>
</ns>
<registrant>jd1234</registrant>
<contact type="admin">sh8013</contact>
<contact type="tech">sh8013</contact>
<authInfo>
<pw>2fooBAR</pw>
</authInfo>
</create>
</create>
<extension>
<create xmlns="urn:ietf:params:xml:ns:secDNS-1.1">
<keyData>
<flags>257</flags>
<protocol>3</protocol>
<alg>1</alg>
<pubKey>AQPJ////4Q==</pubKey>
</keyData>
</create>
</extension>
<clTRID>cltrid:1626454866</clTRID>
</command>
</epp>