Add a new client certificate management parser (XEP-0257).

Emmanuel Gil Peyrot created

Change summary

ChangeLog              |   1 
doap.xml               |   8 +
src/cert_management.rs | 191 ++++++++++++++++++++++++++++++++++++++++++++
src/lib.rs             |   3 
src/ns.rs              |   3 
src/util/macros.rs     |  24 +++++
6 files changed, 230 insertions(+)

Detailed changes

ChangeLog 🔗

@@ -1,6 +1,7 @@
 Version NEXT:
 DATE  Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
     * New parsers/serialisers:
+        - Client Certificate Management for SASL EXTERNAL (XEP-0257)
         - Client State Indication (XEP-0352)
         - OpenPGP for XMPP (XEP-0373)
         - Anonymous unique occupant identifiers for MUCs (XEP-0421)

doap.xml 🔗

@@ -311,6 +311,14 @@
             <xmpp:since>0.1.0</xmpp:since>
         </xmpp:SupportedXep>
     </implements>
+    <implements>
+        <xmpp:SupportedXep>
+            <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0257.html"/>
+            <xmpp:status>complete</xmpp:status>
+            <xmpp:version>0.3</xmpp:version>
+            <xmpp:since>NEXT</xmpp:since>
+        </xmpp:SupportedXep>
+    </implements>
     <implements>
         <xmpp:SupportedXep>
             <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0260.html"/>

src/cert_management.rs 🔗

@@ -0,0 +1,191 @@
+// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+use crate::iq::{IqSetPayload, IqGetPayload, IqResultPayload};
+use crate::util::helpers::Base64;
+
+generate_elem_id!(
+    /// The name of a certificate.
+    Name, "name", SASL_CERT
+);
+
+generate_element!(
+    /// An X.509 certificate.
+    Cert, "x509cert", SASL_CERT,
+    text: (
+        /// The BER X.509 data.
+        data: Base64<Vec<u8>>
+    )
+);
+
+generate_element!(
+    /// For the client to upload an X.509 certificate.
+    Append, "append", SASL_CERT,
+    children: [
+        /// The name of this certificate.
+        name: Required<Name> = ("name", SASL_CERT) => Name,
+
+        /// The X.509 certificate to set.
+        cert: Required<Cert> = ("x509cert", SASL_CERT) => Cert,
+
+        /// This client is forbidden from managing certificates.
+        no_cert_management: Present<_> = ("no-cert-management", SASL_CERT) => bool
+    ]
+);
+
+impl IqSetPayload for Append {}
+
+generate_empty_element!(
+    /// Client requests the current list of X.509 certificates.
+    ListCertsQuery, "items", SASL_CERT
+);
+
+impl IqGetPayload for ListCertsQuery {}
+
+generate_elem_id!(
+    /// One resource currently using a certificate.
+    Resource, "resource", SASL_CERT
+);
+
+generate_element!(
+    /// A list of resources currently using this certificate.
+    Users, "users", SASL_CERT,
+    children: [
+        /// Resources currently using this certificate.
+        resources: Vec<Resource> = ("resource", SASL_CERT) => Resource
+    ]
+);
+
+generate_element!(
+    /// An X.509 certificate being set for this user.
+    Item, "item", SASL_CERT,
+    children: [
+        /// The name of this certificate.
+        name: Required<Name> = ("name", SASL_CERT) => Name,
+
+        /// The X.509 certificate to set.
+        cert: Required<Cert> = ("x509cert", SASL_CERT) => Cert,
+
+        /// This client is forbidden from managing certificates.
+        no_cert_management: Present<_> = ("no-cert-management", SASL_CERT) => bool,
+
+        /// List of resources currently using this certificate.
+        users: Option<Users> = ("users", SASL_CERT) => Users
+    ]
+);
+
+generate_element!(
+    /// Server answers with the current list of X.509 certificates.
+    ListCertsResponse, "items", SASL_CERT,
+    children: [
+        /// List of certificates.
+        items: Vec<Item> = ("item", SASL_CERT) => Item
+    ]
+);
+
+impl IqResultPayload for ListCertsResponse {}
+
+generate_element!(
+    /// Client disables an X.509 certificate.
+    Disable, "disable", SASL_CERT,
+    children: [
+        /// Name of the certificate to disable.
+        name: Required<Name> = ("name", SASL_CERT) => Name
+    ]
+);
+
+impl IqSetPayload for Disable {}
+
+generate_element!(
+    /// Client revokes an X.509 certificate.
+    Revoke, "revoke", SASL_CERT,
+    children: [
+        /// Name of the certificate to revoke.
+        name: Required<Name> = ("name", SASL_CERT) => Name
+    ]
+);
+
+impl IqSetPayload for Revoke {}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use minidom::Element;
+    use std::convert::TryFrom;
+    use std::str::FromStr;
+    use crate::ns;
+
+    #[test]
+    fn test_size() {
+        assert_size!(Append, 56);
+    }
+
+    #[test]
+    fn simple() {
+        let elem: Element = "<append xmlns='urn:xmpp:saslcert:1'><name>Mobile Client</name><x509cert>AAAA</x509cert></append>".parse().unwrap();
+        let append = Append::try_from(elem).unwrap();
+        assert_eq!(append.name.0, "Mobile Client");
+        assert_eq!(append.cert.data, b"\0\0\0");
+
+        let elem: Element = "<disable xmlns='urn:xmpp:saslcert:1'><name>Mobile Client</name></disable>".parse().unwrap();
+        let disable = Disable::try_from(elem).unwrap();
+        assert_eq!(disable.name.0, "Mobile Client");
+
+        let elem: Element = "<revoke xmlns='urn:xmpp:saslcert:1'><name>Mobile Client</name></revoke>".parse().unwrap();
+        let revoke = Revoke::try_from(elem).unwrap();
+        assert_eq!(revoke.name.0, "Mobile Client");
+    }
+
+    #[test]
+    fn list() {
+        let elem: Element = r#"
+        <items xmlns='urn:xmpp:saslcert:1'>
+          <item>
+            <name>Mobile Client</name>
+            <x509cert>AAAA</x509cert>
+            <users>
+              <resource>Phone</resource>
+            </users>
+          </item>
+          <item>
+            <name>Laptop</name>
+            <x509cert>BBBB</x509cert>
+          </item>
+        </items>"#.parse().unwrap();
+        let mut list = ListCertsResponse::try_from(elem).unwrap();
+        assert_eq!(list.items.len(), 2);
+
+        let item = list.items.pop().unwrap();
+        assert_eq!(item.name.0, "Laptop");
+        assert_eq!(item.cert.data, [4, 16, 65]);
+        assert!(item.users.is_none());
+
+        let item = list.items.pop().unwrap();
+        assert_eq!(item.name.0, "Mobile Client");
+        assert_eq!(item.cert.data, b"\0\0\0");
+        assert_eq!(item.users.unwrap().resources.len(), 1);
+    }
+
+    #[test]
+    fn test_serialise() {
+        let append = Append {
+            name: Name::from_str("Mobile Client").unwrap(),
+            cert: Cert { data: b"\0\0\0".to_vec() },
+            no_cert_management: false,
+        };
+        let elem: Element = append.into();
+        assert!(elem.is("append", ns::SASL_CERT));
+
+        let disable = Disable {
+            name: Name::from_str("Mobile Client").unwrap(),
+        };
+        let elem: Element = disable.into();
+        assert!(elem.is("disable", ns::SASL_CERT));
+        let elem = elem.children().cloned().collect::<Vec<_>>().pop().unwrap();
+        assert!(elem.is("name", ns::SASL_CERT));
+        assert_eq!(elem.text(), "Mobile Client");
+    }
+}

src/lib.rs 🔗

@@ -150,6 +150,9 @@ pub mod bob;
 /// XEP-0234: Jingle File Transfer
 pub mod jingle_ft;
 
+/// XEP-0257: Client Certificate Management for SASL EXTERNAL
+pub mod cert_management;
+
 /// XEP-0260: Jingle SOCKS5 Bytestreams Transport Method
 pub mod jingle_s5b;
 

src/ns.rs 🔗

@@ -140,6 +140,9 @@ pub const JINGLE_FT: &str = "urn:xmpp:jingle:apps:file-transfer:5";
 /// XEP-0234: Jingle File Transfer
 pub const JINGLE_FT_ERROR: &str = "urn:xmpp:jingle:apps:file-transfer:errors:0";
 
+/// XEP-0257: Client Certificate Management for SASL EXTERNAL
+pub const SASL_CERT: &str = "urn:xmpp:saslcert:1";
+
 /// XEP-0260: Jingle SOCKS5 Bytestreams Transport Method
 pub const JINGLE_S5B: &str = "urn:xmpp:jingle:transports:s5b:1";
 

src/util/macros.rs 🔗

@@ -494,6 +494,9 @@ macro_rules! start_decl {
     (Required, $type:ty) => (
         $type
     );
+    (Present, $type:ty) => (
+        bool
+    );
 }
 
 macro_rules! start_parse_elem {
@@ -506,6 +509,9 @@ macro_rules! start_parse_elem {
     ($temp:ident: Required) => {
         let mut $temp = None;
     };
+    ($temp:ident: Present) => {
+        let mut $temp = false;
+    };
 }
 
 macro_rules! do_parse {
@@ -548,6 +554,18 @@ macro_rules! do_parse_elem {
         }
         $temp = Some(do_parse!($elem, $constructor));
     };
+    ($temp:ident: Present = $constructor:ident => $elem:ident, $name:tt, $parent_name:tt) => {
+        if $temp {
+            return Err(crate::util::error::Error::ParseError(concat!(
+                "Element ",
+                $parent_name,
+                " must not have more than one ",
+                $name,
+                " child."
+            )));
+        }
+        $temp = true;
+    };
 }
 
 macro_rules! finish_parse_elem {
@@ -566,6 +584,9 @@ macro_rules! finish_parse_elem {
             " element."
         )))?
     };
+    ($temp:ident: Present = $name:tt, $parent_name:tt) => {
+        $temp
+    };
 }
 
 macro_rules! generate_serialiser {
@@ -595,6 +616,9 @@ macro_rules! generate_serialiser {
     ($builder:ident, $parent:ident, $elem:ident, Vec, $constructor:ident, ($name:tt, $ns:ident)) => {
         $builder.append_all($parent.$elem.into_iter())
     };
+    ($builder:ident, $parent:ident, $elem:ident, Present, $constructor:ident, ($name:tt, $ns:ident)) => {
+        $builder.append(::minidom::Node::Element(::minidom::Element::builder($name).ns(crate::ns::$ns).build()))
+    };
     ($builder:ident, $parent:ident, $elem:ident, $_:ident, $constructor:ident, ($name:tt, $ns:ident)) => {
         $builder.append(::minidom::Node::Element(::minidom::Element::from($parent.$elem)))
     };