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