Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
elixir 1.12.3-otp-24
erlang 24.2
1 change: 0 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,4 @@ otp_release:
- 21.0
- 20.3
- 19.3
- 18.3
script: rebar3 do compile, eunit
2 changes: 1 addition & 1 deletion include/esaml.hrl
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@
idp_signs_envelopes = true :: boolean(),
idp_signs_logout_requests = true :: boolean(),
sp_sign_metadata = false :: boolean(),
trusted_fingerprints = [] :: [string() | binary()],
trusted_fingerprints = [] :: [string() | binary()] | any,
metadata_uri = "" :: string(),
consume_uri = "" :: string(),
logout_uri :: string() | undefined,
Expand Down
Binary file removed rebar
Binary file not shown.
2 changes: 1 addition & 1 deletion rebar.config
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
{ct_verbose, true}.

{deps, [
{cowboy, "2.6.0"}
{cowboy, "2.9.0"}
]}.

{edoc_opts, [{preprocess, true}]}.
Expand Down
Binary file added rebar3
Binary file not shown.
36 changes: 15 additions & 21 deletions src/esaml.erl
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,9 @@ validate_assertion(AssertionXml, Recipient, Audience) ->
case proplists:get_value(audience, Conds) of
undefined -> A;
Audience -> A;
_ -> {error, bad_audience}
Other ->
logger:error("ESAML: bad_audience - expected: ~ts, got: ~ts", [Audience, Other]),
{error, bad_audience}
end;
_ -> A
end end,
Expand Down Expand Up @@ -516,29 +518,29 @@ to_xml(#esaml_logoutresp{version = V, issue_instant = Time,

Ns = #xmlNamespace{nodes = [{"md", 'urn:oasis:names:tc:SAML:2.0:metadata'},
{"saml", 'urn:oasis:names:tc:SAML:2.0:assertion'},
{"dsig", 'http://www.w3.org/2000/09/xmldsig#'}]},
{"ds", 'http://www.w3.org/2000/09/xmldsig#'}]},

KeyDescriptorElems = case CertBin of
undefined -> [];
C when is_binary(C) -> [
#xmlElement{name = 'md:KeyDescriptor',
attributes = [#xmlAttribute{name = 'use', value = "signing"}],
content = [#xmlElement{name = 'dsig:KeyInfo',
content = [#xmlElement{name = 'dsig:X509Data',
content = [#xmlElement{name = 'ds:KeyInfo',
content = [#xmlElement{name = 'ds:X509Data',
content =
[#xmlElement{name = 'dsig:X509Certificate',
[#xmlElement{name = 'ds:X509Certificate',
content = [#xmlText{value = base64:encode_to_string(CertBin)}]} |
[#xmlElement{name = 'dsig:X509Certificate',
[#xmlElement{name = 'ds:X509Certificate',
content = [#xmlText{value = base64:encode_to_string(CertChainBin)}]} || CertChainBin <- CertChain]]}]}]},

#xmlElement{name = 'md:KeyDescriptor',
attributes = [#xmlAttribute{name = 'use', value = "encryption"}],
content = [#xmlElement{name = 'dsig:KeyInfo',
content = [#xmlElement{name = 'dsig:X509Data',
content = [#xmlElement{name = 'ds:KeyInfo',
content = [#xmlElement{name = 'ds:X509Data',
content =
[#xmlElement{name = 'dsig:X509Certificate',
[#xmlElement{name = 'ds:X509Certificate',
content = [#xmlText{value = base64:encode_to_string(CertBin)}]} |
[#xmlElement{name = 'dsig:X509Certificate',
[#xmlElement{name = 'ds:X509Certificate',
content = [#xmlText{value = base64:encode_to_string(CertChainBin)}]} || CertChainBin <- CertChain]]}]}]}

]
Expand All @@ -547,9 +549,6 @@ to_xml(#esaml_logoutresp{version = V, issue_instant = Time,
SingleLogoutServiceElems = case SLOLoc of
undefined -> [];
_ -> [
#xmlElement{name = 'md:SingleLogoutService',
attributes = [#xmlAttribute{name = 'Binding', value = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"},
#xmlAttribute{name = 'Location', value = SLOLoc}]},
#xmlElement{name = 'md:SingleLogoutService',
attributes = [#xmlAttribute{name = 'Binding', value = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"},
#xmlAttribute{name = 'Location', value = SLOLoc}]}
Expand All @@ -561,10 +560,6 @@ to_xml(#esaml_logoutresp{version = V, issue_instant = Time,
attributes = [#xmlAttribute{name = 'isDefault', value = "true"},
#xmlAttribute{name = 'index', value = "0"},
#xmlAttribute{name = 'Binding', value = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"},
#xmlAttribute{name = 'Location', value = ConsumerLoc}]},
#xmlElement{name = 'md:AssertionConsumerService',
attributes = [#xmlAttribute{name = 'index', value = "1"},
#xmlAttribute{name = 'Binding', value = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"},
#xmlAttribute{name = 'Location', value = ConsumerLoc}]}
],

Expand Down Expand Up @@ -592,21 +587,20 @@ to_xml(#esaml_logoutresp{version = V, issue_instant = Time,

esaml_util:build_nsinfo(Ns, #xmlElement{
name = 'md:EntityDescriptor',
attributes = [
attributes = [
#xmlAttribute{name = 'xmlns:md', value = atom_to_list(proplists:get_value("md", Ns#xmlNamespace.nodes))},
#xmlAttribute{name = 'xmlns:saml', value = atom_to_list(proplists:get_value("saml", Ns#xmlNamespace.nodes))},
#xmlAttribute{name = 'xmlns:dsig', value = atom_to_list(proplists:get_value("dsig", Ns#xmlNamespace.nodes))},
#xmlAttribute{name = 'xmlns:ds', value = atom_to_list(proplists:get_value("ds", Ns#xmlNamespace.nodes))},
#xmlAttribute{name = 'entityID', value = EntityID}
], content = [
SPSSODescriptorElem,
OrganizationElem,
ContactElem
]
});
});

to_xml(_) -> error("unknown record").


-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").

Expand Down
75 changes: 60 additions & 15 deletions src/esaml_sp.erl
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@

-include("esaml.hrl").
-include_lib("xmerl/include/xmerl.hrl").
-include_lib("kernel/include/logger.hrl").

-export([setup/1, generate_authn_request/2, generate_authn_request/3, generate_metadata/1]).
-export([validate_assertion/2, validate_assertion/3]).
-export([validate_assertion/2, validate_assertion/3, validate_assertion/4]).
-export([generate_logout_request/3, generate_logout_request/4, generate_logout_response/3]).
-export([validate_logout_request/2, validate_logout_response/2]).

Expand Down Expand Up @@ -169,7 +170,8 @@ validate_logout_request(Xml, SP = #esaml_sp{}) ->
fun(X) ->
case xmerl_xpath:string("/samlp:LogoutRequest", X, [{namespace, Ns}]) of
[#xmlElement{}] -> X;
_ -> {error, bad_assertion}
Error -> logger:error("ESAML_SP: Failed parsing LogoutRequest XML with: ~ts",[Error]),
{error, bad_assertion}
end
end,
fun(X) ->
Expand Down Expand Up @@ -239,6 +241,15 @@ validate_assertion(Xml, SP = #esaml_sp{}) ->
-spec validate_assertion(xml(), dupe_fun(), esaml:sp()) ->
{ok, esaml:assertion()} | {error, Reason :: term()}.
validate_assertion(Xml, DuplicateFun, SP = #esaml_sp{}) ->
validate_assertion(Xml, DuplicateFun, SP, nil).

%% @doc Validate and decode an assertion envelope in parsed XML
%%
%% sign_cert() is x509 cert to be used if SAML response is missing
%% X509Certificate element (Cisco)
-spec validate_assertion(xml(), dupe_fun(), esaml:sp(), SignCert :: binary | any) ->
{ok, esaml:assertion()} | {error, Reason :: term()} | {error, Reason :: term(), Details :: any}.
validate_assertion(Xml, DuplicateFun, SP = #esaml_sp{}, SignCert) ->
Ns = [{"samlp", 'urn:oasis:names:tc:SAML:2.0:protocol'},
{"saml", 'urn:oasis:names:tc:SAML:2.0:assertion'}],
esaml_util:threaduntil([
Expand All @@ -250,19 +261,19 @@ validate_assertion(Xml, DuplicateFun, SP = #esaml_sp{}) ->
xmerl_xpath:string("/saml:Assertion", DecryptedAssertion, [{namespace, Ns}]) of
[A2] -> A2
catch
_Error:_Reason -> {error, bad_assertion}
Error -> {error, bad_assertion, Error}
end;
_ ->
case xmerl_xpath:string("/samlp:Response/saml:Assertion", X, [{namespace, Ns}]) of
[A3] -> A3;
_ -> {error, bad_assertion}
Error -> {error, bad_assertion, Error}
end
end
end,
fun(A) ->
if
SP#esaml_sp.idp_signs_envelopes ->
case xmerl_dsig:verify(Xml, SP#esaml_sp.trusted_fingerprints) of
case xmerl_dsig:verify(Xml, SP#esaml_sp.trusted_fingerprints, SignCert) of
ok -> A;
OuterError -> {error, {envelope, OuterError}}
end;
Expand All @@ -271,7 +282,7 @@ validate_assertion(Xml, DuplicateFun, SP = #esaml_sp{}) ->
end,
fun(A) ->
if SP#esaml_sp.idp_signs_assertions ->
case xmerl_dsig:verify(A, SP#esaml_sp.trusted_fingerprints) of
case xmerl_dsig:verify(A, SP#esaml_sp.trusted_fingerprints, SignCert) of
ok -> A;
InnerError -> {error, {assertion, InnerError}}
end;
Expand Down Expand Up @@ -299,19 +310,53 @@ decrypt_assertion(Xml, #esaml_sp{key = PrivateKey}) ->
[EncryptedData] = xmerl_xpath:string("./xenc:EncryptedData", Xml, [{namespace, XencNs}]),
[#xmlText{value = CipherValue64}] = xmerl_xpath:string("xenc:CipherData/xenc:CipherValue/text()", EncryptedData, [{namespace, XencNs}]),
CipherValue = base64:decode(CipherValue64),
SymmetricKey = decrypt_key_info(EncryptedData, PrivateKey),
EncryptedKey = get_encrypted_key(EncryptedData, Xml),
SymmetricKey = decrypt_key(EncryptedKey, PrivateKey),
[#xmlAttribute{value = Algorithm}] = xmerl_xpath:string("./xenc:EncryptionMethod/@Algorithm", EncryptedData, [{namespace, XencNs}]),
AssertionXml = block_decrypt(Algorithm, SymmetricKey, CipherValue),
{Assertion, _} = xmerl_scan:string(AssertionXml, [{namespace_conformant, true}]),
Assertion.


decrypt_key_info(EncryptedData, Key) ->
DsNs = [{"ds", 'http://www.w3.org/2000/09/xmldsig#'}],
get_encrypted_key(EncryptedData, Xml) ->
Ns = [{namespace, [
{"ds", 'http://www.w3.org/2000/09/xmldsig#'},
{"xenc", 'http://www.w3.org/2001/04/xmlenc#'}
]}],
[KeyInfo] = xmerl_xpath:string("./ds:KeyInfo", EncryptedData, Ns),
case xmerl_xpath:string("./xenc:EncryptedKey", KeyInfo, Ns) of
[EncryptedKey] ->
EncryptedKey;
[] ->
retrieve_encrypted_key(KeyInfo, Xml, Ns)
end.


retrieve_encrypted_key(KeyInfo, Xml, Ns) ->
[#xmlAttribute{value = URI}] = xmerl_xpath:string("./ds:RetrievalMethod/@URI", KeyInfo, Ns),
case URI of
"#" ++ Id ->
[EncryptedKey] = xmerl_xpath:string("//xenc:EncryptedKey[@Id='" ++ Id ++ "']", Xml, Ns),
EncryptedKey;
_ ->
case application:get_env(esaml, retrieve_remote_key, fun default_retrieve_remote_key/1) of
Fun when is_function(Fun, 1) ->
Fun(URI);

{M, F, A} ->
erlang:apply(M, F, [URI | A])
end
end.


default_retrieve_remote_key(_URI) ->
error("Retrieval of remote encrypted keys is not configured").


decrypt_key(EncryptedKey, Key) ->
XencNs = [{"xenc", 'http://www.w3.org/2001/04/xmlenc#'}],
[KeyInfo] = xmerl_xpath:string("./ds:KeyInfo", EncryptedData, [{namespace, DsNs}]),
[#xmlAttribute{value = Algorithm}] = xmerl_xpath:string("./xenc:EncryptedKey/xenc:EncryptionMethod/@Algorithm", KeyInfo, [{namespace, XencNs}]),
[#xmlText{value = CipherValue64}] = xmerl_xpath:string("./xenc:EncryptedKey/xenc:CipherData/xenc:CipherValue/text()", KeyInfo, [{namespace, XencNs}]),
[#xmlAttribute{value = Algorithm}] = xmerl_xpath:string("./xenc:EncryptionMethod/@Algorithm", EncryptedKey, [{namespace, XencNs}]),
[#xmlText{value = CipherValue64}] = xmerl_xpath:string("./xenc:CipherData/xenc:CipherValue/text()", EncryptedKey, [{namespace, XencNs}]),
CipherValue = base64:decode(CipherValue64),
decrypt(CipherValue, Algorithm, Key).

Expand All @@ -334,18 +379,18 @@ block_decrypt("http://www.w3.org/2009/xmlenc11#aes128-gcm", SymmetricKey, Cipher
%% IV: 12 bytes and Tag data: 16 bytes
EncryptedDataSize = byte_size(CipherValue) - 12 - 16,
<<IV:12/binary, EncryptedData:EncryptedDataSize/binary, Tag:16/binary>> = CipherValue,
DecryptedData = crypto:block_decrypt(aes_gcm, SymmetricKey, IV, {<<>>, EncryptedData, Tag}),
DecryptedData = crypto:crypto_one_time_aead(aes_128_gcm, SymmetricKey, IV, EncryptedData, <<>>, Tag, false),
binary_to_list(DecryptedData);

block_decrypt("http://www.w3.org/2001/04/xmlenc#aes128-cbc", SymmetricKey, CipherValue) ->
<<IV:16/binary, EncryptedData/binary>> = CipherValue,
DecryptedData = crypto:block_decrypt(aes_cbc128, SymmetricKey, IV, EncryptedData),
DecryptedData = crypto:crypto_one_time(aes_128_cbc, SymmetricKey, IV, EncryptedData, false),
IsPadding = fun(X) -> X < 16 end,
lists:reverse(lists:dropwhile(IsPadding, lists:reverse(binary_to_list(DecryptedData))));

block_decrypt("http://www.w3.org/2001/04/xmlenc#aes256-cbc", SymmetricKey, CipherValue) ->
<<IV:16/binary, EncryptedData/binary>> = CipherValue,
DecryptedData = crypto:block_decrypt(aes_cbc256, SymmetricKey, IV, EncryptedData),
DecryptedData = crypto:crypto_one_time(aes_256_cbc, SymmetricKey, IV, EncryptedData, false),
IsPadding = fun(X) -> X < 16 end,
lists:reverse(lists:dropwhile(IsPadding, lists:reverse(binary_to_list(DecryptedData)))).

Expand Down
18 changes: 15 additions & 3 deletions src/xmerl_dsig.erl
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
%% enveloped mode.
-module(xmerl_dsig).

-export([verify/1, verify/2, sign/3, sign/4, strip/1, digest/1]).
-export([verify/1, verify/2, verify/3, sign/3, sign/4, strip/1, digest/1]).

-include_lib("xmerl/include/xmerl.hrl").
-include_lib("public_key/include/public_key.hrl").
Expand Down Expand Up @@ -162,6 +162,15 @@ digest(Element, HashFunction) ->
%% than rsa+sha1 or sha256 this will asplode. Don't say I didn't warn you.
-spec verify(Element :: #xmlElement{}, Fingerprints :: [fingerprint()] | any) -> ok | {error, bad_digest | bad_signature | cert_not_accepted}.
verify(Element, Fingerprints) ->
verify(Element, Fingerprints, nil).

%% @doc Dubber change with optional x509 certificate as parameter
%%
%% This certificate to be used when X509Certificate child is not found
%%
%%
-spec verify(Element :: #xmlElement{}, Fingerprints :: [fingerprint()] | any, SkipCert :: binary | any) -> ok | {error, bad_digest | bad_signature | cert_not_accepted}.
verify(Element, Fingerprints, SkipCert) ->
DsNs = [{"ds", 'http://www.w3.org/2000/09/xmldsig#'},
{"ec", 'http://www.w3.org/2001/10/xml-exc-c14n#'}],

Expand Down Expand Up @@ -194,8 +203,11 @@ verify(Element, Fingerprints) ->
[#xmlText{value = Sig64}] = xmerl_xpath:string("ds:Signature//ds:SignatureValue/text()", Element, [{namespace, DsNs}]),
Sig = base64:decode(Sig64),

[#xmlText{value = Cert64}] = xmerl_xpath:string("ds:Signature//ds:X509Certificate/text()", Element, [{namespace, DsNs}]),
CertBin = base64:decode(Cert64),
CertBin = case xmerl_xpath:string("ds:Signature//ds:X509Certificate/text()", Element, [{namespace, DsNs}]) of
[] -> SkipCert;
[#xmlText{value = Cert64}] -> base64:decode(Cert64)
end,
%% CertBin = base64:decode(Cert64),
CertHash = crypto:hash(sha, CertBin),
CertHash2 = crypto:hash(sha256, CertBin),

Expand Down
27 changes: 27 additions & 0 deletions test/esaml_sp.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-----BEGIN CERTIFICATE-----
MIIEpDCCAowCCQCSdHd01QFuCDANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAls
b2NhbGhvc3QwHhcNMjAwMjI1MDA0OTIxWhcNMzAwMjIyMDA0OTIxWjAUMRIwEAYD
VQQDDAlsb2NhbGhvc3QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC7
b8IfFHfFveaO0OWOzt7xCMqIAhtB8RDuesmTlQLP6rIysz3jUXr/1VLjl2y5/YCJ
83Nl3DUSkQLNuksdBnuQyDD6AsOyS9iU6yzrcPCalRSzOeDXthklX5hvkbPykM0v
AvFUvjO7Vbjsp9UsuLhCVn9djSRxyaDCBZsLUXrDamIlpltqG+/3kltUp76gr8S7
+0Htjnwh1X+oCzfpCkQBjDJlszAilyZChZhHDR3o9QHeAfno7eMy+XT/DaXt4ERS
ypij0m1RZjB55ceKjOqT6CtfPXsFWzPwcC1Kh0kQbdi8YY/pNSxZhiqcA50f+iut
7Pm6+CpntrZ3VWlC6tcU7pVf4XZZpjRU8eb7L9OTsnnaDONT6h4jc6WMiIyrf2cU
7gBCrOlRT0TOeWbuvrtCy9PZg/pZwDUJlgwFhKMFN3XuVkDWrNwxdNIXrwwc04Bf
sQAztqBaAkD1HqMmQZq4GrGGA1+cS4QFZCiyifUo+3T12DdCICSXMtvpgUGMgVDP
u5c7MaSWNVYD4oSJv1kPQQexG1SrP8qymg1lwXf3smcgxmBF01zY+fhapr817kvl
oE+SwVq1hwSFRaPgIG7+8WhTSYilwXoFBK3ZaM5NzOu7T7zp24C/73M43NR8p5AA
rD9Qb5YmeyuQrs0UXhWBkaFDCmE5C/E+kMN3pmt6KQIDAQABMA0GCSqGSIb3DQEB
CwUAA4ICAQBmmqqHl5DwnlcNabYjNQYuCcRE0s9FX5xy1pY/JkG6H85/pWTuxBDa
CWTtYXpXg+LRv6o2Y6mRLFNNZsyo+Ypuis0hvu5j5NmjDHvhuUjLyw3HtMOvlEqu
tcBpdTczLOAXe5ntzL76Tf5IdnvPItqjIjfzbJJfx9pV77SJbtusILIrJEFTHTcO
cC5Yg8nJP1Zhy7J8rAZdSCrKxaDjcEet79QMJfE0wx7gM0yASCHMGb9v/eT2iugb
ZNj0R5CuTQXbILCo3dvd3T0eqmnsjkbRLN/pVYCPEbtQGcA7k3wAMarlHGcIfH1i
DqB3up/cH0W4N1XYpWYSxwDOPR6gURyZarGAlSqBk9b3rc80UTetiZX9D9F4r83z
rwjD4oiIBSKchpPTn2sEv//4DeKLRa/Q1QvLdMHInf5iPmsA12r0VywNLXGw/c55
M29z/m9QqCfeZ/SvFZhqvHS5lRMuzIbPE+kpNerBOjXI7YHE5+TkTOfjsDhNim8B
o1UDCnpdNJzK5COJ+zJW0AtCVsIWqgCaUA3TeWk7V9o8FGx5OueiGJbVF4rXaUhW
s94wDvnv/fgvGcJtzSrDorLN0lT4AMQvajoSzeaxkNSSykl2Un1J9HmJdiwYAx24
Mf7cpkaGeVa/zoFDd78YQdheuV8w7kHzmZmYodQ9Va7ASsDMe8chLA==
-----END CERTIFICATE-----
Loading