diff --git a/lib/ex_saml/core/xml/dsig.ex b/lib/ex_saml/core/xml/dsig.ex
index 3742fa4..6a6b9ee 100644
--- a/lib/ex_saml/core/xml/dsig.ex
+++ b/lib/ex_saml/core/xml/dsig.ex
@@ -302,6 +302,7 @@ defmodule ExSaml.Core.Xml.Dsig do
:bad_digest
| :bad_signature
| :cert_not_accepted
+ | :missing_certificate
| :no_signature
| :multiple_signatures
| :insecure_algorithm
@@ -403,31 +404,39 @@ defmodule ExSaml.Core.Xml.Dsig do
sig = :base64.decode(xmlText(sig_text, :value))
- {key, cert_bin} = extract_public_key(element, ds_ns)
-
- case :public_key.verify(data, hash_function, sig, key) do
- true -> check_fingerprints(cert_bin, fingerprints)
- false -> {:error, :bad_signature}
+ with {:ok, {key, cert_bin}} <- extract_public_key(element, ds_ns) do
+ case :public_key.verify(data, hash_function, sig, key) do
+ true -> check_fingerprints(cert_bin, fingerprints)
+ false -> {:error, :bad_signature}
+ end
end
end
defp extract_public_key(element, ds_ns) do
- [cert_text] =
- :xmerl_xpath.string(~c"ds:Signature//ds:X509Certificate/text()", element, namespace: ds_ns)
-
- cert_bin = :base64.decode(xmlText(cert_text, :value))
- cert = :public_key.pkix_decode_cert(cert_bin, :plain)
- tbs = certificate(cert, :tbsCertificate)
- spki = tbs_certificate(tbs, :subjectPublicKeyInfo)
-
- key_bin =
- case subject_public_key_info(spki, :subjectPublicKey) do
- {_, kb} -> kb
- kb -> kb
- end
+ case :xmerl_xpath.string(
+ ~c"ds:Signature//ds:X509Certificate/text()",
+ element,
+ namespace: ds_ns
+ ) do
+ [] ->
+ {:error, :missing_certificate}
+
+ # The first certificate is the signing one; the rest is its chain.
+ [cert_text | _] ->
+ cert_bin = :base64.decode(xmlText(cert_text, :value))
+ cert = :public_key.pkix_decode_cert(cert_bin, :plain)
+ tbs = certificate(cert, :tbsCertificate)
+ spki = tbs_certificate(tbs, :subjectPublicKeyInfo)
+
+ key_bin =
+ case subject_public_key_info(spki, :subjectPublicKey) do
+ {_, kb} -> kb
+ kb -> kb
+ end
- key = :public_key.pem_entry_decode({:RSAPublicKey, key_bin, :not_encrypted})
- {key, cert_bin}
+ key = :public_key.pem_entry_decode({:RSAPublicKey, key_bin, :not_encrypted})
+ {:ok, {key, cert_bin}}
+ end
end
defp check_fingerprints(_cert_bin, :any), do: :ok
diff --git a/lib/ex_saml/error_messages.ex b/lib/ex_saml/error_messages.ex
index fdc2ecb..33f0dda 100644
--- a/lib/ex_saml/error_messages.ex
+++ b/lib/ex_saml/error_messages.ex
@@ -20,9 +20,11 @@ defmodule ExSaml.ErrorMessages do
@status_responder {:saml_error, ~c"urn:oasis:names:tc:SAML:2.0:status:Responder", :undefined}
@assertion_signature {:assertion, {:error, :no_signature}}
@assertion_cert_not_accepted {:assertion, {:error, :cert_not_accepted}}
+ @assertion_missing_certificate {:assertion, {:error, :missing_certificate}}
@envelop_signature {:envelope, {:error, :no_signature}}
+ @envelope_missing_certificate {:envelope, {:error, :missing_certificate}}
@name_id_errors {:saml_error, ~c"urn:oasis:names:tc:SAML:2.0:status:Requester", :undefined}
- @errors ~w(bad_digest assertion_cert_not_accepted missing_assertion_key bad_audience cert_no_accepted invalid_nameid_policy invalid_nonce missing_assertion_signature missing_envelope_signature status_responder)a
+ @errors ~w(bad_digest assertion_cert_not_accepted missing_assertion_key bad_audience cert_no_accepted invalid_nameid_policy invalid_nonce missing_assertion_signature missing_certificate missing_envelope_signature status_responder)a
@doc false
def get(error, locale \\ "en")
@@ -38,9 +40,15 @@ defmodule ExSaml.ErrorMessages do
def get(@assertion_cert_not_accepted, locale),
do: get(:assertion_cert_not_accepted, locale)
+ def get(@assertion_missing_certificate, locale),
+ do: get(:missing_certificate, locale)
+
def get(@envelop_signature, locale),
do: get(:missing_envelope_signature, locale)
+ def get(@envelope_missing_certificate, locale),
+ do: get(:missing_certificate, locale)
+
def get(@name_id_errors, locale),
do: get(:invalid_nameid_policy, locale)
diff --git a/priv/gettext/en/LC_MESSAGES/errors.po b/priv/gettext/en/LC_MESSAGES/errors.po
index dd8f927..f12f853 100644
--- a/priv/gettext/en/LC_MESSAGES/errors.po
+++ b/priv/gettext/en/LC_MESSAGES/errors.po
@@ -14,6 +14,9 @@ msgstr "Missing signature. Please ensure that the assertions are properly signed
msgid "assertion_cert_not_accepted"
msgstr "The certificate used to sign the assertions has not been accepted. It is possible that the certificate has expired or is not supported."
+msgid "missing_certificate"
+msgstr "No X.509 certificate was found in the SAML signature. Please configure your Identity Provider to include its signing certificate in signed responses and assertions."
+
msgid "missing_envelope_signature"
msgstr "Missing envelope signature. Please make sure you uploaded correctly the Cryptr Certificate to your IdP"
diff --git a/priv/gettext/errors.pot b/priv/gettext/errors.pot
index 05b6fa1..e12b8ae 100644
--- a/priv/gettext/errors.pot
+++ b/priv/gettext/errors.pot
@@ -13,6 +13,9 @@ msgstr "Missing signature. Please ensure that the assertions are properly signed
msgid "assertion_cert_not_accepted"
msgstr "The certificate used to sign the assertions has not been accepted. It is possible that the certificate has expired or is not supported."
+msgid "missing_certificate"
+msgstr "No X.509 certificate was found in the SAML signature. Please configure your Identity Provider to include its signing certificate in signed responses and assertions."
+
msgid "missing_envelope_signature"
msgstr "Missing envelope signature. Please make sure you uploaded correctly the Cryptr Certificate to your IdP."
diff --git a/priv/gettext/fr/LC_MESSAGES/errors.po b/priv/gettext/fr/LC_MESSAGES/errors.po
index 14fde47..4a92222 100644
--- a/priv/gettext/fr/LC_MESSAGES/errors.po
+++ b/priv/gettext/fr/LC_MESSAGES/errors.po
@@ -13,6 +13,9 @@ msgstr "Signature manquante. Veuillez vous assurer que les assertions sont corre
msgid "assertion_cert_not_accepted"
msgstr "Le certificat utilisé pour signer les assertions n'a pas été accepté. Il est possible que le certificat ait expiré ou qu'il ne soit pas pris en charge."
+msgid "missing_certificate"
+msgstr "Aucun certificat X.509 n'a été trouvé dans la signature SAML. Veuillez configurer votre fournisseur d'identité pour inclure son certificat de signature dans les réponses et assertions signées."
+
msgid "missing_envelope_signature"
msgstr "La Signature pour les enveloppes est manquantes. Assurez-vous que vous avec correctement uploadé votre Certificat Cryptr sur votre IdP."
diff --git a/test/ex_saml/core/xml/dsig_test.exs b/test/ex_saml/core/xml/dsig_test.exs
index 8c8488e..6c4e861 100644
--- a/test/ex_saml/core/xml/dsig_test.exs
+++ b/test/ex_saml/core/xml/dsig_test.exs
@@ -220,6 +220,27 @@ defmodule ExSaml.Core.Xml.DsigTest do
assert :ok = Dsig.verify(signed_xml, [:crypto.hash(:sha, cert_bin)])
end
+
+ test "signature without X509Certificate returns {:error, :missing_certificate}" do
+ doc =
+ parse_xml(~S|blah|)
+
+ {key, cert_bin} = test_sign_256_key()
+ signed_xml = Dsig.sign(doc, key, cert_bin, :rsa_sha256)
+
+ # Drop KeyInfo from the serialized document: the enveloped-signature
+ # transform excludes ds:Signature from the digest, so it stays valid.
+ certless_doc =
+ [signed_xml]
+ |> :xmerl.export(:xmerl_xml)
+ |> List.flatten()
+ |> to_string()
+ |> String.replace(~r|.*|s, "")
+ |> parse_xml()
+
+ assert {:error, :missing_certificate} =
+ Dsig.verify(certless_doc, [:crypto.hash(:sha, cert_bin)])
+ end
end
describe "strip/1" do