instant_epp/extensions/
secdns.rs

1//! DNS security extensions mapping
2//!
3//! As described in [RFC 5910](https://www.rfc-editor.org/rfc/rfc5910)
4use instant_xml::{Error, Id, Serializer, ToXml};
5use std::borrow::Cow;
6use std::fmt::Write;
7use std::time::Duration;
8
9use crate::common::NoExtension;
10use crate::request::{Extension, Transaction};
11
12pub const XMLNS: &str = "urn:ietf:params:xml:ns:secDNS-1.1";
13
14impl<'a> Transaction<CreateData<'a>> for crate::domain::create::DomainCreate<'a> {}
15
16impl<'a> Extension for CreateData<'a> {
17    type Response = NoExtension;
18}
19
20#[derive(Debug, ToXml)]
21#[xml(rename = "create", ns(XMLNS))]
22pub struct CreateData<'a> {
23    data: DsOrKeyType<'a>,
24}
25
26impl<'a> From<&'a [DsDataType<'a>]> for CreateData<'a> {
27    fn from(s: &'a [DsDataType<'a>]) -> Self {
28        Self {
29            data: DsOrKeyType {
30                maximum_signature_lifetime: None,
31                data: DsOrKeyData::DsData(s),
32            },
33        }
34    }
35}
36
37impl<'a> From<&'a [KeyDataType<'a>]> for CreateData<'a> {
38    fn from(s: &'a [KeyDataType<'a>]) -> Self {
39        Self {
40            data: DsOrKeyType {
41                maximum_signature_lifetime: None,
42                data: DsOrKeyData::KeyData(s),
43            },
44        }
45    }
46}
47
48impl<'a> From<(Duration, &'a [DsDataType<'a>])> for CreateData<'a> {
49    fn from((maximum_signature_lifetime, data): (Duration, &'a [DsDataType<'a>])) -> Self {
50        Self {
51            data: DsOrKeyType {
52                maximum_signature_lifetime: Some(maximum_signature_lifetime),
53                data: DsOrKeyData::DsData(data),
54            },
55        }
56    }
57}
58
59impl<'a> From<(Duration, &'a [KeyDataType<'a>])> for CreateData<'a> {
60    fn from((maximum_signature_lifetime, data): (Duration, &'a [KeyDataType<'a>])) -> Self {
61        Self {
62            data: DsOrKeyType {
63                maximum_signature_lifetime: Some(maximum_signature_lifetime),
64                data: DsOrKeyData::KeyData(data),
65            },
66        }
67    }
68}
69
70/// Struct supporting either the `dsData` or the `keyData` interface.
71#[derive(Debug)]
72pub struct DsOrKeyType<'a> {
73    maximum_signature_lifetime: Option<Duration>,
74    data: DsOrKeyData<'a>,
75}
76
77impl ToXml for DsOrKeyType<'_> {
78    fn serialize<W: Write + ?Sized>(
79        &self,
80        _: Option<Id<'_>>,
81        serializer: &mut Serializer<'_, W>,
82    ) -> Result<(), Error> {
83        if let Some(maximum_signature_lifetime) = self.maximum_signature_lifetime {
84            let nc_name = "maxSigLife";
85            let prefix = serializer.write_start(nc_name, XMLNS)?;
86            serializer.end_start()?;
87            maximum_signature_lifetime
88                .as_secs()
89                .serialize(None, serializer)?;
90            serializer.write_close(prefix, nc_name)?;
91        }
92        match &self.data {
93            DsOrKeyData::DsData(data) => data.serialize(None, serializer)?,
94            DsOrKeyData::KeyData(data) => data.serialize(None, serializer)?,
95        }
96        Ok(())
97    }
98}
99
100#[derive(Debug, ToXml)]
101#[xml(forward)]
102pub enum DsOrKeyData<'a> {
103    DsData(&'a [DsDataType<'a>]),
104    KeyData(&'a [KeyDataType<'a>]),
105}
106
107#[derive(Debug, ToXml)]
108#[xml(rename = "dsData", ns(XMLNS))]
109pub struct DsDataType<'a> {
110    #[xml(rename = "keyTag")]
111    key_tag: u16,
112    #[xml(rename = "alg")]
113    algorithm: Algorithm,
114    #[xml(rename = "digestType")]
115    digest_type: DigestAlgorithm,
116    digest: Cow<'a, str>,
117    #[xml(rename = "keyData")]
118    key_data: Option<KeyDataType<'a>>,
119}
120
121impl<'a> DsDataType<'a> {
122    pub fn new(
123        key_tag: u16,
124        algorithm: Algorithm,
125        digest_type: DigestAlgorithm,
126        digest: impl Into<Cow<'a, str>>,
127        key_data: Option<KeyDataType<'a>>,
128    ) -> Self {
129        Self {
130            key_tag,
131            algorithm,
132            digest_type,
133            digest: digest.into(),
134            key_data,
135        }
136    }
137}
138
139/// DigestAlgorithm identifies the algorithm used to construct the digest
140/// https://www.iana.org/assignments/ds-rr-types/ds-rr-types.xhtml
141#[derive(Clone, Copy, Debug)]
142// XXX Do NOT derive PartialEq, Hash or Ord because the variant
143// Other(u8) could clash with one of the other variants. They have to
144// be hand coded.
145pub enum DigestAlgorithm {
146    Sha1,
147    Sha256,
148    Gost,
149    Sha384,
150    Other(u8),
151}
152
153impl From<DigestAlgorithm> for u8 {
154    fn from(s: DigestAlgorithm) -> Self {
155        match s {
156            DigestAlgorithm::Sha1 => 1,
157            DigestAlgorithm::Sha256 => 2,
158            DigestAlgorithm::Gost => 3,
159            DigestAlgorithm::Sha384 => 4,
160            DigestAlgorithm::Other(n) => n,
161        }
162    }
163}
164
165impl ToXml for DigestAlgorithm {
166    fn serialize<W: Write + ?Sized>(
167        &self,
168        id: Option<Id<'_>>,
169        serializer: &mut Serializer<'_, W>,
170    ) -> Result<(), Error> {
171        u8::from(*self).serialize(id, serializer)
172    }
173}
174
175/// Algorithm identifies the public key's cryptographic algorithm
176/// https://www.iana.org/assignments/dns-sec-alg-numbers/dns-sec-alg-numbers.xhtml#dns-sec-alg-numbers-1
177#[derive(Clone, Copy, Debug)]
178// XXX Do NOT derive PartialEq, Hash or Ord because the variant
179// Other(u8) could clash with one of the other variants. They have to
180// be hand coded.
181pub enum Algorithm {
182    // Delete DS
183    Delete,
184    /// RSA/MD5
185    RsaMd5,
186    /// Diffie-Hellman
187    Dh,
188    /// DSA/SHA-1
189    Dsa,
190    /// Elliptic Curve
191    Ecc,
192    /// RSA/SHA-1
193    RsaSha1,
194    /// DSA-NSEC3-SHA1
195    DsaNsec3Sha1,
196    /// RSASHA1-NSEC3-SHA1
197    RsaSha1Nsec3Sha1,
198    /// RSA/SHA-256
199    RsaSha256,
200    /// RSA/SHA-512
201    RsaSha512,
202    /// GOST R 34.10-2001
203    EccGost,
204    /// ECDSA Curve P-256 with SHA-256
205    EcdsaP256Sha256,
206    /// ECDSA Curve P-384 with SHA-384
207    EcdsaP384Sha384,
208    /// Ed25519
209    Ed25519,
210    /// Ed448
211    Ed448,
212    /// Indirect
213    Indirect,
214    /// Private
215    PrivateDns,
216    /// Private
217    PrivateOid,
218    Other(u8),
219}
220
221impl From<Algorithm> for u8 {
222    fn from(s: Algorithm) -> Self {
223        match s {
224            Algorithm::Delete => 0,
225            Algorithm::RsaMd5 => 1,
226            Algorithm::Dh => 2,
227            Algorithm::Dsa => 3,
228            // RFC 4034
229            Algorithm::Ecc => 4,
230            Algorithm::RsaSha1 => 5,
231            Algorithm::DsaNsec3Sha1 => 6,
232            Algorithm::RsaSha1Nsec3Sha1 => 7,
233            Algorithm::RsaSha256 => 8,
234            Algorithm::RsaSha512 => 10,
235            Algorithm::EccGost => 12,
236            Algorithm::EcdsaP256Sha256 => 13,
237            Algorithm::EcdsaP384Sha384 => 14,
238            Algorithm::Ed25519 => 15,
239            Algorithm::Ed448 => 16,
240            Algorithm::Indirect => 252,
241            Algorithm::PrivateDns => 253,
242            Algorithm::PrivateOid => 254,
243            Algorithm::Other(n) => n,
244        }
245    }
246}
247
248impl ToXml for Algorithm {
249    fn serialize<W: Write + ?Sized>(
250        &self,
251        id: Option<Id<'_>>,
252        serializer: &mut Serializer<'_, W>,
253    ) -> Result<(), Error> {
254        u8::from(*self).serialize(id, serializer)
255    }
256}
257
258#[derive(Debug, ToXml)]
259#[xml(rename = "keyData", ns(XMLNS))]
260pub struct KeyDataType<'a> {
261    flags: Flags,
262    protocol: Protocol,
263    #[xml(rename = "alg")]
264    algorithm: Algorithm,
265    #[xml(rename = "pubKey")]
266    public_key: Cow<'a, str>,
267}
268
269impl<'a> KeyDataType<'a> {
270    pub fn new(
271        flags: Flags,
272        protocol: Protocol,
273        algorithm: Algorithm,
274        public_key: &'a str,
275    ) -> Self {
276        Self {
277            flags,
278            protocol,
279            algorithm,
280            public_key: public_key.into(),
281        }
282    }
283}
284
285#[derive(Clone, Copy, Debug)]
286pub struct Flags {
287    /// Zone Key flag. If `true` then the DNSKEY record holds a DNS
288    /// zone key. If `false` then the DNSKEY record holds some other
289    /// type of DNS public key.
290    zone_key: bool,
291    /// Secure Entry Point. If `true` then the DNSKEY record holds a
292    /// key intended for use as a secure entry point.
293    secure_entry_point: bool,
294}
295
296impl From<Flags> for u16 {
297    fn from(flags: Flags) -> Self {
298        let mut res = 0;
299        if flags.zone_key {
300            res |= 0b1_0000_0000;
301        }
302        if flags.secure_entry_point {
303            res |= 0x1;
304        }
305        res
306    }
307}
308
309impl ToXml for Flags {
310    fn serialize<W: Write + ?Sized>(
311        &self,
312        id: Option<Id<'_>>,
313        serializer: &mut Serializer<'_, W>,
314    ) -> Result<(), Error> {
315        u16::from(*self).serialize(id, serializer)
316    }
317}
318
319/// `Flags` for a zone signing key.
320pub const FLAGS_DNS_ZONE_KEY: Flags = Flags {
321    zone_key: true,
322    secure_entry_point: false,
323};
324/// `Flags` for a key signing key.
325pub const FLAGS_DNS_ZONE_KEY_SEP: Flags = Flags {
326    zone_key: true,
327    secure_entry_point: true,
328};
329
330#[derive(Clone, Copy, Debug)]
331// XXX Do NOT derive PartialEq, Hash or Ord because the variant
332// Other(u8) could clash with one of the other variants. They have to
333// be hand coded.
334pub enum Protocol {
335    /// RFC 2535, reserved
336    Tls,
337    /// RFC 2535, reserved
338    Email,
339    /// RFC 5034 DNSSEC
340    Dnssec,
341    /// RFC 2535, reserved
342    Ipsec,
343    /// RFC 2535
344    All,
345    Other(u8),
346}
347
348impl From<Protocol> for u8 {
349    fn from(s: Protocol) -> Self {
350        match s {
351            Protocol::Tls => 1,
352            Protocol::Email => 2,
353            Protocol::Dnssec => 3,
354            Protocol::Ipsec => 4,
355            Protocol::All => 255,
356            Protocol::Other(n) => n,
357        }
358    }
359}
360
361impl ToXml for Protocol {
362    fn serialize<W: Write + ?Sized>(
363        &self,
364        id: Option<Id<'_>>,
365        serializer: &mut Serializer<'_, W>,
366    ) -> Result<(), Error> {
367        u8::from(*self).serialize(id, serializer)
368    }
369}
370
371#[cfg(test)]
372mod tests {
373    use super::*;
374    use crate::domain;
375    use crate::tests::assert_serialized;
376
377    #[test]
378    fn create_ds_data_interface() {
379        let ds_data = [DsDataType::new(
380            12345,
381            Algorithm::Dsa,
382            DigestAlgorithm::Sha1,
383            "49FD46E6C4B45C55D4AC",
384            None,
385        )];
386        let extension = CreateData::from((Duration::from_secs(604800), ds_data.as_ref()));
387        let ns = [
388            domain::HostInfo::Obj(domain::HostObj {
389                name: "ns1.example.com".into(),
390            }),
391            domain::HostInfo::Obj(domain::HostObj {
392                name: "ns2.example.com".into(),
393            }),
394        ];
395        let contact = [
396            domain::DomainContact {
397                contact_type: "admin".into(),
398                id: "sh8013".into(),
399            },
400            domain::DomainContact {
401                contact_type: "tech".into(),
402                id: "sh8013".into(),
403            },
404        ];
405        let object = domain::DomainCreate::new(
406            "example.com",
407            domain::Period::years(2).unwrap(),
408            Some(&ns),
409            Some("jd1234"),
410            "2fooBAR",
411            Some(&contact),
412        );
413        assert_serialized(
414            "request/extensions/secdns_create_ds.xml",
415            (&object, &extension),
416        );
417    }
418
419    #[test]
420    fn create_ds_and_key_data_interface() {
421        let key_data = KeyDataType::new(
422            FLAGS_DNS_ZONE_KEY_SEP,
423            Protocol::Dnssec,
424            Algorithm::Dsa,
425            "AQPJ////4Q==",
426        );
427        let ds_data = [DsDataType::new(
428            12345,
429            Algorithm::Dsa,
430            DigestAlgorithm::Sha1,
431            "49FD46E6C4B45C55D4AC",
432            Some(key_data),
433        )];
434        let extension = CreateData::from((Duration::from_secs(604800), ds_data.as_ref()));
435        let ns = [
436            domain::HostInfo::Obj(domain::HostObj {
437                name: "ns1.example.com".into(),
438            }),
439            domain::HostInfo::Obj(domain::HostObj {
440                name: "ns2.example.com".into(),
441            }),
442        ];
443        let contact = [
444            domain::DomainContact {
445                contact_type: "admin".into(),
446                id: "sh8013".into(),
447            },
448            domain::DomainContact {
449                contact_type: "tech".into(),
450                id: "sh8013".into(),
451            },
452        ];
453        let object = domain::DomainCreate::new(
454            "example.com",
455            domain::Period::years(2).unwrap(),
456            Some(&ns),
457            Some("jd1234"),
458            "2fooBAR",
459            Some(&contact),
460        );
461        assert_serialized(
462            "request/extensions/secdns_create_ds_key.xml",
463            (&object, &extension),
464        );
465    }
466
467    #[test]
468    fn create_key_data_interface() {
469        let key_data = [KeyDataType::new(
470            FLAGS_DNS_ZONE_KEY_SEP,
471            Protocol::Dnssec,
472            Algorithm::RsaMd5,
473            "AQPJ////4Q==",
474        )];
475        let extension = CreateData::from(key_data.as_ref());
476        let ns = [
477            domain::HostInfo::Obj(domain::HostObj {
478                name: "ns1.example.com".into(),
479            }),
480            domain::HostInfo::Obj(domain::HostObj {
481                name: "ns2.example.com".into(),
482            }),
483        ];
484        let contact = [
485            domain::DomainContact {
486                contact_type: "admin".into(),
487                id: "sh8013".into(),
488            },
489            domain::DomainContact {
490                contact_type: "tech".into(),
491                id: "sh8013".into(),
492            },
493        ];
494        let object = domain::DomainCreate::new(
495            "example.com",
496            domain::Period::years(2).unwrap(),
497            Some(&ns),
498            Some("jd1234"),
499            "2fooBAR",
500            Some(&contact),
501        );
502        assert_serialized(
503            "request/extensions/secdns_create_key.xml",
504            (&object, &extension),
505        );
506    }
507}