]> git.kaiwu.me - njs.git/commitdiff
Added "xml" module for working with XML documents.
authorDmitry Volyntsev <xeioex@nginx.com>
Thu, 26 Jan 2023 05:54:47 +0000 (21:54 -0800)
committerDmitry Volyntsev <xeioex@nginx.com>
Thu, 26 Jan 2023 05:54:47 +0000 (21:54 -0800)
    - xml.parse(string|buffer) returns an XMLDoc wrapper object around
        XML structure.
    - xml.c14n(root_node[, excluding_node]]) canonicalizes root_node and
        its children according to https://www.w3.org/TR/xml-c14n, optionally
        excluding_node allows to omit from the output a part of the
        document.
    - xml.exclusiveC14n(root_node[, excluding_node[, withComments [,
            prefix_list]]]) canonicalizes root_node and its children
        according to https://www.w3.org/TR/xml-exc-c14n/.  excluding_node
        allows to omit from the output a part of the document
        corresponding to the node and its children.  withComments
        is a boolean and is false by default. When withComments is true
        canonicalization corresponds to
        http://www.w3.org/2001/10/xml-exc-c14n#WithComments.  prefix_list is
        an optional string with a space separated namespace prefixes for
        namespaces that should also be included into the output.

    - XMLDoc an XMLDoc wrapper object around XML structure.
        doc.xxx returns the first root tag named "xxx" as XMLNode wrapper
        object.

    - XMLNode an XMLNode wrapper object around XML tag node.
        node.$tag$xxx returns the first child tag named "xxx" as XMLNode
        wrapper object.
        node.xxx a shorthand syntax for node.$tag$xxx.
        node.$tags$xxx? returns an array of all children tags named xxx.

        node.$attr$xxx returns an attribute value of xxx.
        node.$attrs returns an XMLAttr wrapper object.
        node.$name returns the tag name of the node.
        node.$ns returns the namespace of the node.
        node.$parent returns the parent of the node.
        node.$text returns the node's content.

    - XMLAttrs an XMLAttrs wrapper object around XML node attributes.
        attrs.xxx returns a value of the xxx attribute.

    - Example:
        const xml = require("xml");
        let data = `<note><to b="bar" a= "foo" >Tove</to><from>Jani</from></note>`;
        let doc = xml.parse(data);

        console.log(doc.note.to.$text) /* 'Tove' */
        console.log(doc.note.to.$attr$b) /* 'bar' */
        console.log(doc.note.$tags[1].$text) /* 'Jani' */

        let dec = new TextDecoder();
        let c14n = dec.decode(xml.exclusiveC14n(doc.note));
        console.log(c14n) /* '<note><to a="foo" b="bar">Tove</to><from>Jani</from></note>' */

        c14n = dec.decode(xml.exclusiveC14n(doc.note.to));
        console.log(c14n) /* '<to a="foo" b="bar">Tove</to>' */

        c14n = dec.decode(xml.exclusiveC14n(doc.note, doc.note.to /* excluding 'to' */));
        console.log(c14n) /* '<note><from>Jani</from></note>' */

24 files changed:
auto/libxml2 [new file with mode: 0644]
auto/modules
auto/options
configure
external/njs_xml_module.c [new file with mode: 0644]
nginx/config
nginx/config.make
nginx/ngx_js.c
src/test/njs_unit_test.c
test/harness/compatNjs.js [new file with mode: 0644]
test/harness/compatXml.js [new file with mode: 0644]
test/xml/README.rst [new file with mode: 0644]
test/xml/auth_r.xml [new file with mode: 0644]
test/xml/auth_r_prefix_list.xml [new file with mode: 0644]
test/xml/auth_r_prefix_list_signed.xml [new file with mode: 0644]
test/xml/auth_r_signed.xml [new file with mode: 0644]
test/xml/auth_r_signed2.xml [new file with mode: 0644]
test/xml/auth_r_with_comments_signed.xml [new file with mode: 0644]
test/xml/example.com.crt [new file with mode: 0644]
test/xml/response_assertion_and_message_signed.xml [new file with mode: 0644]
test/xml/response_signed.xml [new file with mode: 0644]
test/xml/response_signed_broken.xml [new file with mode: 0644]
test/xml/response_signed_broken2.xml [new file with mode: 0644]
test/xml/saml_verify.t.js [new file with mode: 0644]

diff --git a/auto/libxml2 b/auto/libxml2
new file mode 100644 (file)
index 0000000..e241073
--- /dev/null
@@ -0,0 +1,77 @@
+
+# Copyright (C) Dmitry Volyntsev
+# Copyright (C) NGINX, Inc.
+
+NJS_HAVE_LIBXML2=NO
+
+if [ $NJS_LIBXML2 = YES ]; then
+    njs_found=no
+
+    njs_feature="libxml2"
+    njs_feature_name=NJS_HAVE_LIBXML2
+    njs_feature_run=no
+    njs_feature_incs="/usr/include/libxml2"
+    njs_feature_libs="-lxml2"
+    njs_feature_test="#include <libxml/parser.h>
+                      #include <libxml/tree.h>
+
+                      int main() {
+                          xmlDocPtr  tree;
+                          tree = xmlReadMemory(NULL, 0, NULL, NULL, 0);
+                          xmlFreeDoc(tree);
+                          xmlCleanupParser();
+                          return 0;
+                      }"
+    . auto/feature
+
+    if [ $njs_found = no ]; then
+
+        # FreeBSD port
+
+        njs_feature="libxml2 in /usr/local/"
+        njs_feature_incs="/usr/local/include/libxml2 /usr/local/include"
+        njs_feature_libs="-L/usr/local/lib -lxml2"
+
+        . auto/feature
+    fi
+
+    if [ $njs_found = no ]; then
+
+        # NetBSD port
+
+        njs_feature="libxml2 in /usr/pkg/"
+        njs_feature_incs="/usr/pkg/include/libxml2 /usr/pkg/include"
+        njs_feature_libs="-L/usr/pkg/lib -lxml2"
+
+        . auto/feature
+    fi
+
+    if [ $njs_found = no ]; then
+
+        # MacPorts
+
+        njs_feature="libxml2 in /opt/local/"
+        njs_feature_incs="/opt/local/include/libxml2 /opt/local/include"
+        njs_feature_libs="-L/opt/local/lib -lxml2 -lxslt"
+
+        . auto/feature
+    fi
+
+    if [ $njs_found = yes ]; then
+        njs_feature="libxml2 version"
+        njs_feature_name=NJS_LIBXML2_VERSION
+        njs_feature_run=value
+        njs_feature_test="#include <libxml/xmlversion.h>
+                          #include <stdio.h>
+
+                          int main() {
+                              printf(\"\\\"%s\\\"\", LIBXML_DOTTED_VERSION);
+                              return 0;
+                          }"
+        . auto/feature
+
+        NJS_HAVE_LIBXML2=YES
+        NJS_LIB_INCS="$NJS_LIB_INCS $njs_feature_incs"
+        NJS_LIB_AUX_LIBS="$NJS_LIB_AUX_LIBS $njs_feature_libs"
+    fi
+fi
index 2bf04c7ebad885185221c620697ee977e98ddd70..6307e14f24728a88266ad08dbadd681bb4c6a4c4 100644 (file)
@@ -21,6 +21,14 @@ if [ $NJS_OPENSSL = YES -a $NJS_HAVE_OPENSSL = YES ]; then
        . auto/module
 fi
 
+if [ $NJS_LIBXML2 = YES -a $NJS_HAVE_LIBXML2 = YES ]; then
+       njs_module_name=njs_xml_module
+       njs_module_incs=
+       njs_module_srcs=external/njs_xml_module.c
+
+       . auto/module
+fi
+
 njs_module_name=njs_fs_module
 njs_module_incs=
 njs_module_srcs=external/njs_fs_module.c
index 3f6ae6501bc431dea3552c121372b92fe36871bd..9e155e8cab4457dfffd288dbd4826744888b49e2 100644 (file)
@@ -16,6 +16,7 @@ NJS_ADDR2LINE=NO
 NJS_TEST262=YES
 
 NJS_OPENSSL=YES
+NJS_LIBXML2=YES
 
 NJS_PCRE=YES
 NJS_TRY_PCRE2=YES
@@ -48,6 +49,7 @@ do
         --test262=*)                     NJS_TEST262="$value"                ;;
 
         --no-openssl)                    NJS_OPENSSL=NO                      ;;
+        --no-libxml2)                    NJS_LIBXML2=NO                      ;;
 
         --no-pcre)                       NJS_PCRE=NO                         ;;
         --no-pcre2)                      NJS_TRY_PCRE2=NO                    ;;
index 8fc71f336a4655ba2fef14af7e661fbc331ce002..1a6b653c96caafb87070a28cea53c19c9d60372d 100755 (executable)
--- a/configure
+++ b/configure
@@ -51,6 +51,7 @@ NJS_LIB_AUX_LIBS=
 . auto/pcre
 . auto/readline
 . auto/openssl
+. auto/libxml2
 . auto/libbfd
 . auto/link
 
diff --git a/external/njs_xml_module.c b/external/njs_xml_module.c
new file mode 100644 (file)
index 0000000..a8c8a1d
--- /dev/null
@@ -0,0 +1,1322 @@
+
+/*
+ * Copyright (C) Dmitry Volyntsev
+ * Copyright (C) NGINX, Inc.
+ */
+
+
+#include <njs.h>
+#include <string.h>
+#include <libxml/parser.h>
+#include <libxml/tree.h>
+#include <libxml/c14n.h>
+#include <libxml/xpathInternals.h>
+
+
+typedef struct {
+    xmlDoc         *doc;
+    xmlParserCtxt  *ctx;
+} njs_xml_doc_t;
+
+
+typedef enum {
+    XML_NSET_TREE = 0,
+    XML_NSET_TREE_NO_COMMENTS,
+    XML_NSET_TREE_INVERT,
+} njs_xml_nset_type_t;
+
+
+typedef struct njs_xml_nset_s  njs_xml_nset_t;
+
+struct njs_xml_nset_s {
+    xmlNodeSet           *nodes;
+    xmlDoc               *doc;
+    njs_xml_nset_type_t  type;
+    njs_xml_nset_t       *next;
+    njs_xml_nset_t       *prev;
+};
+
+static njs_int_t njs_xml_ext_parse(njs_vm_t *vm, njs_value_t *args,
+    njs_uint_t nargs, njs_index_t unused);
+static njs_int_t njs_xml_ext_canonicalization(njs_vm_t *vm, njs_value_t *args,
+    njs_uint_t nargs, njs_index_t unused);
+static njs_int_t njs_xml_doc_ext_prop_keys(njs_vm_t *vm, njs_value_t *value,
+    njs_value_t *keys);
+static njs_int_t njs_xml_doc_ext_root(njs_vm_t *vm, njs_object_prop_t *prop,
+     njs_value_t *value, njs_value_t *unused, njs_value_t *retval);
+static void njs_xml_doc_cleanup(void *data);
+static njs_int_t njs_xml_node_ext_prop_keys(njs_vm_t *vm, njs_value_t *value,
+    njs_value_t *keys);
+static njs_int_t njs_xml_node_ext_prop_handler(njs_vm_t *vm,
+    njs_object_prop_t *prop, njs_value_t *value, njs_value_t *unused,
+    njs_value_t *retval);
+static njs_int_t njs_xml_attr_ext_prop_keys(njs_vm_t *vm, njs_value_t *value,
+    njs_value_t *keys);
+static njs_int_t njs_xml_attr_ext_prop_handler(njs_vm_t *vm,
+    njs_object_prop_t *prop, njs_value_t *value, njs_value_t *unused,
+    njs_value_t *retval);
+static njs_int_t njs_xml_node_ext_attrs(njs_vm_t *vm, njs_object_prop_t *prop,
+    njs_value_t *value, njs_value_t *setval, njs_value_t *retval);
+static njs_int_t njs_xml_node_ext_name(njs_vm_t *vm, njs_object_prop_t *prop,
+    njs_value_t *value, njs_value_t *setval, njs_value_t *retval);
+static njs_int_t njs_xml_node_ext_ns(njs_vm_t *vm, njs_object_prop_t *prop,
+    njs_value_t *value, njs_value_t *setval, njs_value_t *retval);
+static njs_int_t njs_xml_node_ext_parent(njs_vm_t *vm, njs_object_prop_t *prop,
+     njs_value_t *value, njs_value_t *setval, njs_value_t *retval);
+static njs_int_t njs_xml_node_ext_tags(njs_vm_t *vm, njs_object_prop_t *prop,
+     njs_value_t *value, njs_value_t *setval, njs_value_t *retval);
+static njs_int_t njs_xml_node_ext_text(njs_vm_t *vm, njs_object_prop_t *prop,
+    njs_value_t *value, njs_value_t *setval, njs_value_t *retval);
+
+static njs_xml_nset_t *njs_xml_nset_create(njs_vm_t *vm, xmlDoc *doc,
+    xmlNodeSet *nodes, njs_xml_nset_type_t type);
+static njs_xml_nset_t *njs_xml_nset_children(njs_vm_t *vm, xmlNode *parent);
+static njs_xml_nset_t *njs_xml_nset_add(njs_xml_nset_t *nset,
+    njs_xml_nset_t *add);
+static void njs_xml_nset_cleanup(void *data);
+static void njs_xml_error(njs_vm_t *vm, njs_xml_doc_t *tree, const char *fmt,
+    ...);
+static njs_int_t njs_xml_init(njs_vm_t *vm);
+
+
+static njs_external_t  njs_ext_xml[] = {
+
+    {
+        .flags = NJS_EXTERN_PROPERTY | NJS_EXTERN_SYMBOL,
+        .name.symbol = NJS_SYMBOL_TO_STRING_TAG,
+        .u.property = {
+            .value = "xml",
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("parse"),
+        .writable = 1,
+        .configurable = 1,
+        .u.method = {
+            .native = njs_xml_ext_parse,
+            .magic8 = 0,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("c14n"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = njs_xml_ext_canonicalization,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("exclusiveC14n"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = njs_xml_ext_canonicalization,
+            .magic8 = 1,
+        }
+    },
+
+};
+
+
+static njs_external_t  njs_ext_xml_doc[] = {
+
+    {
+        .flags = NJS_EXTERN_PROPERTY | NJS_EXTERN_SYMBOL,
+        .name.symbol = NJS_SYMBOL_TO_STRING_TAG,
+        .u.property = {
+            .value = "XMLDoc",
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_SELF,
+        .u.object = {
+            .enumerable = 1,
+            .prop_handler = njs_xml_doc_ext_root,
+            .keys = njs_xml_doc_ext_prop_keys,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_PROPERTY,
+        .name.string = njs_str("$root"),
+        .enumerable = 1,
+        .u.property = {
+            .handler = njs_xml_doc_ext_root,
+            .magic32 = 1,
+        }
+    },
+
+};
+
+
+static njs_external_t  njs_ext_xml_node[] = {
+
+    {
+        .flags = NJS_EXTERN_PROPERTY | NJS_EXTERN_SYMBOL,
+        .name.symbol = NJS_SYMBOL_TO_STRING_TAG,
+        .u.property = {
+            .value = "XMLNode",
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_SELF,
+        .u.object = {
+            .enumerable = 1,
+            .prop_handler = njs_xml_node_ext_prop_handler,
+            .keys = njs_xml_node_ext_prop_keys,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_PROPERTY,
+        .name.string = njs_str("$attrs"),
+        .enumerable = 1,
+        .u.property = {
+            .handler = njs_xml_node_ext_attrs,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_PROPERTY,
+        .name.string = njs_str("$name"),
+        .enumerable = 1,
+        .u.property = {
+            .handler = njs_xml_node_ext_name,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_PROPERTY,
+        .name.string = njs_str("$ns"),
+        .enumerable = 1,
+        .u.property = {
+            .handler = njs_xml_node_ext_ns,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_PROPERTY,
+        .name.string = njs_str("$parent"),
+        .enumerable = 1,
+        .u.property = {
+            .handler = njs_xml_node_ext_parent,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_PROPERTY,
+        .name.string = njs_str("$tags"),
+        .enumerable = 1,
+        .u.property = {
+            .handler = njs_xml_node_ext_tags,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_PROPERTY,
+        .name.string = njs_str("$text"),
+        .enumerable = 1,
+        .u.property = {
+            .handler = njs_xml_node_ext_text,
+        }
+    },
+
+};
+
+
+static njs_external_t  njs_ext_xml_attr[] = {
+
+    {
+        .flags = NJS_EXTERN_PROPERTY | NJS_EXTERN_SYMBOL,
+        .name.symbol = NJS_SYMBOL_TO_STRING_TAG,
+        .u.property = {
+            .value = "XMLAttr",
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_SELF,
+        .u.object = {
+            .enumerable = 1,
+            .prop_handler = njs_xml_attr_ext_prop_handler,
+            .keys = njs_xml_attr_ext_prop_keys,
+        }
+    },
+
+};
+
+
+njs_module_t  njs_xml_module = {
+    .name = njs_str("xml"),
+    .init = njs_xml_init,
+};
+
+
+static njs_int_t    njs_xml_doc_proto_id;
+static njs_int_t    njs_xml_node_proto_id;
+static njs_int_t    njs_xml_attr_proto_id;
+
+
+static njs_int_t
+njs_xml_ext_parse(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
+    njs_index_t unused)
+{
+    njs_int_t         ret;
+    njs_str_t         data;
+    njs_xml_doc_t     *tree;
+    njs_mp_cleanup_t  *cln;
+
+    ret = njs_vm_value_to_bytes(vm, &data, njs_arg(args, nargs, 1));
+    if (njs_slow_path(ret != NJS_OK)) {
+        return NJS_ERROR;
+    }
+
+    tree = njs_mp_zalloc(njs_vm_memory_pool(vm), sizeof(njs_xml_doc_t));
+    if (njs_slow_path(tree == NULL)) {
+        njs_vm_memory_error(vm);
+        return NJS_ERROR;
+    }
+
+    tree->ctx = xmlNewParserCtxt();
+    if (njs_slow_path(tree->ctx == NULL)) {
+        njs_vm_error(vm, "xmlNewParserCtxt() failed");
+        return NJS_ERROR;
+    }
+
+    tree->doc = xmlCtxtReadMemory(tree->ctx, (char *) data.start, data.length,
+                                  NULL, NULL, XML_PARSE_DTDVALID
+                                              | XML_PARSE_NOWARNING
+                                              | XML_PARSE_NOERROR);
+    if (njs_slow_path(tree->doc == NULL)) {
+        njs_xml_error(vm, tree, "failed to parse XML");
+        return NJS_ERROR;
+    }
+
+    cln = njs_mp_cleanup_add(njs_vm_memory_pool(vm), 0);
+    if (njs_slow_path(cln == NULL)) {
+        njs_vm_memory_error(vm);
+        return NJS_ERROR;
+    }
+
+    cln->handler = njs_xml_doc_cleanup;
+    cln->data = tree;
+
+    return njs_vm_external_create(vm, njs_vm_retval(vm), njs_xml_doc_proto_id,
+                                  tree, 0);
+}
+
+
+static njs_int_t
+njs_xml_doc_ext_prop_keys(njs_vm_t *vm, njs_value_t *value, njs_value_t *keys)
+{
+    xmlNode        *node;
+    njs_int_t      ret;
+    njs_value_t    *push;
+    njs_xml_doc_t  *tree;
+
+    tree = njs_vm_external(vm, njs_xml_doc_proto_id, value);
+    if (njs_slow_path(tree == NULL)) {
+        njs_value_undefined_set(keys);
+        return NJS_DECLINED;
+    }
+
+    ret = njs_vm_array_alloc(vm, keys, 2);
+    if (njs_slow_path(ret != NJS_OK)) {
+        return NJS_ERROR;
+    }
+
+    for (node = xmlDocGetRootElement(tree->doc);
+         node != NULL;
+         node = node->next)
+    {
+        if (node->type != XML_ELEMENT_NODE) {
+            continue;
+        }
+
+        push = njs_vm_array_push(vm, keys);
+        if (njs_slow_path(push == NULL)) {
+            return NJS_ERROR;
+        }
+
+        ret = njs_vm_value_string_create(vm, push, node->name,
+                                         njs_strlen(node->name));
+        if (njs_slow_path(ret != NJS_OK)) {
+            return NJS_ERROR;
+        }
+    }
+
+    return NJS_OK;
+}
+
+
+static njs_int_t
+njs_xml_doc_ext_root(njs_vm_t *vm, njs_object_prop_t *prop, njs_value_t *value,
+     njs_value_t *unused, njs_value_t *retval)
+{
+    xmlNode        *node;
+    njs_int_t      ret;
+    njs_str_t      name;
+    njs_bool_t     any;
+    njs_xml_doc_t  *tree;
+
+    tree = njs_vm_external(vm, njs_xml_doc_proto_id, value);
+    if (njs_slow_path(tree == NULL)) {
+        njs_value_undefined_set(retval);
+        return NJS_DECLINED;
+    }
+
+    any = njs_vm_prop_magic32(prop);
+
+    if (!any) {
+        ret = njs_vm_prop_name(vm, prop, &name);
+        if (njs_slow_path(ret != NJS_OK)) {
+            njs_value_undefined_set(retval);
+            return NJS_DECLINED;
+        }
+    }
+
+    for (node = xmlDocGetRootElement(tree->doc);
+         node != NULL;
+         node = node->next)
+    {
+        if (node->type != XML_ELEMENT_NODE) {
+            continue;
+        }
+
+        if (!any) {
+            if (name.length != njs_strlen(node->name)
+                || njs_strncmp(name.start, node->name, name.length) != 0)
+            {
+                continue;
+            }
+        }
+
+        return njs_vm_external_create(vm, retval, njs_xml_node_proto_id, node,
+                                      0);
+    }
+
+    njs_value_undefined_set(retval);
+
+    return NJS_DECLINED;
+}
+
+
+static void
+njs_xml_doc_cleanup(void *data)
+{
+    njs_xml_doc_t  *current = data;
+
+    xmlFreeDoc(current->doc);
+    xmlFreeParserCtxt(current->ctx);
+}
+
+
+static njs_int_t
+njs_xml_node_ext_prop_keys(njs_vm_t *vm, njs_value_t *value, njs_value_t *keys)
+{
+    xmlNode      *node, *current;
+    njs_int_t    ret;
+    njs_value_t  *push;
+
+    current = njs_vm_external(vm, njs_xml_node_proto_id, value);
+    if (njs_slow_path(current == NULL)) {
+        njs_value_undefined_set(keys);
+        return NJS_DECLINED;
+    }
+
+    ret = njs_vm_array_alloc(vm, keys, 2);
+    if (njs_slow_path(ret != NJS_OK)) {
+        return NJS_ERROR;
+    }
+
+    if (current->name != NULL && current->type == XML_ELEMENT_NODE) {
+        push = njs_vm_array_push(vm, keys);
+        if (njs_slow_path(push == NULL)) {
+            return NJS_ERROR;
+        }
+
+        ret = njs_vm_value_string_set(vm, push, (u_char *) "$name",
+                                      njs_length("$name"));
+        if (njs_slow_path(ret != NJS_OK)) {
+            return NJS_ERROR;
+        }
+    }
+
+    if (current->ns != NULL) {
+        push = njs_vm_array_push(vm, keys);
+        if (njs_slow_path(push == NULL)) {
+            return NJS_ERROR;
+        }
+
+        ret = njs_vm_value_string_set(vm, push, (u_char *) "$ns",
+                                      njs_length("$ns"));
+        if (njs_slow_path(ret != NJS_OK)) {
+            return NJS_ERROR;
+        }
+    }
+
+    if (current->properties != NULL) {
+        push = njs_vm_array_push(vm, keys);
+        if (njs_slow_path(push == NULL)) {
+            return NJS_ERROR;
+        }
+
+        ret = njs_vm_value_string_set(vm, push, (u_char *) "$attrs",
+                                      njs_length("$attrs"));
+        if (njs_slow_path(ret != NJS_OK)) {
+            return NJS_ERROR;
+        }
+    }
+
+    if (current->children != NULL && current->children->content != NULL) {
+        push = njs_vm_array_push(vm, keys);
+        if (njs_slow_path(push == NULL)) {
+            return NJS_ERROR;
+        }
+
+        ret = njs_vm_value_string_set(vm, push, (u_char *) "$text",
+                                      njs_length("$text"));
+        if (njs_slow_path(ret != NJS_OK)) {
+            return NJS_ERROR;
+        }
+    }
+
+    for (node = current->children; node != NULL; node = node->next) {
+        if (node->type != XML_ELEMENT_NODE) {
+            continue;
+        }
+
+        push = njs_vm_array_push(vm, keys);
+        if (njs_slow_path(push == NULL)) {
+            return NJS_ERROR;
+        }
+
+        ret = njs_vm_value_string_set(vm, push, (u_char *) "$tags",
+                                      njs_length("$tags"));
+        if (njs_slow_path(ret != NJS_OK)) {
+            return NJS_ERROR;
+        }
+
+        break;
+    }
+
+    return NJS_OK;
+}
+
+
+static njs_int_t
+njs_xml_node_ext_prop_handler(njs_vm_t *vm, njs_object_prop_t *prop,
+    njs_value_t *value, njs_value_t *unused, njs_value_t *retval)
+{
+    size_t        size;
+    xmlAttr       *attr;
+    xmlNode       *node, *current;
+    njs_int_t     ret;
+    njs_str_t     name;
+    njs_value_t   *push;
+    const u_char  *content;
+
+    /*
+     * $tag$foo - the first tag child with the name "foo"
+     * $tags$foo - the all children with the name "foo" as an array
+     * $attr$foo - the attribute with the name "foo"
+     * foo - the same as $tag$foo
+     */
+
+    current = njs_vm_external(vm, njs_xml_node_proto_id, value);
+    if (njs_slow_path(current == NULL)) {
+        njs_value_undefined_set(retval);
+        return NJS_DECLINED;
+    }
+
+    ret = njs_vm_prop_name(vm, prop, &name);
+    if (njs_slow_path(ret != NJS_OK)) {
+        njs_value_undefined_set(retval);
+        return NJS_DECLINED;
+    }
+
+    if (name.length > 1 && name.start[0] == '$') {
+        if (name.length > njs_length("$attr$")
+            && njs_strncmp(&name.start[1], "attr$", njs_length("attr$")) == 0)
+        {
+            for (attr = current->properties; attr != NULL; attr = attr->next) {
+                if (attr->type != XML_ATTRIBUTE_NODE) {
+                    continue;
+                }
+
+                size = njs_strlen(attr->name);
+
+                if (name.length != (size + njs_length("$attr$"))
+                    || njs_strncmp(&name.start[njs_length("$attr$")],
+                                   attr->name, size) != 0)
+                {
+                    continue;
+                }
+
+                content = (const u_char *) attr->children->content;
+
+                return njs_vm_value_string_create(vm, retval, content,
+                                                  njs_strlen(content));
+            }
+        }
+
+        if (name.length > njs_length("$tag$")
+            && njs_strncmp(&name.start[1], "tag$", njs_length("tag$")) == 0)
+        {
+            for (node = current->children; node != NULL; node = node->next) {
+                if (node->type != XML_ELEMENT_NODE) {
+                    continue;
+                }
+
+                size = njs_strlen(node->name);
+
+                if (name.length != (size + njs_length("$tag$"))
+                    || njs_strncmp(&name.start[njs_length("$tag$")],
+                                   node->name, size) != 0)
+                {
+                    continue;
+                }
+
+                return njs_vm_external_create(vm, retval, njs_xml_node_proto_id,
+                                              node, 0);
+            }
+        }
+
+        if (name.length >= njs_length("$tags$")
+            && njs_strncmp(&name.start[1], "tags$", njs_length("tags$")) == 0)
+        {
+            ret = njs_vm_array_alloc(vm, retval, 2);
+            if (njs_slow_path(ret != NJS_OK)) {
+                return NJS_ERROR;
+            }
+
+            for (node = current->children; node != NULL; node = node->next) {
+                if (node->type != XML_ELEMENT_NODE) {
+                    continue;
+                }
+
+                size = njs_strlen(node->name);
+
+                if (name.length > njs_length("$tags$")
+                    && (name.length != (size + njs_length("$tags$"))
+                        || njs_strncmp(&name.start[njs_length("$tags$")],
+                                       node->name, size) != 0))
+                {
+                    continue;
+                }
+
+                push = njs_vm_array_push(vm, retval);
+                if (njs_slow_path(push == NULL)) {
+                    return NJS_ERROR;
+                }
+
+                ret = njs_vm_external_create(vm, push, njs_xml_node_proto_id,
+                                             node, 0);
+                if (njs_slow_path(ret != NJS_OK)) {
+                    return NJS_ERROR;
+                }
+            }
+
+            return NJS_OK;
+        }
+    }
+
+    for (node = current->children; node != NULL; node = node->next) {
+        if (node->type != XML_ELEMENT_NODE) {
+            continue;
+        }
+
+        size = njs_strlen(node->name);
+
+        if (name.length != size
+            || njs_strncmp(name.start, node->name, size) != 0)
+        {
+            continue;
+        }
+
+        return njs_vm_external_create(vm, retval, njs_xml_node_proto_id,
+                                      node, 0);
+    }
+
+    njs_value_undefined_set(retval);
+
+    return NJS_DECLINED;
+}
+
+
+static njs_int_t
+njs_xml_node_ext_attrs(njs_vm_t *vm, njs_object_prop_t *prop,
+    njs_value_t *value, njs_value_t *setval, njs_value_t *retval)
+{
+    xmlNode  *current;
+
+    current = njs_vm_external(vm, njs_xml_node_proto_id, value);
+    if (njs_slow_path(current == NULL || current->properties == NULL)) {
+        njs_value_undefined_set(retval);
+        return NJS_DECLINED;
+    }
+
+    return njs_vm_external_create(vm, retval, njs_xml_attr_proto_id,
+                                  current->properties, 0);
+}
+
+
+static njs_int_t
+njs_xml_node_ext_name(njs_vm_t *vm, njs_object_prop_t *prop, njs_value_t *value,
+     njs_value_t *setval, njs_value_t *retval)
+{
+    xmlNode  *current;
+
+    current = njs_vm_external(vm, njs_xml_node_proto_id, value);
+    if (current == NULL || current->type != XML_ELEMENT_NODE) {
+        njs_value_undefined_set(retval);
+        return NJS_DECLINED;
+    }
+
+    return njs_vm_value_string_create(vm, retval, current->name,
+                                      njs_strlen(current->name));
+}
+
+
+static njs_int_t
+njs_xml_node_ext_ns(njs_vm_t *vm, njs_object_prop_t *prop,
+    njs_value_t *value, njs_value_t *setval, njs_value_t *retval)
+{
+    xmlNode  *current;
+
+    current = njs_vm_external(vm, njs_xml_node_proto_id, value);
+    if (njs_slow_path(current == NULL || current->ns == NULL)) {
+        njs_value_undefined_set(retval);
+        return NJS_DECLINED;
+    }
+
+    return njs_vm_value_string_create(vm, retval, current->ns->href,
+                                      njs_strlen(current->ns->href));
+}
+
+
+static njs_int_t
+njs_xml_node_ext_parent(njs_vm_t *vm, njs_object_prop_t *prop,
+    njs_value_t *value, njs_value_t *setval, njs_value_t *retval)
+{
+    xmlNode  *current;
+
+    current = njs_vm_external(vm, njs_xml_node_proto_id, value);
+    if (njs_slow_path(current == NULL
+                      || current->parent == NULL
+                      || current->parent->type != XML_ELEMENT_NODE))
+    {
+        njs_value_undefined_set(retval);
+        return NJS_DECLINED;
+    }
+
+    return njs_vm_external_create(vm, retval, njs_xml_node_proto_id,
+                                  current->parent, 0);
+}
+
+
+static njs_int_t
+njs_xml_node_ext_tags(njs_vm_t *vm, njs_object_prop_t *prop, njs_value_t *value,
+     njs_value_t *setval, njs_value_t *retval)
+{
+    xmlNode      *node, *current;
+    njs_int_t    ret;
+    njs_value_t  *push;
+
+    current = njs_vm_external(vm, njs_xml_node_proto_id, value);
+    if (njs_slow_path(current == NULL || current->children == NULL)) {
+        njs_value_undefined_set(retval);
+        return NJS_DECLINED;
+    }
+
+    ret = njs_vm_array_alloc(vm, retval, 2);
+    if (njs_slow_path(ret != NJS_OK)) {
+        return NJS_ERROR;
+    }
+
+    for (node = current->children; node != NULL; node = node->next) {
+        if (node->type != XML_ELEMENT_NODE) {
+            continue;
+        }
+
+        push = njs_vm_array_push(vm, retval);
+        if (njs_slow_path(push == NULL)) {
+            return NJS_ERROR;
+        }
+
+        ret = njs_vm_external_create(vm, push, njs_xml_node_proto_id, node, 0);
+        if (njs_slow_path(ret != NJS_OK)) {
+            return NJS_ERROR;
+        }
+    }
+
+    return NJS_OK;
+}
+
+
+static njs_int_t
+njs_xml_node_ext_text(njs_vm_t *vm, njs_object_prop_t *prop, njs_value_t *value,
+     njs_value_t *setval, njs_value_t *retval)
+{
+    xmlNode    *current, *node;
+    njs_int_t  ret;
+    njs_chb_t  chain;
+
+    current = njs_vm_external(vm, njs_xml_node_proto_id, value);
+    if (njs_slow_path(current == NULL)) {
+        njs_value_undefined_set(retval);
+        return NJS_DECLINED;
+    }
+
+    njs_chb_init(&chain, njs_vm_memory_pool(vm));
+
+    for (node = current->children; node != NULL; node = node->next) {
+        if (node->type != XML_TEXT_NODE) {
+            continue;
+        }
+
+        njs_chb_append(&chain, node->content, njs_strlen(node->content));
+    }
+
+    ret = njs_vm_value_string_create_chb(vm, retval, &chain);
+
+    njs_chb_destroy(&chain);
+
+    return ret;
+}
+
+
+static int
+njs_xml_buf_write_cb(void *context, const char *buffer, int len)
+{
+    njs_chb_t  *chain = context;
+
+    njs_chb_append(chain, buffer, len);
+
+    return chain->error ? -1 : len;
+}
+
+
+static int
+njs_xml_node_one_contains(njs_xml_nset_t *nset, xmlNode *node, xmlNode *parent)
+{
+    int    in;
+    xmlNs  ns;
+
+    if (nset->type == XML_NSET_TREE_NO_COMMENTS
+        && node->type == XML_COMMENT_NODE)
+    {
+        return 0;
+    }
+
+    in = 1;
+
+    if (nset->nodes != NULL) {
+        if (node->type != XML_NAMESPACE_DECL) {
+            in = xmlXPathNodeSetContains(nset->nodes, node);
+
+        } else {
+
+            memcpy(&ns, node, sizeof(ns));
+
+            /* libxml2 workaround, check xpath.c for details */
+
+            if ((parent != NULL) && (parent->type == XML_ATTRIBUTE_NODE)) {
+                ns.next = (xmlNs *) parent->parent;
+
+            } else {
+                ns.next = (xmlNs *) parent;
+            }
+
+            in = xmlXPathNodeSetContains(nset->nodes, (xmlNode *) &ns);
+        }
+    }
+
+    switch (nset->type) {
+    case XML_NSET_TREE:
+    case XML_NSET_TREE_NO_COMMENTS:
+        if (in != 0) {
+            return 1;
+        }
+
+        if ((parent != NULL) && (parent->type == XML_ELEMENT_NODE)) {
+            return njs_xml_node_one_contains(nset, parent, parent->parent);
+        }
+
+        return 0;
+
+    case XML_NSET_TREE_INVERT:
+    default:
+        if (in != 0) {
+            return 0;
+        }
+
+        if ((parent != NULL) && (parent->type == XML_ELEMENT_NODE)) {
+            return njs_xml_node_one_contains(nset, parent, parent->parent);
+        }
+    }
+
+    return 1;
+}
+
+
+static int
+njs_xml_c14n_visibility_cb(void *user_data, xmlNode *node, xmlNode *parent)
+{
+    int             status;
+    njs_xml_nset_t  *n, *nset;
+
+    nset = user_data;
+
+    if (nset == NULL) {
+        return 1;
+    }
+
+    status = 1;
+
+    n = nset;
+
+    do {
+        if (status && !njs_xml_node_one_contains(n, node, parent)) {
+            status = 0;
+        }
+
+        n = n->next;
+    } while (n != nset);
+
+    return status;
+}
+
+
+static u_char **
+njs_xml_parse_ns_list(njs_vm_t *vm, njs_str_t *src)
+{
+    u_char    *p, **buf, **n, **out;
+    size_t  size, idx;
+
+    out = NULL;
+
+    p =  njs_mp_alloc(njs_vm_memory_pool(vm), src->length + 1);
+    if (njs_slow_path(p == NULL)) {
+        njs_vm_memory_error(vm);
+        return NULL;
+    }
+
+    memcpy(p, src->start, src->length);
+    p[src->length] = '\0';
+
+    size = 8;
+
+    buf = njs_mp_alloc(njs_vm_memory_pool(vm), size * sizeof(char *));
+    if (njs_slow_path(buf == NULL)) {
+        njs_vm_memory_error(vm);
+        return NULL;
+    }
+
+    out = buf;
+
+    while (*p != '\0') {
+        idx = out - buf;
+
+        if (idx >= size) {
+            size *= 2;
+
+            n = njs_mp_alloc(njs_vm_memory_pool(vm), size * sizeof(char *));
+            if (njs_slow_path(buf == NULL)) {
+                njs_vm_memory_error(vm);
+                return NULL;
+            }
+
+            memcpy(n, buf, size * sizeof(char *) / 2);
+            buf = n;
+
+            out = &buf[idx];
+        }
+
+        *out++ = p;
+
+        while (*p != ' ' && *p != '\0') {
+            p++;
+        }
+
+        if (*p == ' ') {
+            *p++ = '\0';
+        }
+    }
+
+    *out = NULL;
+
+    return buf;
+}
+
+
+static njs_int_t
+njs_xml_ext_canonicalization(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
+    njs_index_t exclusive)
+{
+    u_char           **prefix_list;
+    ssize_t          size;
+    xmlNode          *node, *current;
+    njs_int_t        ret;
+    njs_str_t        data, string;
+    njs_chb_t        chain;
+    njs_bool_t       comments;
+    xmlNodeSet       *nodes;
+    njs_value_t      *excluding, *prefixes;
+    njs_xml_doc_t    *tree;
+    njs_xml_nset_t   *nset, *children;
+    xmlOutputBuffer  *buf;
+
+    current = njs_vm_external(vm, njs_xml_node_proto_id, njs_argument(args, 1));
+    if (njs_slow_path(current == NULL)) {
+        tree = njs_vm_external(vm, njs_xml_doc_proto_id, njs_argument(args, 1));
+        if (njs_slow_path(tree == NULL)) {
+            njs_vm_error(vm, "\"this\" is not a XMLNode object");
+            return NJS_ERROR;
+        }
+
+        current = xmlDocGetRootElement(tree->doc);
+        if (njs_slow_path(current == NULL)) {
+            njs_vm_error(vm, "\"this\" is not a XMLNode object");
+            return NJS_ERROR;
+        }
+    }
+
+    njs_chb_init(&chain, njs_vm_memory_pool(vm));
+
+    buf = xmlOutputBufferCreateIO(njs_xml_buf_write_cb, NULL, &chain, NULL);
+    if (njs_slow_path(buf == NULL)) {
+        njs_vm_error(vm, "xmlOutputBufferCreateIO() failed");
+        goto error;
+    }
+
+    comments = njs_value_bool(njs_arg(args, nargs, 3));
+
+    nodes = xmlXPathNodeSetCreate(current);
+    if (njs_slow_path(nodes == NULL)) {
+        njs_vm_memory_pool(vm);
+        goto error;
+    }
+
+    excluding = njs_arg(args, nargs, 2);
+
+    if (!njs_value_is_null_or_undefined(excluding)) {
+        node = njs_vm_external(vm, njs_xml_node_proto_id, excluding);
+        if (njs_slow_path(node == NULL)) {
+            njs_vm_error(vm, "\"excluding\" argument is not a XMLNode object");
+            goto error;
+        }
+
+        nset = njs_xml_nset_create(vm, current->doc, nodes,
+                                   XML_NSET_TREE_NO_COMMENTS);
+        if (njs_slow_path(nset == NULL)) {
+            goto error;
+        }
+
+        children = njs_xml_nset_children(vm, node);
+        if (njs_slow_path(children == NULL)) {
+            goto error;
+        }
+
+        nset = njs_xml_nset_add(nset, children);
+
+    } else {
+        nset = njs_xml_nset_create(vm, current->doc, nodes,
+                                   comments ? XML_NSET_TREE
+                                            : XML_NSET_TREE_NO_COMMENTS);
+        if (njs_slow_path(nset == NULL)) {
+            goto error;
+        }
+    }
+
+    prefix_list = NULL;
+    prefixes = njs_arg(args, nargs, 4);
+
+    if (!njs_value_is_null_or_undefined(prefixes)) {
+        if (!njs_value_is_string(prefixes)) {
+            njs_vm_error(vm, "\"prefixes\" argument is not a string");
+            goto error;
+        }
+
+        ret = njs_vm_value_string(vm, &string, prefixes);
+        if (njs_slow_path(ret != NJS_OK)) {
+            goto error;
+        }
+
+        prefix_list = njs_xml_parse_ns_list(vm, &string);
+        if (njs_slow_path(prefix_list == NULL)) {
+            goto error;
+        }
+    }
+
+    ret = xmlC14NExecute(current->doc, njs_xml_c14n_visibility_cb, nset,
+                         exclusive ? XML_C14N_EXCLUSIVE_1_0 : XML_C14N_1_0,
+                         prefix_list, comments, buf);
+
+    (void) xmlOutputBufferClose(buf);
+
+    if (njs_slow_path(ret < 0)) {
+        njs_vm_error(vm, "xmlC14NExecute() failed");
+        goto error;
+    }
+
+    size = njs_chb_size(&chain);
+    if (njs_slow_path(size < 0)) {
+        njs_vm_memory_error(vm);
+        goto error;
+    }
+
+    ret = njs_chb_join(&chain, &data);
+    if (njs_slow_path(ret != NJS_OK)) {
+        goto error;
+    }
+
+    njs_chb_destroy(&chain);
+
+    return njs_vm_value_buffer_set(vm, njs_vm_retval(vm), data.start,
+                                   data.length);
+
+error:
+
+    njs_chb_destroy(&chain);
+
+    return NJS_ERROR;
+}
+
+
+static njs_int_t
+njs_xml_attr_ext_prop_keys(njs_vm_t *vm, njs_value_t *value, njs_value_t *keys)
+{
+    xmlAttr      *node, *current;
+    njs_int_t    ret;
+    njs_value_t  *push;
+
+    current = njs_vm_external(vm, njs_xml_attr_proto_id, value);
+    if (njs_slow_path(current == NULL)) {
+        njs_value_undefined_set(keys);
+        return NJS_DECLINED;
+    }
+
+    ret = njs_vm_array_alloc(vm, keys, 2);
+    if (njs_slow_path(ret != NJS_OK)) {
+        return NJS_ERROR;
+    }
+
+    for (node = current; node != NULL; node = node->next) {
+        if (node->type != XML_ATTRIBUTE_NODE) {
+            continue;
+        }
+
+        push = njs_vm_array_push(vm, keys);
+        if (njs_slow_path(push == NULL)) {
+            return NJS_ERROR;
+        }
+
+        ret = njs_vm_value_string_create(vm, push, node->name,
+                                         njs_strlen(node->name));
+        if (njs_slow_path(ret != NJS_OK)) {
+            return NJS_ERROR;
+        }
+    }
+
+    return NJS_OK;
+}
+
+
+static njs_int_t
+njs_xml_attr_ext_prop_handler(njs_vm_t *vm, njs_object_prop_t *prop,
+    njs_value_t *value, njs_value_t *unused, njs_value_t *retval)
+{
+    size_t     size;
+    xmlAttr    *node, *current;
+    njs_int_t  ret;
+    njs_str_t  name;
+
+    current = njs_vm_external(vm, njs_xml_attr_proto_id, value);
+    if (njs_slow_path(current == NULL)) {
+        njs_value_undefined_set(retval);
+        return NJS_DECLINED;
+    }
+
+    ret = njs_vm_prop_name(vm, prop, &name);
+    if (njs_slow_path(ret != NJS_OK)) {
+        njs_value_undefined_set(retval);
+        return NJS_DECLINED;
+    }
+
+    for (node = current; node != NULL; node = node->next) {
+        if (node->type != XML_ATTRIBUTE_NODE) {
+            continue;
+        }
+
+        size = njs_strlen(node->name);
+
+        if (name.length != size
+            || njs_strncmp(name.start, node->name, size) != 0)
+        {
+            continue;
+        }
+
+        return njs_vm_value_string_create(vm, retval, node->children->content,
+                                          njs_strlen(node->children->content));
+    }
+
+    return NJS_OK;
+}
+
+
+static njs_xml_nset_t *
+njs_xml_nset_create(njs_vm_t *vm, xmlDoc *doc, xmlNodeSet *nodes,
+    njs_xml_nset_type_t type)
+{
+    njs_xml_nset_t    *nset;
+    njs_mp_cleanup_t  *cln;
+
+    nset = njs_mp_zalloc(njs_vm_memory_pool(vm), sizeof(njs_xml_nset_t));
+    if (njs_slow_path(nset == NULL)) {
+        njs_vm_memory_pool(vm);
+        return NULL;
+    }
+
+    cln = njs_mp_cleanup_add(njs_vm_memory_pool(vm), 0);
+    if (njs_slow_path(cln == NULL)) {
+        njs_vm_memory_error(vm);
+        return NULL;
+    }
+
+    cln->handler = njs_xml_nset_cleanup;
+    cln->data = nset;
+
+    nset->doc = doc;
+    nset->type = type;
+    nset->nodes = nodes;
+    nset->next = nset->prev = nset;
+
+    return nset;
+}
+
+
+static njs_xml_nset_t *
+njs_xml_nset_children(njs_vm_t *vm, xmlNode *parent)
+{
+    xmlNodeSet  *nodes;
+
+    nodes = xmlXPathNodeSetCreate(parent);
+    if (njs_slow_path(nodes == NULL)) {
+        njs_vm_memory_pool(vm);
+        return NULL;
+    }
+
+    return njs_xml_nset_create(vm, parent->doc, nodes, XML_NSET_TREE_INVERT);
+}
+
+
+static njs_xml_nset_t *
+njs_xml_nset_add(njs_xml_nset_t *nset, njs_xml_nset_t *add)
+{
+    if (nset == NULL) {
+        return add;
+    }
+
+    add->next = nset;
+    add->prev = nset->prev;
+    nset->prev->next = add;
+    nset->prev = add;
+
+    return nset;
+}
+
+
+static void
+njs_xml_nset_cleanup(void *data)
+{
+    njs_xml_nset_t  *nset = data;
+
+    if (nset->nodes != NULL) {
+        xmlXPathFreeNodeSet(nset->nodes);
+    }
+}
+
+
+static void
+njs_xml_error(njs_vm_t *vm, njs_xml_doc_t *current, const char *fmt, ...)
+{
+    u_char         *p, *last;
+    va_list        args;
+    xmlError       *err;
+    u_char         errstr[NJS_MAX_ERROR_STR];
+
+    last = &errstr[NJS_MAX_ERROR_STR];
+
+    va_start(args, fmt);
+    p = njs_vsprintf(errstr, last - 1, fmt, args);
+    va_end(args);
+
+    err = xmlCtxtGetLastError(current->ctx);
+
+    if (err != NULL) {
+        p = njs_sprintf(p, last - 1, " (libxml2: \"%*s\" at %d:%d)",
+                        njs_strlen(err->message) - 1, err->message, err->line,
+                        err->int2);
+    }
+
+    njs_vm_value_error_set(vm, njs_vm_retval(vm), "%*s", p - errstr, errstr);
+}
+
+
+static njs_int_t
+njs_xml_init(njs_vm_t *vm)
+{
+    njs_int_t           ret, proto_id;
+    njs_mod_t           *module;
+    njs_opaque_value_t  value;
+
+    xmlInitParser();
+
+    njs_xml_doc_proto_id = njs_vm_external_prototype(vm, njs_ext_xml_doc,
+                                                  njs_nitems(njs_ext_xml_doc));
+    if (njs_slow_path(njs_xml_doc_proto_id < 0)) {
+        return NJS_ERROR;
+    }
+
+    njs_xml_node_proto_id = njs_vm_external_prototype(vm, njs_ext_xml_node,
+                                                  njs_nitems(njs_ext_xml_node));
+    if (njs_slow_path(njs_xml_node_proto_id < 0)) {
+        return NJS_ERROR;
+    }
+
+    njs_xml_attr_proto_id = njs_vm_external_prototype(vm, njs_ext_xml_attr,
+                                                  njs_nitems(njs_ext_xml_attr));
+    if (njs_slow_path(njs_xml_attr_proto_id < 0)) {
+        return NJS_ERROR;
+    }
+
+    proto_id = njs_vm_external_prototype(vm, njs_ext_xml,
+                                         njs_nitems(njs_ext_xml));
+    if (njs_slow_path(proto_id < 0)) {
+        return NJS_ERROR;
+    }
+
+    ret = njs_vm_external_create(vm, njs_value_arg(&value), proto_id, NULL, 1);
+    if (njs_slow_path(ret != NJS_OK)) {
+        return NJS_ERROR;
+    }
+
+    module = njs_vm_add_module(vm, &njs_str_value("xml"),
+                               njs_value_arg(&value));
+    if (njs_slow_path(module == NULL)) {
+        return NJS_ERROR;
+    }
+
+    return NJS_OK;
+}
index 1cc3c4360f56bbeb650db46b03784fd4ad09d58d..fb7fb923228fdc12782041cc7717c70b34fcdee6 100644 (file)
@@ -5,7 +5,8 @@ NJS_DEPS="$ngx_addon_dir/ngx_js.h \
 NJS_SRCS="$ngx_addon_dir/ngx_js.c \
     $ngx_addon_dir/ngx_js_fetch.c \
     $ngx_addon_dir/ngx_js_regex.c \
-    $ngx_addon_dir/../external/njs_webcrypto_module.c"
+    $ngx_addon_dir/../external/njs_webcrypto_module.c
+    $ngx_addon_dir/../external/njs_xml_module.c"
 
 if [ $HTTP != NO ]; then
     ngx_module_type=HTTP_AUX_FILTER
@@ -13,7 +14,7 @@ if [ $HTTP != NO ]; then
     ngx_module_incs="$ngx_addon_dir/../src $ngx_addon_dir/../build"
     ngx_module_deps="$ngx_addon_dir/../build/libnjs.a $NJS_DEPS"
     ngx_module_srcs="$ngx_addon_dir/ngx_http_js_module.c $NJS_SRCS"
-    ngx_module_libs="PCRE OPENSSL $ngx_addon_dir/../build/libnjs.a -lm"
+    ngx_module_libs="PCRE OPENSSL LIBXSLT $ngx_addon_dir/../build/libnjs.a -lm"
 
     . auto/module
 
index 64e435159663a6aaee5983b98e2c0865570b6120..2870285dbdaea3c71a4e9c0e7e2e505c12175b89 100644 (file)
@@ -3,7 +3,7 @@ cat << END                                            >> $NGX_MAKEFILE
 $ngx_addon_dir/../build/libnjs.a: $NGX_MAKEFILE
        cd $ngx_addon_dir/.. \\
        && if [ -f build/Makefile ]; then \$(MAKE) clean; fi \\
-       && CFLAGS="\$(CFLAGS)" CC="\$(CC)" ./configure --no-openssl --no-pcre \\
+       && CFLAGS="\$(CFLAGS)" CC="\$(CC)" ./configure --no-openssl --no-libxml2 --no-pcre \\
        && \$(MAKE) libnjs
 
 END
index 52428229e59b0e16f3019aad2ec9fdb450376ebc..dccfea195ca2b72914b8f003edf555eff817af41 100644 (file)
@@ -18,6 +18,7 @@ static void ngx_js_cleanup_vm(void *data);
 
 
 extern njs_module_t  njs_webcrypto_module;
+extern njs_module_t  njs_xml_module;
 
 
 static njs_external_t  ngx_js_ext_core[] = {
@@ -87,6 +88,7 @@ static njs_external_t  ngx_js_ext_core[] = {
 
 njs_module_t *njs_js_addon_modules[] = {
     &njs_webcrypto_module,
+    &njs_xml_module,
     NULL,
 };
 
index 68b7f6ee94bc9ab85bbdd95ce5004b4a2a2af4b5..ae2c2e5d18bcdb3f01abb222434f49726da44bc0 100644 (file)
@@ -21466,6 +21466,88 @@ static njs_unit_test_t  njs_webcrypto_test[] =
 };
 
 
+static njs_unit_test_t  njs_xml_test[] =
+{
+    { njs_str("const xml = require('xml');"
+              "let doc = xml.parse(`<note><to b=\"bar\" a= \"foo\" >Tove</to><from>Jani</from></note>`);"
+              "[doc.note.$name,"
+              " doc.note.to.$text,"
+              " doc.note.$parent,"
+              " doc.note.to.$parent.$name,"
+              " doc.note.$tag$to.$text,"
+              " doc.note.to.$attr$b,"
+              " doc.note.$tags[1].$text,"
+              " doc.note.$tags$from[0].$text]"),
+      njs_str("note,Tove,,note,Tove,bar,Jani,Jani") },
+
+    { njs_str("const xml = require('xml');"
+              "let doc = xml.parse(`<root><foo>FOO</foo><foo>BAR</foo></root>`);"
+              "[doc.root.$tags$foo[0].$text,"
+              " doc.root.$tags$foo[1].$text,"
+              " doc.root.$tags$bar.length,"
+              " doc.root.$tags$.length]"),
+      njs_str("FOO,BAR,0,2") },
+
+    { njs_str("const xml = require('xml');"
+              "let doc = xml.parse(`<r><a></a>TEXT</r>`);"
+              "doc.r.$text"),
+      njs_str("TEXT") },
+
+    { njs_str("const xml = require('xml');"
+              "let doc = xml.parse(`<r>俄语<a></a>данные</r>`);"
+              "doc.r.$text[2]"),
+      njs_str("д") },
+
+    { njs_str("const xml = require('xml');"
+              "let doc = xml.parse(`<俄语 Õ¬Õ¥Õ¦Õ¸Ö‚=\"Õ¼Õ¸Ö‚Õ½Õ¥Ö€Õ¥Õ¶\">данные</俄语>`);"
+              "[doc['俄语'].$name[1],"
+              " doc['俄语']['$attr$Õ¬Õ¥Õ¦Õ¸Ö‚'][7],"
+              " doc['俄语'].$text[5]]"),
+      njs_str("语,Õ¶,е") },
+
+    { njs_str("const xml = require('xml');"
+              "var doc = xml.parse(`<n0:pdu xmlns:n0=\"http://a\"><n1:elem1 xmlns:n1=\"http://b\">"
+                                   "<!-- comment -->foo</n1:elem1></n0:pdu>`);"
+              "[xml.c14n(doc.pdu.elem1),"
+              " xml.exclusiveC14n(doc.pdu.elem1),"
+              " xml.exclusiveC14n(doc.pdu.elem1, null, 1),"
+              " xml.exclusiveC14n(doc.pdu.elem1, null, false, 'n0 n1')]"
+              ".map(v => (new TextDecoder().decode(v)))"),
+      njs_str("<n1:elem1 xmlns:n0=\"http://a\" xmlns:n1=\"http://b\">foo</n1:elem1>,"
+              "<n1:elem1 xmlns:n1=\"http://b\">foo</n1:elem1>,"
+              "<n1:elem1 xmlns:n1=\"http://b\"><!-- comment -->foo</n1:elem1>,"
+              "<n1:elem1 xmlns:n0=\"http://a\" xmlns:n1=\"http://b\">foo</n1:elem1>") },
+
+    { njs_str("const xml = require('xml');"
+              "let doc = xml.parse(`<note><to b=\"bar\" a= \"foo\" >Tove</to><from>Jani</from></note>`);"
+              "let dec = new TextDecoder();"
+              "dec.decode(xml.exclusiveC14n(doc.note))"),
+      njs_str("<note><to a=\"foo\" b=\"bar\">Tove</to><from>Jani</from></note>") },
+
+    { njs_str("const xml = require('xml');"
+              "let doc = xml.parse(`<note><to b=\"bar\" a= \"foo\" >Tove</to><from>Jani</from></note>`);"
+              "let dec = new TextDecoder();"
+              "dec.decode(xml.exclusiveC14n(doc.note, doc.note.to))"),
+      njs_str("<note><from>Jani</from></note>") },
+
+    { njs_str("const xml = require('xml');"
+              "let doc = xml.parse(`<note><to b=\"bar\" a= \"foo\" >Tove</to><from>Jani</from></note>`);"
+              "njs.dump(doc)"),
+      njs_str("XMLDoc {note:XMLNode {$name:'note',"
+              "$tags:[XMLNode {$name:'to',"
+              "$attrs:XMLAttr {b:'bar',a:'foo'},"
+              "$text:'Tove'},"
+              "XMLNode {$name:'from',$text:'Jani'}]}}") },
+
+    { njs_str("const xml = require('xml');"
+              "let doc = xml.parse(`<note><to b=\"bar\" a= \"foo\" >Tove</to><from>Jani</from></note>`);"
+              "JSON.stringify(doc)"),
+      njs_str("{\"note\":{\"$name\":\"note\",\"$tags\":"
+              "[{\"$name\":\"to\",\"$attrs\":{\"b\":\"bar\",\"a\":\"foo\"},"
+              "\"$text\":\"Tove\"},{\"$name\":\"from\",\"$text\":\"Jani\"}]}}") },
+};
+
+
 static njs_unit_test_t  njs_module_test[] =
 {
     { njs_str("function f(){return 2}; var f; f()"),
@@ -24268,6 +24350,17 @@ static njs_test_suite_t  njs_suites[] =
       njs_nitems(njs_webcrypto_test),
       njs_unit_test },
 
+    {
+#if (NJS_HAVE_LIBXML2 && !NJS_HAVE_MEMORY_SANITIZER)
+        njs_str("xml"),
+#else
+        njs_str(""),
+#endif
+      { .externals = 1, .repeat = 1, .unsafe = 1 },
+      njs_xml_test,
+      njs_nitems(njs_xml_test),
+      njs_unit_test },
+
     { njs_str("module"),
       { .repeat = 1, .module = 1, .unsafe = 1 },
       njs_module_test,
diff --git a/test/harness/compatNjs.js b/test/harness/compatNjs.js
new file mode 100644 (file)
index 0000000..844a7ae
--- /dev/null
@@ -0,0 +1,18 @@
+function has_njs(required_version) {
+    if (typeof njs != 'object'
+        || typeof njs.version != 'string'
+        || typeof njs.version_number != 'number')
+    {
+        return false;
+    }
+
+    if (required_version) {
+        let req_number = required_version.split('.')
+                         .map(v => Number(v)).reduce((acc, v) => acc * 256 + v, 0);
+
+        return njs.version_number <= req_number;
+    }
+
+
+    return true;
+}
diff --git a/test/harness/compatXml.js b/test/harness/compatXml.js
new file mode 100644 (file)
index 0000000..465d95e
--- /dev/null
@@ -0,0 +1,8 @@
+let xml = null;
+
+if (typeof require == 'function'
+    && typeof njs == 'object'
+    && typeof njs.version == 'string')
+{
+    xml = require('xml');
+}
diff --git a/test/xml/README.rst b/test/xml/README.rst
new file mode 100644 (file)
index 0000000..ec2c851
--- /dev/null
@@ -0,0 +1,26 @@
+==========
+XML tests
+==========
+
+SAML signing
+============
+
+Generating SAML signed AuthnRequest
+-----------------------------------
+
+Note: XMLSec library is used (https://www.aleksey.com/xmlsec/).
+
+.. code-block:: shell
+
+  xmlsec1 --sign  --pkcs8-pem test/webcrypto/rsa.pkcs8 \
+    --output test/xml/<template>_signed.xml  --id-attr:ID AuthnRequest test/xml/<template>.xml
+  xmlsec1 --sign  --pkcs8-pem test/webcrypto/rsa2.pkcs8 \
+    --output test/xml/<template>_signed2.xml  --id-attr:ID AuthnRequest test/xml/<template>.xml
+
+Generating X509 self-signed certificate with an existing RSA key
+----------------------------------------------------------------
+
+.. code-block:: shell
+
+   openssl req -x509 -key test/webcrypto/rsa.pkcs8
+     -out test/xml/example.com.crt -sha256 -days 3650 -subj '/CN=example.com'
diff --git a/test/xml/auth_r.xml b/test/xml/auth_r.xml
new file mode 100644 (file)
index 0000000..da269a8
--- /dev/null
@@ -0,0 +1,27 @@
+<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
+                    Destination="https://example.com/cas/login"
+                    ForceAuthn="false"
+                    xml:ID="_0x14956c887e664bdb71d7685b89b70619"
+                    IsPassive="false"
+                    IssueInstant="2023-01-14T06:36:15.026Z"
+                    Version="2.0"
+                    >
+    <saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">FOO</saml:Issuer>
+    <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+        <ds:SignedInfo>
+            <!-- COMMENT -->
+            <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
+            <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" />
+            <ds:Reference URI="#_0x14956c887e664bdb71d7685b89b70619">
+                <ds:Transforms>
+                    <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
+                    <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
+                </ds:Transforms>
+                <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" />
+                <ds:DigestValue></ds:DigestValue>
+            </ds:Reference>
+        </ds:SignedInfo>
+        <ds:SignatureValue>
+</ds:SignatureValue>
+    </ds:Signature>
+</samlp:AuthnRequest>
diff --git a/test/xml/auth_r_prefix_list.xml b/test/xml/auth_r_prefix_list.xml
new file mode 100644 (file)
index 0000000..4003fbc
--- /dev/null
@@ -0,0 +1,28 @@
+<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
+                    Destination="https://example.com/cas/login"
+                    ForceAuthn="false"
+                    xml:ID="UUID"
+                    IsPassive="false"
+                    IssueInstant="2023-01-14T06:36:15.026Z"
+                    Version="2.0"
+                    >
+    <saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">FOO</saml:Issuer>
+    <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+        <ds:SignedInfo>
+            <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
+            <ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
+            <ds:Reference URI="#UUID">
+                <ds:Transforms>
+                    <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
+                    <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
+                    <ec:InclusiveNamespaces xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#" PrefixList="ds saml samlp"/>
+                    </ds:Transform>
+                </ds:Transforms>
+                <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" />
+                <ds:DigestValue></ds:DigestValue>
+            </ds:Reference>
+        </ds:SignedInfo>
+        <ds:SignatureValue>
+</ds:SignatureValue>
+    </ds:Signature>
+</samlp:AuthnRequest>
diff --git a/test/xml/auth_r_prefix_list_signed.xml b/test/xml/auth_r_prefix_list_signed.xml
new file mode 100644 (file)
index 0000000..d9c6e85
--- /dev/null
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://example.com/cas/login" ForceAuthn="false" xml:ID="UUID" IsPassive="false" IssueInstant="2023-01-14T06:36:15.026Z" Version="2.0">
+    <saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">FOO</saml:Issuer>
+    <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+        <ds:SignedInfo>
+            <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
+            <ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
+            <ds:Reference URI="#UUID">
+                <ds:Transforms>
+                    <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
+                    <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
+                    <ec:InclusiveNamespaces xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#" PrefixList="ds saml samlp"/>
+                    </ds:Transform>
+                </ds:Transforms>
+                <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
+                <ds:DigestValue>G+mga6rnvpus96slB6HmEEuYyaMjQYLDrK8P3Kq/nd4=</ds:DigestValue>
+            </ds:Reference>
+        </ds:SignedInfo>
+        <ds:SignatureValue>k7QVaQcoQ9kKgNcNjgaVWPDiWKIAWisOgFOBh/wwNh02m9qNRChTQL9hg3U82XGt
+KpjIv3VN+ebxdnkltrU1c/QTOj1RP4H9nqtHla3yVnR/+iUnOFmYGnPgzSRv5DeG
+p6S+/1X84vO+6/IgtM4RRJP0XVoQQqB4tlVQAx0YFSM=</ds:SignatureValue>
+    </ds:Signature>
+</samlp:AuthnRequest>
diff --git a/test/xml/auth_r_signed.xml b/test/xml/auth_r_signed.xml
new file mode 100644 (file)
index 0000000..0836933
--- /dev/null
@@ -0,0 +1,22 @@
+<?xml version="1.0"?>
+<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://example.com/cas/login" ForceAuthn="false" xml:ID="_0x14956c887e664bdb71d7685b89b70619" IsPassive="false" IssueInstant="2023-01-14T06:36:15.026Z" Version="2.0">
+    <saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">FOO</saml:Issuer>
+    <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+        <ds:SignedInfo>
+            <!-- COMMENT -->
+            <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
+            <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
+            <ds:Reference URI="#_0x14956c887e664bdb71d7685b89b70619">
+                <ds:Transforms>
+                    <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
+                    <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
+                </ds:Transforms>
+                <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
+                <ds:DigestValue>JZkcRthnoo1iI2/kLXsPpC+3ZOh3Nfl4aGQrNj3y9XM=</ds:DigestValue>
+            </ds:Reference>
+        </ds:SignedInfo>
+        <ds:SignatureValue>kl5IyCav6YQcd4TMaoJi2LQq6Lxq2AmCwNmtVaLHeUUp/dsWAwbMsOmfCEo3XxFS
+c7wCBVamQ4i3Zw37C6KwAcJ19U4Qwf3LdOu+Ek2aZBosgMpNdgpUPd0vNRFCeUF3
+DU+fcFvQ0kO5kJyBd4EBrBIwxZQG1K29Cts65U3rqQM=</ds:SignatureValue>
+    </ds:Signature>
+</samlp:AuthnRequest>
diff --git a/test/xml/auth_r_signed2.xml b/test/xml/auth_r_signed2.xml
new file mode 100644 (file)
index 0000000..7253ff5
--- /dev/null
@@ -0,0 +1,22 @@
+<?xml version="1.0"?>
+<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://example.com/cas/login" ForceAuthn="false" xml:ID="_0x14956c887e664bdb71d7685b89b70619" IsPassive="false" IssueInstant="2023-01-14T06:36:15.026Z" Version="2.0">
+    <saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">FOO</saml:Issuer>
+    <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+        <ds:SignedInfo>
+            <!-- COMMENT -->
+            <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
+            <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
+            <ds:Reference URI="#_0x14956c887e664bdb71d7685b89b70619">
+                <ds:Transforms>
+                    <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
+                    <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
+                </ds:Transforms>
+                <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
+                <ds:DigestValue>JZkcRthnoo1iI2/kLXsPpC+3ZOh3Nfl4aGQrNj3y9XM=</ds:DigestValue>
+            </ds:Reference>
+        </ds:SignedInfo>
+        <ds:SignatureValue>A6iMtm1g278JFMMqQSynGj8Qv35lvzAarb0PyVK8IbL7FfeK2d3+maJ0PKyUmccg
+rhKUR5sUmpoBCCaMTu0phBTADYxR24v40sB2eQ8Pa7Zh5qpfDAs9wqRSQ0cvCK+b
+BAIP0o/UvOmmGVUUJ71aLlkxpCKuBrdY5r0hfXRaMg4=</ds:SignatureValue>
+    </ds:Signature>
+</samlp:AuthnRequest>
diff --git a/test/xml/auth_r_with_comments_signed.xml b/test/xml/auth_r_with_comments_signed.xml
new file mode 100644 (file)
index 0000000..b3c1d13
--- /dev/null
@@ -0,0 +1,22 @@
+<?xml version="1.0"?>
+<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://example.com/cas/login" ForceAuthn="false" xml:ID="_0x14956c887e664bdb71d7685b89b70619" IsPassive="false" IssueInstant="2023-01-14T06:36:15.026Z" Version="2.0">
+    <saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">FOO</saml:Issuer>
+    <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+        <ds:SignedInfo>
+            <!-- COMMENT -->
+            <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#WithComments"/>
+            <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
+            <ds:Reference URI="#_0x14956c887e664bdb71d7685b89b70619">
+                <ds:Transforms>
+                    <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
+                    <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
+                </ds:Transforms>
+                <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
+                <ds:DigestValue>JZkcRthnoo1iI2/kLXsPpC+3ZOh3Nfl4aGQrNj3y9XM=</ds:DigestValue>
+            </ds:Reference>
+        </ds:SignedInfo>
+        <ds:SignatureValue>T8/7mMdPwoJfLC3rmpvC3WXop/aLjs9DsSDOxb24LiyEmXRh2xXW62GgezCl5I81
+X5FtJwFi0J5BkzsKZIftn8BP5vA/aTaRi/4swEHQTWX30Nu2TSyLVbUG5KeGMXdT
+NZPB9LuTiTE6avxZe/h8pBZH1LBE/6/QANnIhVa4LUA=</ds:SignatureValue>
+    </ds:Signature>
+</samlp:AuthnRequest>
diff --git a/test/xml/example.com.crt b/test/xml/example.com.crt
new file mode 100644 (file)
index 0000000..b87f3b1
--- /dev/null
@@ -0,0 +1,13 @@
+-----BEGIN CERTIFICATE-----
+MIICCDCCAXGgAwIBAgIUSncb2ZqxGCm2OzT8wrr5RiTu8nAwDQYJKoZIhvcNAQEL
+BQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wHhcNMjMwMTI0MDUwNDEwWhcNMzMw
+MTIxMDUwNDEwWjAWMRQwEgYDVQQDDAtleGFtcGxlLmNvbTCBnzANBgkqhkiG9w0B
+AQEFAAOBjQAwgYkCgYEAyUmxoJC8VAM5hyYZa+XUBZg1N1ywFMPUpWsF1kaSGed9
+8P3XUgPzgX80wpyzd5qdGuALqnf2lMc7O8PrGBtO5YrvQlI96NX0jUo5bc5wz220
+ob3AUCeQnTfx+UFqM4pCwjoDSo2PlphJdWgFYymGBaBCJgnENQL9H1N/8/yNiN8C
+AwEAAaNTMFEwHQYDVR0OBBYEFE2pdHHEYufJu4dnU88NgqnxBmmhMB8GA1UdIwQY
+MBaAFE2pdHHEYufJu4dnU88NgqnxBmmhMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI
+hvcNAQELBQADgYEAiLTQbPQWz3Mj8b4mTUJZ72PEaWuc7+4H5FQQBScrrH6SrvCy
+UmOfqzaaku9L4nublUfFaTg/8lkWoU2cQ0lGrVwvA6hFYKJMhKQNVbHN+FUwlMal
+C9CFn9GpNj09AfK4c6HDE05L/2yrpfUyorv8Ux8O1krhP+Zk6ckHwVkX+xM=
+-----END CERTIFICATE-----
diff --git a/test/xml/response_assertion_and_message_signed.xml b/test/xml/response_assertion_and_message_signed.xml
new file mode 100644 (file)
index 0000000..7b884b2
--- /dev/null
@@ -0,0 +1,46 @@
+<?xml version="1.0"?>
+<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="pfx4151366c-e22c-cc5e-fdea-3263a7ef8ba6" Version="2.0" IssueInstant="2023-01-24T05:05:45.547Z" Destination="http://example.com/sso" InResponseTo="4ab1c392-9ba5-11ed-a8fc-0242ac120002 ">
+  <saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer><ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+  <ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
+    <ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
+  <ds:Reference URI="#pfx4151366c-e22c-cc5e-fdea-3263a7ef8ba6"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><ds:DigestValue>e5pgN/a9H2MMtJDDbdzSF8uhfBs=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>Oeh9hUUyt2vSSY59IIr2SgDmenRwpv1Iz1VGfofRTZqsuy3iMdM1nXSzQkKxjhOq4wQpcH6MPu+eZQaKC7xbVD3S/zwJC1MhMS87TRCz6e7LYtjFfF1xuMtMt+o+hmVTLMv1878v9heUF36uZqjEl53JaVB6nSRIzyRckA5F4KU=</ds:SignatureValue>
+<ds:KeyInfo><ds:X509Data><ds:X509Certificate>MIICCDCCAXGgAwIBAgIUSncb2ZqxGCm2OzT8wrr5RiTu8nAwDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wHhcNMjMwMTI0MDUwNDEwWhcNMzMwMTIxMDUwNDEwWjAWMRQwEgYDVQQDDAtleGFtcGxlLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAyUmxoJC8VAM5hyYZa+XUBZg1N1ywFMPUpWsF1kaSGed98P3XUgPzgX80wpyzd5qdGuALqnf2lMc7O8PrGBtO5YrvQlI96NX0jUo5bc5wz220ob3AUCeQnTfx+UFqM4pCwjoDSo2PlphJdWgFYymGBaBCJgnENQL9H1N/8/yNiN8CAwEAAaNTMFEwHQYDVR0OBBYEFE2pdHHEYufJu4dnU88NgqnxBmmhMB8GA1UdIwQYMBaAFE2pdHHEYufJu4dnU88NgqnxBmmhMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADgYEAiLTQbPQWz3Mj8b4mTUJZ72PEaWuc7+4H5FQQBScrrH6SrvCyUmOfqzaaku9L4nublUfFaTg/8lkWoU2cQ0lGrVwvA6hFYKJMhKQNVbHN+FUwlMalC9CFn9GpNj09AfK4c6HDE05L/2yrpfUyorv8Ux8O1krhP+Zk6ckHwVkX+xM=</ds:X509Certificate></ds:X509Data></ds:KeyInfo></ds:Signature>
+  <samlp:Status>
+    <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
+  </samlp:Status>
+  <saml:Assertion xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" ID="pfx3f30286e-d8d2-4e7c-f03e-eb89ddf4c0e3" Version="2.0" IssueInstant="2023-01-24T05:05:45.547Z">
+    <saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer><ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+  <ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
+    <ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
+  <ds:Reference URI="#pfx3f30286e-d8d2-4e7c-f03e-eb89ddf4c0e3"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><ds:DigestValue>YvDHDaI1HuaeRaLTB7f2sG1a4aA=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>xUgTH+e8PaYSKsqRSqUQO+bogh/YglJHZ4El/L+kPdgXkHz1Uzo88Y5vu6OBpKiTx4HblsXfPbrDJpArJf/6a53em7w08P0fCOgGYmNG30gUHx7bJAF9KXb5Hem9KqaCRDdYy/CHUbo2CmLvaAoveDmrPTNSAJji3UHEMxwxBKI=</ds:SignatureValue>
+<ds:KeyInfo><ds:X509Data><ds:X509Certificate>MIICCDCCAXGgAwIBAgIUSncb2ZqxGCm2OzT8wrr5RiTu8nAwDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wHhcNMjMwMTI0MDUwNDEwWhcNMzMwMTIxMDUwNDEwWjAWMRQwEgYDVQQDDAtleGFtcGxlLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAyUmxoJC8VAM5hyYZa+XUBZg1N1ywFMPUpWsF1kaSGed98P3XUgPzgX80wpyzd5qdGuALqnf2lMc7O8PrGBtO5YrvQlI96NX0jUo5bc5wz220ob3AUCeQnTfx+UFqM4pCwjoDSo2PlphJdWgFYymGBaBCJgnENQL9H1N/8/yNiN8CAwEAAaNTMFEwHQYDVR0OBBYEFE2pdHHEYufJu4dnU88NgqnxBmmhMB8GA1UdIwQYMBaAFE2pdHHEYufJu4dnU88NgqnxBmmhMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADgYEAiLTQbPQWz3Mj8b4mTUJZ72PEaWuc7+4H5FQQBScrrH6SrvCyUmOfqzaaku9L4nublUfFaTg/8lkWoU2cQ0lGrVwvA6hFYKJMhKQNVbHN+FUwlMalC9CFn9GpNj09AfK4c6HDE05L/2yrpfUyorv8Ux8O1krhP+Zk6ckHwVkX+xM=</ds:X509Certificate></ds:X509Data></ds:KeyInfo></ds:Signature>
+    <saml:Subject>
+      <saml:NameID SPNameQualifier="http://sp.example.com/demo1/metadata.php" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7</saml:NameID>
+      <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
+        <saml:SubjectConfirmationData NotOnOrAfter="2024-01-18T06:21:48Z" Recipient="http://sp.example.com/demo1/index.php?acs" InResponseTo="4ab1c392-9ba5-11ed-a8fc-0242ac120002 "/>
+      </saml:SubjectConfirmation>
+    </saml:Subject>
+    <saml:Conditions NotBefore="2014-07-17T01:01:18Z" NotOnOrAfter="2024-01-18T06:21:48Z">
+      <saml:AudienceRestriction>
+        <saml:Audience>http://example.com/demo1/metadata.php</saml:Audience>
+      </saml:AudienceRestriction>
+    </saml:Conditions>
+    <saml:AuthnStatement AuthnInstant="2023-01-24T05:05:45.547Z" SessionNotOnOrAfter="2024-07-17T09:01:48Z" SessionIndex="_be9967abd904ddcae3c0eb4189adbe3f71e327cf93">
+      <saml:AuthnContext>
+        <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef>
+      </saml:AuthnContext>
+    </saml:AuthnStatement>
+    <saml:AttributeStatement>
+      <saml:Attribute Name="uid" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
+        <saml:AttributeValue xsi:type="xs:string">test</saml:AttributeValue>
+      </saml:Attribute>
+      <saml:Attribute Name="mail" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
+        <saml:AttributeValue xsi:type="xs:string">test@example.com</saml:AttributeValue>
+      </saml:Attribute>
+      <saml:Attribute Name="eduPersonAffiliation" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
+        <saml:AttributeValue xsi:type="xs:string">users</saml:AttributeValue>
+        <saml:AttributeValue xsi:type="xs:string">examplerole1</saml:AttributeValue>
+      </saml:Attribute>
+    </saml:AttributeStatement>
+  </saml:Assertion>
+</samlp:Response>
diff --git a/test/xml/response_signed.xml b/test/xml/response_signed.xml
new file mode 100644 (file)
index 0000000..6827fb0
--- /dev/null
@@ -0,0 +1,42 @@
+<?xml version="1.0"?>
+<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="pfxb2150afb-a9f8-253b-cf66-aafb87618ccb" Version="2.0" IssueInstant="2023-01-24T05:05:45.547Z" Destination="http://example.com/sso" InResponseTo="4ab1c392-9ba5-11ed-a8fc-0242ac120002 ">
+  <saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer><ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+  <ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
+    <ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
+  <ds:Reference URI="#pfxb2150afb-a9f8-253b-cf66-aafb87618ccb"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><ds:DigestValue>lYfYJRCeOzl5QcFrlqdvY6S26ag=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>dL94oqjCdGujb7R7TWwkj6CcXDv69og0rdIde7MfWObWYhJTvMwidC89n15eTWr+SdV9QV6ziabLmhSVVtwgnAiIQpgHQGNuKeN2p7TlpmnBNP3Y1UyQPkeMLyXiWqks3wv9eMS0chdTJ8/yZhjRRjUzegbBB9Sv8W2MzBt7Hrs=</ds:SignatureValue>
+<ds:KeyInfo><ds:X509Data><ds:X509Certificate>MIICCDCCAXGgAwIBAgIUSncb2ZqxGCm2OzT8wrr5RiTu8nAwDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wHhcNMjMwMTI0MDUwNDEwWhcNMzMwMTIxMDUwNDEwWjAWMRQwEgYDVQQDDAtleGFtcGxlLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAyUmxoJC8VAM5hyYZa+XUBZg1N1ywFMPUpWsF1kaSGed98P3XUgPzgX80wpyzd5qdGuALqnf2lMc7O8PrGBtO5YrvQlI96NX0jUo5bc5wz220ob3AUCeQnTfx+UFqM4pCwjoDSo2PlphJdWgFYymGBaBCJgnENQL9H1N/8/yNiN8CAwEAAaNTMFEwHQYDVR0OBBYEFE2pdHHEYufJu4dnU88NgqnxBmmhMB8GA1UdIwQYMBaAFE2pdHHEYufJu4dnU88NgqnxBmmhMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADgYEAiLTQbPQWz3Mj8b4mTUJZ72PEaWuc7+4H5FQQBScrrH6SrvCyUmOfqzaaku9L4nublUfFaTg/8lkWoU2cQ0lGrVwvA6hFYKJMhKQNVbHN+FUwlMalC9CFn9GpNj09AfK4c6HDE05L/2yrpfUyorv8Ux8O1krhP+Zk6ckHwVkX+xM=</ds:X509Certificate></ds:X509Data></ds:KeyInfo></ds:Signature>
+  <samlp:Status>
+    <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
+  </samlp:Status>
+  <saml:Assertion xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" ID="_d71a3a8e9fcc45c9e9d248ef7049393fc8f04e5f75" Version="2.0" IssueInstant="2023-01-24T05:05:45.547Z">
+    <saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer>
+    <saml:Subject>
+      <saml:NameID SPNameQualifier="http://sp.example.com/demo1/metadata.php" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7</saml:NameID>
+      <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
+        <saml:SubjectConfirmationData NotOnOrAfter="2024-01-18T06:21:48Z" Recipient="http://sp.example.com/demo1/index.php?acs" InResponseTo="4ab1c392-9ba5-11ed-a8fc-0242ac120002 "/>
+      </saml:SubjectConfirmation>
+    </saml:Subject>
+    <saml:Conditions NotBefore="2014-07-17T01:01:18Z" NotOnOrAfter="2024-01-18T06:21:48Z">
+      <saml:AudienceRestriction>
+        <saml:Audience>http://example.com/demo1/metadata.php</saml:Audience>
+      </saml:AudienceRestriction>
+    </saml:Conditions>
+    <saml:AuthnStatement AuthnInstant="2023-01-24T05:05:45.547Z" SessionNotOnOrAfter="2024-07-17T09:01:48Z" SessionIndex="_be9967abd904ddcae3c0eb4189adbe3f71e327cf93">
+      <saml:AuthnContext>
+        <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef>
+      </saml:AuthnContext>
+    </saml:AuthnStatement>
+    <saml:AttributeStatement>
+      <saml:Attribute Name="uid" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
+        <saml:AttributeValue xsi:type="xs:string">test</saml:AttributeValue>
+      </saml:Attribute>
+      <saml:Attribute Name="mail" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
+        <saml:AttributeValue xsi:type="xs:string">test@example.com</saml:AttributeValue>
+      </saml:Attribute>
+      <saml:Attribute Name="eduPersonAffiliation" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
+        <saml:AttributeValue xsi:type="xs:string">users</saml:AttributeValue>
+        <saml:AttributeValue xsi:type="xs:string">examplerole1</saml:AttributeValue>
+      </saml:Attribute>
+    </saml:AttributeStatement>
+  </saml:Assertion>
+</samlp:Response>
diff --git a/test/xml/response_signed_broken.xml b/test/xml/response_signed_broken.xml
new file mode 100644 (file)
index 0000000..04367b0
--- /dev/null
@@ -0,0 +1,42 @@
+<?xml version="1.0"?>
+<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="pfxb2150afb-a9f8-253b-cf66-aafb87618ccb" Version="2.0" IssueInstant="2023-01-24T05:05:45.547Z" Destination="http://example.com/sso" InResponseTo="4ab1c392-9ba5-11ed-a8fc-0242ac120002 ">
+  <saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer><ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+  <ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
+    <ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
+  <ds:Reference URI="#pfxb2150afb-a9f8-253b-cf66-aafb87618ccb"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><ds:DigestValue>lYfYJRCeOzl5QcFrlqdvY6S26ag=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>dL94oqjCdGujb7R7TWwkj6CcXDv69og0rdIde7MfWObWYhJTvMwidC89n15eTWr+SdV9QV6ziabLmhSVVtwgnAiIQpgHQGNuKeN2p7TlpmnBNP3Y1UyQPkeMLyXiWqks3wv9eMS0chdTJ8/yZhjRRjUzegbBB9Sv8W2MzBt7Hrs=</ds:SignatureValue>
+<ds:KeyInfo><ds:X509Data><ds:X509Certificate>MIICCDCCAXGgAwIBAgIUSncb2ZqxGCm2OzT8wrr5RiTu8nAwDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wHhcNMjMwMTI0MDUwNDEwWhcNMzMwMTIxMDUwNDEwWjAWMRQwEgYDVQQDDAtleGFtcGxlLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAyUmxoJC8VAM5hyYZa+XUBZg1N1ywFMPUpWsF1kaSGed98P3XUgPzgX80wpyzd5qdGuALqnf2lMc7O8PrGBtO5YrvQlI96NX0jUo5bc5wz220ob3AUCeQnTfx+UFqM4pCwjoDSo2PlphJdWgFYymGBaBCJgnENQL9H1N/8/yNiN8CAwEAAaNTMFEwHQYDVR0OBBYEFE2pdHHEYufJu4dnU88NgqnxBmmhMB8GA1UdIwQYMBaAFE2pdHHEYufJu4dnU88NgqnxBmmhMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADgYEAiLTQbPQWz3Mj8b4mTUJZ72PEaWuc7+4H5FQQBScrrH6SrvCyUmOfqzaaku9L4nublUfFaTg/8lkWoU2cQ0lGrVwvA6hFYKJMhKQNVbHN+FUwlMalC9CFn9GpNj09AfK4c6HDE05L/2yrpfUyorv8Ux8O1krhP+Zk6ckHwVkX+xM=</ds:X509Certificate></ds:X509Data></ds:KeyInfo></ds:Signature>
+  <samlp:Status>
+    <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
+  </samlp:Status>
+  <saml:Assertion xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" ID="_d71a3a8e9fcc45c9e9d248ef7049393fc8f04e5f75" Version="2.0" IssueInstant="2023-01-24T05:05:45.547Z">
+    <saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer>
+    <saml:Subject>
+      <saml:NameID SPNameQualifier="http://sp.example.com/demo1/metadata.php" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7</saml:NameID>
+      <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
+        <saml:SubjectConfirmationData NotOnOrAfter="2024-01-18T06:21:48Z" Recipient="http://sp.example.com/demo1/index.php?acs" InResponseTo="4ab1c392-9ba5-11ed-a8fc-0242ac120002 "/>
+      </saml:SubjectConfirmation>
+    </saml:Subject>
+    <saml:Conditions NotBefore="2014-07-17T01:01:18Z" NotOnOrAfter="2024-01-18T06:21:48Z">
+      <saml:AudienceRestriction>
+        <saml:Audience>http://example.com/demo1/metadata.php</saml:Audience>
+      </saml:AudienceRestriction>
+    </saml:Conditions>
+    <saml:AuthnStatement AuthnInstant="2023-01-24T05:05:45.547Z" SessionNotOnOrAfter="2024-07-17T09:01:48Z" SessionIndex="_be9967abd904ddcae3c0eb4189adbe3f71e327cf93">
+      <saml:AuthnContext>
+        <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef>
+      </saml:AuthnContext>
+    </saml:AuthnStatement>
+    <saml:AttributeStatement>
+      <saml:Attribute Name="uid" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
+        <saml:AttributeValue xsi:type="xs:string">test2</saml:AttributeValue>
+      </saml:Attribute>
+      <saml:Attribute Name="mail" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
+        <saml:AttributeValue xsi:type="xs:string">test@example.com</saml:AttributeValue>
+      </saml:Attribute>
+      <saml:Attribute Name="eduPersonAffiliation" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
+        <saml:AttributeValue xsi:type="xs:string">users</saml:AttributeValue>
+        <saml:AttributeValue xsi:type="xs:string">examplerole1</saml:AttributeValue>
+      </saml:Attribute>
+    </saml:AttributeStatement>
+  </saml:Assertion>
+</samlp:Response>
diff --git a/test/xml/response_signed_broken2.xml b/test/xml/response_signed_broken2.xml
new file mode 100644 (file)
index 0000000..88219e6
--- /dev/null
@@ -0,0 +1,42 @@
+<?xml version="1.0"?>
+<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="pfxb2150afb-a9f8-253b-cf66-aafb87618ccb" Version="2.0" IssueInstant="2023-01-24T05:05:45.547Z" Destination="http://example.com/sso" InResponseTo="4ab1c392-9ba5-11ed-a8fc-0242ac120002 ">
+  <saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer><ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+  <ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
+    <ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
+  <ds:Reference URI="#pfxb2150afb-a9f8-253b-cf66-aafb87618ccb"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><ds:DigestValue>lYfYJRCeOzl5QcFrlqdvY6S26ag=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>dL94oqjCdGujb7R7TWwkj6CcXDv69og0rdIde7MfWObWYhJTvMwidC89n15eTWr+SdV9QV6ziabLmhSVVtwgnAiIQpgHQGNuKeN2p7TlpmnBNP3Y1UyQPkeMLyXiWqks3wv9eMS0chdTJ8/yZhjRRjUzegbBB9Sv8W2MzBt7Hrs=</ds:SignatureValue>
+<ds:KeyInfo><ds:X509Data><ds:X509Certificate>MIICCDCCAXGgAwIBAgIUSncb2ZqxGCm2OzT8wrr5RiTu8nAwDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wHhcNMjMwMTI0MDUwNDEwWhcNMzMwMTIxMDUwNDEwWjAWMRQwEgYDVQQDDAtleGFtcGxlLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAyUmxoJC8VAM5hyYZa+XUBZg1N1ywFMPUpWsF1kaSGed98P3XUgPzgX80wpyzd5qdGuALqnf2lMc7O8PrGBtO5YrvQlI96NX0jUo5bc5wz220ob3AUCeQnTfx+UFqM4pCwjoDSo2PlphJdWgFYymGBaBCJgnENQL9H1N/8/yNiN8CAwEAAaNTMFEwHQYDVR0OBBYEFE2pdHHEYufJu4dnU88NgqnxBmmhMB8GA1UdIwQYMBaAFE2pdHHEYufJu4dnU88NgqnxBmmhMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADgYEAiLTQbPQWz3Mj8b4mTUJZ72PEaWuc7+4H5FQQBScrrH6SrvCyUmOfqzaaku9L4nublUfFaTg/8lkWoU2cQ0lGrVwvA6hFYKJMhKQNVbHN+FUwlMalC9CFn9GpNj09AfK4c6HDE05L/2yrpfUyorv8Ux8O1krhP+Zk6ckHwVkX+xM=</ds:X509Certificate></ds:X509Data></ds:KeyInfo></ds:Signature>
+  <samlp:Status>
+    <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
+  </samlp:Status>
+  <saml:Assertion xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" ID="_d71a3a8e9fcc45c9e9d248ef7049393fc8f04e5f75" Version="2.0" IssueInstant="2023-01-01T05:05:45.547Z">
+    <saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer>
+    <saml:Subject>
+      <saml:NameID SPNameQualifier="http://sp.example.com/demo1/metadata.php" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7</saml:NameID>
+      <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
+        <saml:SubjectConfirmationData NotOnOrAfter="2024-01-18T06:21:48Z" Recipient="http://sp.example.com/demo1/index.php?acs" InResponseTo="4ab1c392-9ba5-11ed-a8fc-0242ac120002 "/>
+      </saml:SubjectConfirmation>
+    </saml:Subject>
+    <saml:Conditions NotBefore="2014-07-17T01:01:18Z" NotOnOrAfter="2024-01-18T06:21:48Z">
+      <saml:AudienceRestriction>
+        <saml:Audience>http://example.com/demo1/metadata.php</saml:Audience>
+      </saml:AudienceRestriction>
+    </saml:Conditions>
+    <saml:AuthnStatement AuthnInstant="2023-01-24T05:05:45.547Z" SessionNotOnOrAfter="2024-07-17T09:01:48Z" SessionIndex="_be9967abd904ddcae3c0eb4189adbe3f71e327cf93">
+      <saml:AuthnContext>
+        <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef>
+      </saml:AuthnContext>
+    </saml:AuthnStatement>
+    <saml:AttributeStatement>
+      <saml:Attribute Name="uid" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
+        <saml:AttributeValue xsi:type="xs:string">test</saml:AttributeValue>
+      </saml:Attribute>
+      <saml:Attribute Name="mail" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
+        <saml:AttributeValue xsi:type="xs:string">test@example.com</saml:AttributeValue>
+      </saml:Attribute>
+      <saml:Attribute Name="eduPersonAffiliation" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
+        <saml:AttributeValue xsi:type="xs:string">users</saml:AttributeValue>
+        <saml:AttributeValue xsi:type="xs:string">examplerole1</saml:AttributeValue>
+      </saml:Attribute>
+    </saml:AttributeStatement>
+  </saml:Assertion>
+</samlp:Response>
diff --git a/test/xml/saml_verify.t.js b/test/xml/saml_verify.t.js
new file mode 100644 (file)
index 0000000..9a2d98a
--- /dev/null
@@ -0,0 +1,228 @@
+/*---
+includes: [compatFs.js, compatXml.js, compatWebcrypto.js, compatNjs.js, runTsuite.js]
+flags: [async]
+---*/
+
+async function verify(params) {
+    let file_data = fs.readFileSync(`test/xml/${params.saml}`);
+    let key_data = fs.readFileSync(`test/webcrypto/${params.key.file}`);
+    let saml = xml.parse(file_data);
+
+    let r = await verifySAMLSignature(saml, key_data)
+                .catch (e => {
+                    if (e.toString().startsWith("Error: EVP_PKEY_CTX_set_signature_md() failed")) {
+                        /* Red Hat Enterprise Linux: SHA-1 is disabled */
+                        return "SKIPPED";
+                    }
+                });
+
+    if (r == "SKIPPED") {
+        return r;
+    }
+
+    if (params.expected !== r) {
+        throw Error(`VERIFY ${params.saml} with key ${params.key.file} failed expected: "${params.expected}" vs "${r}"`);
+    }
+
+    return 'SUCCESS';
+}
+
+/*
+ * verifySAMLSignature() implements a verify clause
+ * from Profiles for the OASIS SAML V2.0
+ * 4.1.4.3 <Response> Message Processing Rules
+ *  Verify any signatures present on the assertion(s) or the response
+ *
+ * verification is done in accordance with
+ * Assertions and Protocols for the OASIS SAML V2.0
+ * 5.4 XML Signature Profile
+ *
+ * The following signature algorithms are supported:
+ * - http://www.w3.org/2001/04/xmldsig-more#rsa-sha256
+ * - http://www.w3.org/2000/09/xmldsig#rsa-sha1
+ *
+ * The following digest algorithms are supported:
+ * - http://www.w3.org/2000/09/xmldsig#sha1
+ * - http://www.w3.org/2001/04/xmlenc#sha256
+ *
+ * @param doc an XMLDoc object returned by xml.parse().
+ * @param key_data is SubjectPublicKeyInfo in PEM format.
+ */
+
+async function verifySAMLSignature(saml, key_data) {
+    const root = saml.$root;
+    const rootSignature = root.Signature;
+
+    if (!rootSignature) {
+        throw Error(`SAML message is unsigned`);
+    }
+
+    const assertion = root.Assertion;
+    const assertionSignature = assertion ? assertion.Signature : null;
+
+    if (assertionSignature) {
+        if (!await verifyDigest(assertionSignature)) {
+            return false;
+        }
+
+        if (!await verifySignature(assertionSignature, key_data)) {
+            return false;
+        }
+    }
+
+    if (rootSignature) {
+        if (!await verifyDigest(rootSignature)) {
+            return false;
+        }
+
+        if (!await verifySignature(rootSignature, key_data)) {
+            return false;
+        }
+    }
+
+    return true;
+}
+
+async function verifyDigest(signature) {
+    const parent = signature.$parent;
+    const signedInfo = signature.SignedInfo;
+    const reference = signedInfo.Reference;
+
+    /* Sanity check. */
+
+    const URI = reference.$attr$URI;
+    const ID = parent.$attr$ID;
+
+    if (URI != `#${ID}`) {
+        throw Error(`signed reference URI ${URI} does not point to the parent ${ID}`);
+    }
+
+    /*
+     * Assertions and Protocols for the OASIS SAML V2.0
+     * 5.4.4 Transforms
+     *
+     * Signatures in SAML messages SHOULD NOT contain transforms other than
+     * the http://www.w3.org/2000/09/xmldsig#enveloped-signature and
+     * canonicalization transforms http://www.w3.org/2001/10/xml-exc-c14n# or
+     * http://www.w3.org/2001/10/xml-exc-c14n#WithComments.
+     */
+
+    const transforms = reference.Transforms.$tags$Transform;
+    const transformAlgs = transforms.map(t => t.$attr$Algorithm);
+
+    if (transformAlgs[0] != 'http://www.w3.org/2000/09/xmldsig#enveloped-signature') {
+        throw Error(`unexpected digest transform ${transforms[0]}`);
+    }
+
+    if (!transformAlgs[1].startsWith('http://www.w3.org/2001/10/xml-exc-c14n#')) {
+        throw Error(`unexpected digest transform ${transforms[1]}`);
+    }
+
+    const namespaces = transformAlgs[1].InclusiveNamespaces;
+    const prefixList = namespaces ? namespaces.$attr$PrefixList: null;
+
+    const withComments = transformAlgs[1].slice(39) == 'WithComments';
+
+    let hash;
+    const alg = reference.DigestMethod.$attr$Algorithm;
+
+    switch (alg) {
+    case "http://www.w3.org/2000/09/xmldsig#sha1":
+        hash = "SHA-1";
+        break;
+    case "http://www.w3.org/2001/04/xmlenc#sha256":
+        hash = "SHA-256";
+        break;
+    default:
+        throw Error(`unexpected digest Algorithm ${alg}`);
+    }
+
+    const expectedDigest = signedInfo.Reference.DigestValue.$text;
+
+    const c14n = xml.exclusiveC14n(parent, signature, withComments, prefixList);
+    const dgst = await crypto.subtle.digest(hash, c14n);
+    const b64dgst = Buffer.from(dgst).toString('base64');
+
+    return expectedDigest === b64dgst;
+}
+
+function keyPem2Der(pem, type) {
+    const pemJoined = pem.toString().split('\n').join('');
+    const pemHeader = `-----BEGIN ${type} KEY-----`;
+    const pemFooter = `-----END ${type} KEY-----`;
+    const pemContents = pemJoined.substring(pemHeader.length, pemJoined.length - pemFooter.length);
+    return Buffer.from(pemContents, 'base64');
+}
+
+function base64decode(b64) {
+    const joined = b64.toString().split('\n').join('');
+    return Buffer.from(joined, 'base64');
+}
+
+async function verifySignature(signature, key_data) {
+    const der = keyPem2Der(key_data, "PUBLIC");
+
+    let method, hash;
+    const signedInfo = signature.SignedInfo;
+    const alg = signedInfo.SignatureMethod.$attr$Algorithm;
+
+    switch (alg) {
+    case "http://www.w3.org/2000/09/xmldsig#rsa-sha1":
+        method = "RSASSA-PKCS1-v1_5";
+        hash = "SHA-1";
+        break;
+    case "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256":
+        method = "RSASSA-PKCS1-v1_5";
+        hash = "SHA-256";
+        break;
+    default:
+        throw Error(`unexpected signature Algorithm ${alg}`);
+    }
+
+    const expectedValue = base64decode(signature.SignatureValue.$text);
+    const withComments = signedInfo.CanonicalizationMethod
+                         .$attr$Algorithm.slice(39) == 'WithComments';
+
+    const signedInfoC14n = xml.exclusiveC14n(signedInfo, null, withComments);
+
+    const key = await crypto.subtle.importKey("spki", der, { name: method, hash },
+                                            false, [ "verify" ]);
+
+    return await crypto.subtle.verify({ name: method }, key, expectedValue,
+                                      signedInfoC14n);
+}
+
+function p(args, default_opts) {
+    let params = merge({}, default_opts);
+    params = merge(params, args);
+
+    return params;
+}
+
+let saml_verify_tsuite = {
+    name: "SAML verify",
+    skip: () => (!has_njs() || !has_fs() || !has_webcrypto()),
+    T: verify,
+    prepare_args: p,
+    opts: {
+        key: { fmt: "spki", file: "rsa.spki" },
+    },
+
+    tests: [
+        { saml: "auth_r_signed.xml", expected: true },
+        { saml: "auth_r_with_comments_signed.xml", expected: true },
+        { saml: "auth_r_prefix_list_signed.xml", expected: true },
+        { saml: "auth_r_signed2.xml", expected: false },
+        { saml: "auth_r_signed.xml", key: { file: "rsa2.spki"}, expected: false },
+        { saml: "auth_r_signed2.xml", key: { file: "rsa2.spki"}, expected: true },
+        { saml: "response_signed.xml", expected: true },
+        { saml: "response_signed_broken.xml", expected: false },
+        { saml: "response_signed_broken2.xml", expected: false },
+        { saml: "response_signed.xml", key: { file: "rsa2.spki"}, expected: false },
+        { saml: "response_assertion_and_message_signed.xml", expected: true },
+]};
+
+run([
+    saml_verify_tsuite,
+])
+.then($DONE, $DONE);