diff --git a/apps/upi_core/lib/upi_core/upi_xml_schema.ex b/apps/upi_core/lib/upi_core/upi_xml_schema.ex
index b890eea..d963d22 100644
--- a/apps/upi_core/lib/upi_core/upi_xml_schema.ex
+++ b/apps/upi_core/lib/upi_core/upi_xml_schema.ex
@@ -905,61 +905,80 @@ defmodule UpiCore.UpiXmlSchema do
     {:ok, xml}
 end
 
+# [Signature SignedInfo / canonicalization / key path] OLD FUNCTION kept for reference. It signed the whole document string instead of SignedInfo (:public_key.sign(canonical_xml, ...)), used the regex whitespace stripping as 'canonicalization'
+# while declaring c14n 20010315 (inclusive), computed DigestValue over that string with the Signature still absent only by accident, and fell back to the hard-coded /var/www/internaltesting/madhoolika/.../private.pem.
+# [Signature Canonicalization] CHANGED (2nd revision): inclusive C14N 1.0 (UpiCore.XmlDsig.c14n/1, http://www.w3.org/TR/2001/REC-xml-c14n-20010315) - the URI the TSD §7.2.5 sample declares - for the Reference digest and SignedInfo.
+# OLD: # [Signature Canonicalization] CHANGED: Exclusive C14N (UpiCore.XmlDsig.exc_c14n/1, http://www.w3.org/2001/10/xml-exc-c14n#) for both the Reference digest and SignedInfo.
+# [Signature Canonicalization] the canonicalization used equals the declared CanonicalizationMethod URI.
+# [Signature SignedInfo] CHANGED: SignatureValue is RSA-SHA256 over the canonical <SignedInfo>; DigestValue is the SHA-256 of the canonical document without the Signature (enveloped-signature transform).
+# [Signature key path] CHANGED: from config :upi_core, :xml_signing_priv_key_path (env XML_SIGNING_PRIV_KEY_PATH); the /var/www/... default is gone, a missing path raises a clear error.
+# [Signature TSD] CHANGED: sample-conform SignedInfo (c14n-20010315, rsa-sha256, one enveloped-signature Transform, xmlenc#sha256, KeyInfo RSAKeyValue)
+# OLD: # TSD v16 §7.2.5: XMLDSig, Exclusive C14N, RSA-SHA256, signature over SignedInfo
+# TSD v16 §7.2.5: XMLDSig, inclusive C14N, RSA-SHA256, signature over SignedInfo
+# OLD: defp generate_signature_block(xml_body) do
+# OLD:   # Implements actual XML digital signature generation using RSA private key.
+# OLD:   # Uses :crypto for digest and signature, assumes PEM private key is loaded from config.
+# OLD:   # Returns XML Signature block as per XMLDSIG standard.
+# OLD:
+# OLD:   # Load private key from config (PEM format)
+# OLD:   priv_key_path = Application.get_env(:upi_core, :xml_signing_priv_key_path, "/var/www/internaltesting/madhoolika/prverification/upi_psp_platform/private.pem")
+# OLD:   priv_key_pem = File.read!(priv_key_path)
+# OLD:   [entry] = :public_key.pem_decode(priv_key_pem)
+# OLD:   priv_key = :public_key.pem_entry_decode(entry)
+# OLD:
+# OLD:   # Improved canonicalization for NPCI compatibility
+# OLD:   canonical_xml =
+# OLD:     xml_body
+# OLD:     |> String.replace(~r/>\s+</, "><")  # Remove whitespace between tags
+# OLD:     |> String.replace(~r/\s+/, " ")     # Normalize multiple spaces to single space
+# OLD:     |> String.replace(~r/\s+>/, ">")    # Remove trailing spaces before closing tags
+# OLD:     |> String.replace(~r/<\s+/, "<")    # Remove leading spaces after opening tags
+# OLD:     |> String.trim()
+# OLD:
+# OLD:   # Compute SHA256 digest of canonicalized XML
+# OLD:   digest = :crypto.hash(:sha256, canonical_xml) |> Base.encode64()
+# OLD:
+# OLD:   # Sign the digest using RSA private key (PKCS1 v1.5)
+# OLD:   signature = :public_key.sign(canonical_xml, :sha256, priv_key) |> Base.encode64()
+# OLD:
+# OLD:   # Extract modulus and exponent for KeyInfo
+# OLD:   {:RSAPrivateKey, :"two-prime", modulus, public_exponent, _, _, _, _, _, _, _} = priv_key
+# OLD:   modulus_b64 = :binary.encode_unsigned(modulus) |> Base.encode64()
+# OLD:   exponent_b64 = :binary.encode_unsigned(public_exponent) |> Base.encode64()
+# OLD:
+# OLD:   """
+# OLD:   <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
+# OLD:       <SignedInfo>
+# OLD:           <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
+# OLD:           <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
+# OLD:           <Reference URI="">
+# OLD:               <Transforms>
+# OLD:                   <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
+# OLD:               </Transforms>
+# OLD:               <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
+# OLD:               <DigestValue>#{digest}</DigestValue>
+# OLD:           </Reference>
+# OLD:       </SignedInfo>
+# OLD:       <SignatureValue>#{signature}</SignatureValue>
+# OLD:       <KeyInfo>
+# OLD:           <KeyValue>
+# OLD:               <RSAKeyValue>
+# OLD:                   <Modulus>#{modulus_b64}</Modulus>
+# OLD:                   <Exponent>#{exponent_b64}</Exponent>
+# OLD:               </RSAKeyValue>
+# OLD:           </KeyValue>
+# OLD:       </KeyInfo>
+# OLD:   </Signature>
+# OLD:   """
+# OLD: end
 defp generate_signature_block(xml_body) do
-  # Implements actual XML digital signature generation using RSA private key.
-  # Uses :crypto for digest and signature, assumes PEM private key is loaded from config.
-  # Returns XML Signature block as per XMLDSIG standard.
-
-  # Load private key from config (PEM format)
-  priv_key_path = Application.get_env(:upi_core, :xml_signing_priv_key_path, "/var/www/internaltesting/madhoolika/prverification/upi_psp_platform/private.pem")
-  priv_key_pem = File.read!(priv_key_path)
-  [entry] = :public_key.pem_decode(priv_key_pem)
-  priv_key = :public_key.pem_entry_decode(entry)
-
-  # Improved canonicalization for NPCI compatibility
-  canonical_xml =
-    xml_body
-    |> String.replace(~r/>\s+</, "><")  # Remove whitespace between tags
-    |> String.replace(~r/\s+/, " ")     # Normalize multiple spaces to single space
-    |> String.replace(~r/\s+>/, ">")    # Remove trailing spaces before closing tags
-    |> String.replace(~r/<\s+/, "<")    # Remove leading spaces after opening tags
-    |> String.trim()
-
-  # Compute SHA256 digest of canonicalized XML
-  digest = :crypto.hash(:sha256, canonical_xml) |> Base.encode64()
+  priv_key = UpiCore.XmlDsig.load_private_key!(Application.get_env(:upi_core, :xml_signing_priv_key_path))
 
-  # Sign the digest using RSA private key (PKCS1 v1.5)
-  signature = :public_key.sign(canonical_xml, :sha256, priv_key) |> Base.encode64()
+  # the document as it is after the enveloped-signature transform: generate_resp_pay builds
+  # xml_body <> "\n" <> signature_block <> "\n</upi:RespPay>", so with the Signature removed it is this
+  unsigned_document = xml_body <> "\n" <> "\n</upi:RespPay>"
 
-  # Extract modulus and exponent for KeyInfo
-  {:RSAPrivateKey, :"two-prime", modulus, public_exponent, _, _, _, _, _, _, _} = priv_key
-  modulus_b64 = :binary.encode_unsigned(modulus) |> Base.encode64()
-  exponent_b64 = :binary.encode_unsigned(public_exponent) |> Base.encode64()
-
-  """
-  <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
-      <SignedInfo>
-          <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
-          <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
-          <Reference URI="">
-              <Transforms>
-                  <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
-              </Transforms>
-              <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
-              <DigestValue>#{digest}</DigestValue>
-          </Reference>
-      </SignedInfo>
-      <SignatureValue>#{signature}</SignatureValue>
-      <KeyInfo>
-          <KeyValue>
-              <RSAKeyValue>
-                  <Modulus>#{modulus_b64}</Modulus>
-                  <Exponent>#{exponent_b64}</Exponent>
-              </RSAKeyValue>
-          </KeyValue>
-      </KeyInfo>
-  </Signature>
-  """
+  UpiCore.XmlDsig.signature_block(unsigned_document, priv_key)
 end
   @doc """
   Generate International Credit Request (ReqPay) XML
diff --git a/apps/upi_core/lib/upi_core/xml_dsig.ex b/apps/upi_core/lib/upi_core/xml_dsig.ex
new file mode 100644
index 0000000..8bb8ff0
--- /dev/null
+++ b/apps/upi_core/lib/upi_core/xml_dsig.ex
@@ -0,0 +1,290 @@
+defmodule UpiCore.XmlDsig do
+  @moduledoc """
+  XML digital signature (XMLDSig, enveloped) for the messages we send to NPCI (TSD v16 §7.2.5).
+
+    * `exc_c14n/1` - Exclusive XML Canonicalization (http://www.w3.org/2001/10/xml-exc-c14n#, without comments)
+    * `signature_block/2` - builds a `<Signature>` element for an enveloped signature:
+        1. `DigestValue` = SHA-256 of the exclusive C14N form of the document without its `<Signature>`
+           (what the `enveloped-signature` + `xml-exc-c14n` transforms of the Reference produce)
+        2. `SignatureValue` = RSA-SHA256 (PKCS#1 v1.5) over the exclusive C14N form of the `<SignedInfo>` element
+    * `load_private_key!/1` - reads the PEM private key from the configured path
+
+  The canonicalizer works on the documents this application generates: one root element, elements, attributes,
+  text, namespace declarations; comments are dropped, processing instructions are kept; no DTD.
+  """
+
+  require Record
+
+  Record.defrecordp(:xml_element, :xmlElement, Record.extract(:xmlElement, from_lib: "xmerl/include/xmerl.hrl"))
+  Record.defrecordp(:xml_attribute, :xmlAttribute, Record.extract(:xmlAttribute, from_lib: "xmerl/include/xmerl.hrl"))
+  Record.defrecordp(:xml_text, :xmlText, Record.extract(:xmlText, from_lib: "xmerl/include/xmerl.hrl"))
+  Record.defrecordp(:xml_pi, :xmlPI, Record.extract(:xmlPI, from_lib: "xmerl/include/xmerl.hrl"))
+
+  @dsig_ns "http://www.w3.org/2000/09/xmldsig#"
+  # [Signature CanonicalizationMethod] CHANGED: TSD §7.2.5 sample declares inclusive C14N 1.0; the canonicalization used (c14n/1) is inclusive, so the declared URI matches it
+  # OLD:   @c14n_alg "http://www.w3.org/2001/10/xml-exc-c14n#"
+  @c14n_alg "http://www.w3.org/TR/2001/REC-xml-c14n-20010315"
+  @sig_alg "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
+  @digest_alg "http://www.w3.org/2001/04/xmlenc#sha256"
+  @enveloped_alg "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
+  @xml_ns "http://www.w3.org/XML/1998/namespace"
+
+  # ------------------------------------------------------------------
+  # Key
+  # ------------------------------------------------------------------
+
+  @doc """
+  Reads the RSA private key (PEM) from `path`. Raises with a clear message when the path is not configured.
+  """
+  def load_private_key!(path) when path in [nil, ""] do
+    raise ArgumentError,
+          "XML signing key path is not configured: set the XML_SIGNING_PRIV_KEY_PATH environment variable " <>
+            "(config :upi_core, :xml_signing_priv_key_path)"
+  end
+
+  def load_private_key!(path) when is_binary(path) do
+    [entry | _] = path |> File.read!() |> :public_key.pem_decode()
+    :public_key.pem_entry_decode(entry)
+  end
+
+  # ------------------------------------------------------------------
+  # Signature
+  # ------------------------------------------------------------------
+
+  @doc """
+  Builds the `<Signature>` element for `unsigned_document` (the full document, already containing everything that is
+  signed but WITHOUT the signature). `private_key` is an `:RSAPrivateKey` record.
+  """
+  @spec signature_block(binary(), tuple()) :: binary()
+  def signature_block(unsigned_document, private_key) when is_binary(unsigned_document) do
+    # [Signature DigestValue] CHANGED: SHA-256 of the inclusive C14N of the document without the Signature (enveloped-signature is the only Transform, canonicalized with CanonicalizationMethod)
+    # OLD:     digest = :crypto.hash(:sha256, exc_c14n(unsigned_document)) |> Base.encode64()
+    digest = :crypto.hash(:sha256, c14n(unsigned_document)) |> Base.encode64()
+
+    signed_info = signed_info(digest)
+
+    # [Signature SignedInfo] CHANGED: inclusive C14N renders every in-scope namespace on the SignedInfo apex, so the namespaces of the document root (xmlns:upi) are added next to the Signature's default xmlns
+    # OLD:     # SignedInfo is canonicalized in the context of the Signature element, which declares the default namespace
+    # OLD:     canonical_signed_info = exc_c14n(~s(<SignedInfo xmlns="#{@dsig_ns}">) <> signed_info <> "</SignedInfo>")
+    ns = unsigned_document |> root_namespaces() |> Map.put("", @dsig_ns)
+
+    ns_decls =
+      Enum.map_join(ns, fn
+        {"", uri} -> ~s( xmlns="#{uri}")
+        {prefix, uri} -> ~s( xmlns:#{prefix}="#{uri}")
+      end)
+
+    canonical_signed_info = c14n("<SignedInfo" <> ns_decls <> ">" <> signed_info <> "</SignedInfo>")
+    signature = :public_key.sign(canonical_signed_info, :sha256, private_key) |> Base.encode64()
+
+    {:RSAPrivateKey, _version, modulus, public_exponent, _, _, _, _, _, _, _} = private_key
+    modulus_b64 = modulus |> :binary.encode_unsigned() |> Base.encode64()
+    exponent_b64 = public_exponent |> :binary.encode_unsigned() |> Base.encode64()
+
+    # SignedInfo is written without any insignificant whitespace: its text nodes are part of what is signed
+    """
+    <Signature xmlns="#{@dsig_ns}">
+    <SignedInfo>#{signed_info}</SignedInfo>
+    <SignatureValue>#{signature}</SignatureValue>
+    <KeyInfo>
+    <KeyValue>
+    <RSAKeyValue>
+    <Modulus>#{modulus_b64}</Modulus>
+    <Exponent>#{exponent_b64}</Exponent>
+    </RSAKeyValue>
+    </KeyValue>
+    </KeyInfo>
+    </Signature>
+    """
+    |> String.trim_trailing()
+  end
+
+  defp signed_info(digest_b64) do
+    ~s(<CanonicalizationMethod Algorithm="#{@c14n_alg}"/>) <>
+      ~s(<SignatureMethod Algorithm="#{@sig_alg}"/>) <>
+      ~s(<Reference URI="">) <>
+      ~s(<Transforms>) <>
+      ~s(<Transform Algorithm="#{@enveloped_alg}"/>) <>
+      # [Signature Transform] REMOVED: second Transform (exclusive c14n); TSD §7.2.5 sample has only the enveloped-signature Transform
+      # OLD:       ~s(<Transform Algorithm="#{@c14n_alg}"/>) <>
+      ~s(</Transforms>) <>
+      ~s(<DigestMethod Algorithm="#{@digest_alg}"/>) <>
+      ~s(<DigestValue>#{digest_b64}</DigestValue>) <>
+      ~s(</Reference>)
+  end
+
+  # ------------------------------------------------------------------
+  # Exclusive XML Canonicalization (no comments, no InclusiveNamespaces PrefixList)
+  # ------------------------------------------------------------------
+
+  @doc """
+  Exclusive XML Canonicalization of a whole XML document string. The XML declaration and comments are dropped (processing
+  instructions are kept); empty elements are written as start + end tag; namespace declarations are only written
+  where visibly utilized; attributes are sorted by namespace URI then local name.
+  """
+  @spec exc_c14n(binary()) :: binary()
+  def exc_c14n(xml) when is_binary(xml) do
+    {root, _rest} =
+      xml
+      |> String.to_charlist()
+      |> :xmerl_scan.string(namespace_conformant: true, space: :preserve, quiet: true)
+
+    root
+    |> render_element(%{"xml" => @xml_ns}, %{})
+    |> IO.iodata_to_binary()
+  end
+
+  @doc """
+  Inclusive XML Canonicalization (Canonical XML 1.0, http://www.w3.org/TR/2001/REC-xml-c14n-20010315, no comments) of a
+  whole XML document string: every in-scope namespace declaration is written on the first element that sees it.
+  """
+  @spec c14n(binary()) :: binary()
+  def c14n(xml) when is_binary(xml) do
+    {root, _rest} =
+      xml
+      |> String.to_charlist()
+      |> :xmerl_scan.string(namespace_conformant: true, space: :preserve, quiet: true)
+
+    root
+    |> render_element(%{"xml" => @xml_ns, :mode => :inclusive}, %{})
+    |> IO.iodata_to_binary()
+  end
+
+  @doc """
+  Namespace declarations (prefix => uri, "" = default) written on the root element of `xml`.
+  """
+  @spec root_namespaces(binary()) :: %{optional(binary()) => binary()}
+  def root_namespaces(xml) when is_binary(xml) do
+    {xml_element(attributes: attributes), _rest} =
+      xml
+      |> String.to_charlist()
+      |> :xmerl_scan.string(namespace_conformant: true, space: :preserve, quiet: true)
+
+    {decls, _regular} = split_attributes(attributes)
+    Map.new(decls)
+  end
+
+  defp render_element(xml_element(name: name, nsinfo: nsinfo, attributes: attributes, content: content), in_scope, rendered) do
+    qname = Atom.to_string(name)
+    {decls, regular} = split_attributes(attributes)
+    in_scope = Enum.reduce(decls, in_scope, fn {prefix, uri}, acc -> Map.put(acc, prefix, uri) end)
+
+    element_prefix = prefix_of(nsinfo)
+
+    # [Signature Canonicalization] CHANGED: inclusive mode (in_scope[:mode]) treats every in-scope prefix as utilized; exclusive mode keeps the visibly-utilized rule
+    # OLD:     utilized =
+    # OLD:       [element_prefix | for({_name, attr_nsinfo, _value} <- regular, prefix_of(attr_nsinfo) != "", do: prefix_of(attr_nsinfo))]
+    # OLD:       |> Enum.uniq()
+    utilized =
+      case Map.get(in_scope, :mode) do
+        :inclusive ->
+          Enum.uniq([element_prefix, "" | for(key <- Map.keys(in_scope), is_binary(key), do: key)])
+
+        _exclusive ->
+          [element_prefix | for({_name, attr_nsinfo, _value} <- regular, prefix_of(attr_nsinfo) != "", do: prefix_of(attr_nsinfo))]
+          |> Enum.uniq()
+      end
+
+    {ns_out, rendered} =
+      utilized
+      |> Enum.sort()
+      |> Enum.reduce({[], rendered}, fn prefix, {out, rendered_acc} ->
+        uri = Map.get(in_scope, prefix)
+        already = Map.get(rendered_acc, prefix)
+
+        cond do
+          prefix == "xml" ->
+            {out, rendered_acc}
+
+          prefix == "" and uri in [nil, ""] ->
+            if already in [nil, ""],
+              do: {out, rendered_acc},
+              else: {[{"", ""} | out], Map.put(rendered_acc, "", "")}
+
+          uri == nil ->
+            {out, rendered_acc}
+
+          already == uri ->
+            {out, rendered_acc}
+
+          true ->
+            {[{prefix, uri} | out], Map.put(rendered_acc, prefix, uri)}
+        end
+      end)
+
+    ns_iodata =
+      ns_out
+      |> Enum.sort()
+      |> Enum.map(fn
+        {"", uri} -> [" xmlns=\"", escape_attr(uri), "\""]
+        {prefix, uri} -> [" xmlns:", prefix, "=\"", escape_attr(uri), "\""]
+      end)
+
+    attr_iodata =
+      regular
+      |> Enum.map(fn {attr_name, attr_nsinfo, value} ->
+        attr_prefix = prefix_of(attr_nsinfo)
+        attr_uri = if attr_prefix == "", do: "", else: Map.get(in_scope, attr_prefix, "")
+        {{attr_uri, local_of(attr_nsinfo, attr_name)}, attr_name, value}
+      end)
+      |> Enum.sort_by(fn {key, _attr_name, _value} -> key end)
+      |> Enum.map(fn {_key, attr_name, value} -> [" ", attr_name, "=\"", escape_attr(value), "\""] end)
+
+    children = Enum.map(content, &render_node(&1, in_scope, rendered))
+
+    ["<", qname, ns_iodata, attr_iodata, ">", children, "</", qname, ">"]
+  end
+
+  defp render_node(xml_element() = element, in_scope, rendered), do: render_element(element, in_scope, rendered)
+  defp render_node(xml_text(value: value), _in_scope, _rendered), do: escape_text(to_utf8(value))
+  defp render_node(xml_pi(name: name, value: value), _in_scope, _rendered) do
+    case to_utf8(value) do
+      "" -> ["<?", Atom.to_string(name), "?>"]
+      data -> ["<?", Atom.to_string(name), " ", data, "?>"]
+    end
+  end
+
+  # comments are not part of the canonical form
+  defp render_node(_other, _in_scope, _rendered), do: []
+
+  defp split_attributes(attributes) do
+    Enum.reduce(attributes, {[], []}, fn attr, {decls, regular} ->
+      attr_name = attr |> xml_attribute(:name) |> Atom.to_string()
+      value = attr |> xml_attribute(:value) |> to_utf8()
+
+      cond do
+        attr_name == "xmlns" -> {[{"", value} | decls], regular}
+        String.starts_with?(attr_name, "xmlns:") -> {[{String.replace_prefix(attr_name, "xmlns:", ""), value} | decls], regular}
+        true -> {decls, [{attr_name, xml_attribute(attr, :nsinfo), value} | regular]}
+      end
+    end)
+    |> then(fn {decls, regular} -> {Enum.reverse(decls), Enum.reverse(regular)} end)
+  end
+
+  defp prefix_of([]), do: ""
+  defp prefix_of({prefix, _local}), do: to_utf8(prefix)
+
+  defp local_of({_prefix, local}, _attr_name), do: to_utf8(local)
+  defp local_of([], attr_name), do: attr_name
+
+  defp to_utf8(value) when is_binary(value), do: value
+  defp to_utf8(value), do: :unicode.characters_to_binary(value)
+
+  defp escape_text(text) do
+    text
+    |> String.replace("&", "&amp;")
+    |> String.replace("<", "&lt;")
+    |> String.replace(">", "&gt;")
+    |> String.replace("\r", "&#xD;")
+  end
+
+  defp escape_attr(text) do
+    text
+    |> String.replace("&", "&amp;")
+    |> String.replace("<", "&lt;")
+    |> String.replace("\"", "&quot;")
+    |> String.replace("\t", "&#x9;")
+    |> String.replace("\n", "&#xA;")
+    |> String.replace("\r", "&#xD;")
+  end
+end
diff --git a/apps/upi_core/test/upi_core/xml_dsig_test.exs b/apps/upi_core/test/upi_core/xml_dsig_test.exs
new file mode 100644
index 0000000..4b9850e
--- /dev/null
+++ b/apps/upi_core/test/upi_core/xml_dsig_test.exs
@@ -0,0 +1,182 @@
+defmodule UpiCore.XmlDsigTest do
+  use ExUnit.Case, async: false
+
+  import SweetXml
+
+  alias UpiCore.{UpiXmlSchema, XmlDsig}
+
+  @key_path Path.expand("../../../../private.pem", __DIR__)
+
+  # Expected values below were produced by an independent implementation (lxml / libxml2,
+  # etree.tostring(method="c14n", exclusive=True)).
+  describe "exc_c14n/1" do
+    test "drops unused namespace declarations (W3C exclusive c14n example)" do
+      xml = """
+      <n0:local xmlns:n0="foo:bar" xmlns:n3="ftp://example.org">
+        <n1:elem2 xmlns:n1="http://example.net" xml:lang="en">
+          <n3:stuff xmlns:n3="ftp://example.org"/>
+        </n1:elem2>
+      </n0:local>
+      """
+
+      assert XmlDsig.exc_c14n(xml) ==
+               ~s(<n0:local xmlns:n0="foo:bar">\n  <n1:elem2 xmlns:n1="http://example.net" xml:lang="en">\n    <n3:stuff xmlns:n3="ftp://example.org"></n3:stuff>\n  </n1:elem2>\n</n0:local>)
+    end
+
+    test "sorts attributes, escapes text and attribute values, expands empty elements, drops declaration and comments, keeps PIs" do
+      xml = ~s(<?xml version="1.0" encoding="UTF-8"?>\n<!-- comment -->\n<a z="1" b="x &amp; &lt; &quot; y" a="2"><b/><c>t &amp; &lt; &gt; "q"</c><?pi x?></a>)
+
+      assert XmlDsig.exc_c14n(xml) ==
+               ~s(<a a="2" b="x &amp; &lt; &quot; y" z="1"><b></b><c>t &amp; &lt; &gt; "q"</c><?pi x?></a>)
+    end
+
+    test "default and prefixed namespaces: declared once where first used, attributes sorted by namespace then name" do
+      xml =
+        ~s(<root xmlns="urn:one" xmlns:p="urn:two" p:z="1" a="2"><child xmlns="urn:one"><p:leaf p:y="3" x="4"/></child><other xmlns="urn:three"/></root>)
+
+      assert XmlDsig.exc_c14n(xml) ==
+               ~s(<root xmlns="urn:one" xmlns:p="urn:two" a="2" p:z="1"><child><p:leaf x="4" p:y="3"></p:leaf></child><other xmlns="urn:three"></other></root>)
+    end
+
+    test "undeclaring the default namespace" do
+      assert XmlDsig.exc_c14n(~s(<a xmlns="urn:one"><b xmlns=""><c/></b></a>)) ==
+               ~s(<a xmlns="urn:one"><b xmlns=""><c></c></b></a>)
+    end
+
+    test "whitespace is not 'normalized' (unlike the old regex canonicalization)" do
+      assert XmlDsig.exc_c14n("<a>\n  <b>  x   y  </b>\n</a>") == "<a>\n  <b>  x   y  </b>\n</a>"
+    end
+  end
+
+  describe "c14n/1 (inclusive, Canonical XML 1.0)" do
+    test "matches libxml2 on the same inputs" do
+      w3c = """
+      <n0:local xmlns:n0="foo:bar" xmlns:n3="ftp://example.org">
+        <n1:elem2 xmlns:n1="http://example.net" xml:lang="en">
+          <n3:stuff xmlns:n3="ftp://example.org"/>
+        </n1:elem2>
+      </n0:local>
+      """
+
+      assert XmlDsig.c14n(w3c) == ~s(<n0:local xmlns:n0="foo:bar" xmlns:n3="ftp://example.org">\n  <n1:elem2 xmlns:n1="http://example.net" xml:lang="en">\n    <n3:stuff></n3:stuff>\n  </n1:elem2>\n</n0:local>)
+      assert XmlDsig.c14n(~s(<root xmlns="urn:one" xmlns:p="urn:two" p:z="1" a="2"><child xmlns="urn:one"><p:leaf p:y="3" x="4"/></child><other xmlns="urn:three"/></root>)) == ~s(<root xmlns="urn:one" xmlns:p="urn:two" a="2" p:z="1"><child><p:leaf x="4" p:y="3"></p:leaf></child><other xmlns="urn:three"></other></root>)
+      assert XmlDsig.c14n(~s(<a xmlns="urn:one"><b xmlns=""><c/></b></a>)) == ~s(<a xmlns="urn:one"><b xmlns=""><c></c></b></a>)
+    end
+  end
+
+  describe "signature on RespPay" do
+    @req %{
+      msg_id: "NPCIREQPAY00000000000000000000001",
+      txn_id: "NPCITXN1",
+      note: ~s(N <&> "q"),
+      cust_ref: "CUST1",
+      ref_id: "R1",
+      org_txn_id: "O1",
+      ref_category: "00",
+      txn_type: "CREDIT",
+      purpose: "11",
+      sub_type: "PAY",
+      initiation_mode: "16",
+      txn_ts: "2026-07-09T14:46:31+05:30",
+      qr_ver: "01",
+      qr_ts: "2026-07-09T14:46:11+05:30",
+      qr_medium: "03",
+      expire_ts: "2026-07-09T15:16:11+05:30",
+      payee_addr: "restaurant@mercury",
+      payee_name: "Dubai & Fine <Dining>",
+      payee_code: "5812",
+      payee_ifsc: "TEST0000001",
+      payee_ac_num: "123",
+      payee_ac_type: "SAVINGS",
+      payee_amount: "5.00"
+    }
+
+    setup do
+      previous = Application.get_env(:upi_core, :xml_signing_priv_key_path)
+      Application.put_env(:upi_core, :xml_signing_priv_key_path, @key_path)
+
+      on_exit(fn ->
+        if previous,
+          do: Application.put_env(:upi_core, :xml_signing_priv_key_path, previous),
+          else: Application.delete_env(:upi_core, :xml_signing_priv_key_path)
+      end)
+
+      {:ok, xml} =
+        UpiXmlSchema.generate_resp_pay(%{org_id: "MER101", req_msg_id: @req.msg_id, result: "SUCCESS", resp_code: "00", request_data: @req})
+
+      %{xml: xml, key: XmlDsig.load_private_key!(@key_path)}
+    end
+
+    defp public_key({:RSAPrivateKey, _, modulus, exponent, _, _, _, _, _, _, _}), do: {:RSAPublicKey, modulus, exponent}
+
+    defp signature_pieces(xml) do
+      [_, rest] = String.split(xml, "<Signature", parts: 2)
+      signature_xml = "<Signature" <> rest |> String.replace(~r/\n<\/upi:RespPay>\s*$/, "")
+
+      [_, si_inner] = Regex.run(~r|<SignedInfo>(.*)</SignedInfo>|s, signature_xml)
+      [_, sig_b64] = Regex.run(~r|<SignatureValue>([^<]+)</SignatureValue>|, signature_xml)
+      [_, digest_b64] = Regex.run(~r|<DigestValue>([^<]+)</DigestValue>|, signature_xml)
+
+      unsigned = String.replace(xml, signature_xml, "")
+      {si_inner, Base.decode64!(sig_b64), digest_b64, unsigned}
+    end
+
+    test "SignatureMethod / DigestMethod / CanonicalizationMethod / Transforms", %{xml: xml} do
+      assert attr(xml, "//*[local-name()='CanonicalizationMethod']/@Algorithm") == "http://www.w3.org/TR/2001/REC-xml-c14n-20010315"
+      assert attr(xml, "//*[local-name()='SignatureMethod']/@Algorithm") == "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
+      assert attr(xml, "//*[local-name()='DigestMethod']/@Algorithm") == "http://www.w3.org/2001/04/xmlenc#sha256"
+      assert attr(xml, "//*[local-name()='Reference']/@URI") == ""
+      assert attr(xml, "count(//*[local-name()='Transform'])") == "1"
+      assert attr(xml, "//*[local-name()='Transform']/@Algorithm") == "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
+      assert attr(xml, "count(//*[local-name()='KeyInfo']/*[local-name()='KeyValue']/*[local-name()='RSAKeyValue']/*)") == "2"
+    end
+
+    test "SignatureValue is RSA-SHA256 over the canonical <SignedInfo>", %{xml: xml, key: key} do
+      {si_inner, signature, _digest, _unsigned} = signature_pieces(xml)
+      canonical_si = XmlDsig.c14n(~s(<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#" xmlns:upi="http://npci.org/upi/schema/">) <> si_inner <> "</SignedInfo>")
+
+      assert :public_key.verify(canonical_si, :sha256, signature, public_key(key))
+    end
+
+    test "SignatureValue does NOT verify over the document (the old behaviour)", %{xml: xml, key: key} do
+      {_si, signature, _digest, unsigned} = signature_pieces(xml)
+      refute :public_key.verify(XmlDsig.c14n(unsigned), :sha256, signature, public_key(key))
+      refute :public_key.verify(unsigned, :sha256, signature, public_key(key))
+    end
+
+    test "DigestValue is SHA-256 of the exclusive c14n form of the document without the Signature", %{xml: xml} do
+      {_si, _signature, digest_b64, unsigned} = signature_pieces(xml)
+      assert digest_b64 == :crypto.hash(:sha256, XmlDsig.c14n(unsigned)) |> Base.encode64()
+    end
+
+    test "changing the signed content breaks the digest", %{xml: xml} do
+      {_si, _signature, digest_b64, unsigned} = signature_pieces(xml)
+      tampered = String.replace(unsigned, ~s(orgAmount="5.00"), ~s(orgAmount="6.00"))
+      refute digest_b64 == :crypto.hash(:sha256, XmlDsig.c14n(tampered)) |> Base.encode64()
+    end
+
+    test "the document is still well formed and carries the Signature as last child of RespPay", %{xml: xml} do
+      assert attr(xml, "local-name(/*)") == "RespPay"
+      assert attr(xml, "local-name(/*/*[last()])") == "Signature"
+    end
+  end
+
+  describe "key path" do
+    test "load_private_key!/1 raises a clear error when no path is configured" do
+      assert_raise ArgumentError, ~r/XML_SIGNING_PRIV_KEY_PATH/, fn -> XmlDsig.load_private_key!(nil) end
+      assert_raise ArgumentError, ~r/XML_SIGNING_PRIV_KEY_PATH/, fn -> XmlDsig.load_private_key!("") end
+    end
+
+    test "there is no built-in /var/www default any more" do
+      previous = Application.get_env(:upi_core, :xml_signing_priv_key_path)
+      Application.delete_env(:upi_core, :xml_signing_priv_key_path)
+      on_exit(fn -> if previous, do: Application.put_env(:upi_core, :xml_signing_priv_key_path, previous) end)
+
+      assert_raise ArgumentError, ~r/XML_SIGNING_PRIV_KEY_PATH/, fn ->
+        UpiXmlSchema.generate_resp_pay(%{org_id: "MER101", result: "SUCCESS", resp_code: "00", request_data: %{}})
+      end
+    end
+  end
+
+  defp attr(xml, path), do: xpath(xml, ~x"#{path}"s)
+end
diff --git a/config/runtime.exs b/config/runtime.exs
index 93b11c6..457a703 100755
--- a/config/runtime.exs
+++ b/config/runtime.exs
@@ -62,6 +62,10 @@ config :upi_static, :npci_resp_hbt_endpoint, "#{npci_base_url}/RespHbt/2.0/urn:t
 config :upi_core, :npci_base_url, npci_base_url
 config :upi_core, :npci_mode, npci_mode
 
+# [Signature key path] ADDED: RSA private key (PEM) used to sign RespPay (XMLDSig, TSD v16 §7.2.5). No default path:
+# the old /var/www/internaltesting/madhoolika/... default is gone.
+config :upi_core, :xml_signing_priv_key_path, System.get_env("XML_SIGNING_PRIV_KEY_PATH")
+
 # [RespValQr Payee.Ac] ADDED: PSP's Indian settlement account sent as Payee/Ac in RespValQr
 # (TSD Table 2: ACTYPE / ACNUM / IFSC). Real values come from the environment - never hard-coded.
 config :upi_core, :psp_settlement_account,
