%% xmlrat %% %% Copyright 2021 The University of Queensland %% Author: Alex Wilson %% %% Redistribution and use in source and binary forms, with or without %% modification, are permitted provided that the following conditions %% are met: %% 1. Redistributions of source code must retain the above copyright %% notice, this list of conditions and the following disclaimer. %% 2. Redistributions in binary form must reproduce the above copyright %% notice, this list of conditions and the following disclaimer in the %% documentation and/or other materials provided with the distribution. %% %% THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR %% IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES %% OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. %% IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, %% INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT %% NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, %% DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY %% THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT %% (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF %% THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. %% %% @doc Implementation of XML Digital Signature (DSIG). %% %% See W3C Recommendation: %% XML Signature Syntax and Processing Version 1.1. %% %% Features supported: %% %% %% The behaviours {@link xmlrat_dsig_signer} and {@link xmlrat_dsig_verifier} %% allow providing a callback module to customise the behaviour of %% xmlrat_dsig with respect to obtaining and using private keys and %% validating public keys and certificates. -module(xmlrat_dsig). -include_lib("xmlrat/include/records.hrl"). -include_lib("xmlrat/include/dsig.hrl"). -include_lib("public_key/include/public_key.hrl"). -compile({parse_transform, xmlrat_parse_transform}). -export([ verify/2, verify/3, sign/2, parse_key/1, generate_key/1 ]). % These are exported to silence the "unused function" warnings. -export([ match_id/1, encode_xform/1, encode_ref/1, encode_keyvalue_rsa/1, encode_keyvalue_ec/1, encode_keyinfo/1 ]). -export_type([ pubkey_algo/0, hash_algo/0, pubkey/0, cert/0, key_details/0 ]). -type base64() :: binary(). -type uri() :: binary(). -type pubkey_algo() :: rsa | dsa | ecdsa | hmac. -type hash_algo() :: sha | sha256 | sha384 | sha512. -type pubkey() :: #'RSAPublicKey'{} | {integer(), #'Dss-Parms'{}} | {#'ECPoint'{}, {namedCurve, tuple() | atom()}}. -type cert() :: #'OTPCertificate'{}. -type key_details() :: #{ public_key => pubkey(), certificate => cert(), name => binary() }. -record(transform, { algo :: uri(), force_ns :: undefined | binary() }). -record(reference, { uri :: undefined | uri(), xforms :: [#transform{}], digest_meth :: uri(), digest :: base64() }). -record(keyvalue_rsa, { modulus :: base64(), exponent :: base64() }). -record(keyvalue_ec, { curve :: uri(), pubkey :: base64() }). -record(keyinfo, { name :: undefined | binary(), certs :: undefined | [base64()], key :: undefined | #keyvalue_rsa{} | #keyvalue_ec{} }). -record(signedinfo, { c14n_meth :: uri(), sig_meth :: uri(), refs :: [#reference{}] }). -record(signature, { id :: undefined | binary(), info :: #signedinfo{}, sig :: base64(), key :: #keyinfo{} }). -type verify_options() :: #{ verifier_options => map(), verifier => module() }. %% @doc Parse a ds:KeyInfo element. %% %% Converts a ds:KeyInfo element into the more friendly %% {@link key_details()} map, containing decoded certificates and public %% keys. -spec parse_key(xmlrat:document()) -> {ok, key_details()} | {error, term()}. parse_key(Doc) -> case (catch decode_keyinfo(Doc)) of {'EXIT', Why} -> {error, Why}; #keyinfo{name = undefined, certs = undefined, key = undefined} -> {error, insufficient_key_info}; #keyinfo{name = N, certs = undefined, key = undefined} when is_binary(N) -> {ok, #{name => N}}; #keyinfo{certs = [CertB64 | _]} -> CertBin = base64:decode(CertB64), Cert = #'OTPCertificate'{} = public_key:pkix_decode_cert(CertBin, otp), PubKey = cert_to_pubkey(Cert), {ok, #{certificate => Cert, public_key => PubKey}}; #keyinfo{name = N, key = KeyValue} -> Details0 = case N of undefined -> #{}; _ -> #{name => N} end, PubKey = keyvalue_to_pubkey(KeyValue), {ok, Details0#{public_key => PubKey}} end. %% @doc Generate a ds:KeyInfo element. %% %% Inverse of {@link parse_key/1}. Converts a {@link key_details()} map into %% a ds:KeyInfo element. -spec generate_key(key_details()) -> {ok, xmlrat:document()} | {error, term()}. generate_key(#{certificate := Cert}) -> Der = public_key:pkix_encode('OTPCertificate', Cert, otp), B64 = base64:encode(Der), case (catch encode_keyinfo(#keyinfo{certs = [B64]})) of {'EXIT', Why} -> {error, Why}; Enc -> {ok, Enc} end; generate_key(KD = #{public_key := PubKey}) -> KV = pubkey_to_keyvalue(PubKey), KI0 = #keyinfo{key = KV}, KI1 = case KD of #{name := Name} -> KI0#keyinfo{name = Name}; _ -> KI0 end, case (catch encode_keyinfo(KI1)) of {'EXIT', Why} -> {error, Why}; Enc -> {ok, Enc} end. %% @doc Verifies an enveloped XML-DSIG signature. %% %% The signature may not cover every part of the subject document, so this %% function (and {@link verify/3}) return a VerifiedSubset document %% containing just the parts covered by the signature. Parent elements of %% covered elements are preserved, but have all their attributes and other %% content (other than the signed parts) removed. -spec verify(xmlrat:document(), verify_options()) -> {ok, VerifiedSubset :: xmlrat:document()} | {error, term()}. verify(Doc, Opts) -> case extract_sig(Doc) of SigDoc = [_SigElem] -> SignedDoc = strip_sig(Doc), verify(SignedDoc, SigDoc, Opts); [] -> {error, no_signature_element} end. %% @doc Verifies a detached XML-DSIG signature. -spec verify(xmlrat:document(), xmlrat:document(), verify_options()) -> {ok, VerifiedSubset :: xmlrat:document()} | {error, term()}. verify(SignedDoc, SigDoc, Opts) -> % First, parse the signature and its contained information. case (catch decode_sig(SigDoc)) of {'EXIT', Why} -> {error, Why}; #signature{info = SignedInfo, sig = SignatureB64, key = KeyInfo} -> #signedinfo{c14n_meth = C14NMethURI, sig_meth = SigMethURI} = SignedInfo, case C14NMethURI of ?dsig_XML_c14n_exc -> ok; ?dsig_XML_c14n_10 -> ok; ?dsig_XML_c14n_11 -> ok; _ -> error({unsupported_c14n_method, C14NMethURI}) end, SigAlgo = algo_from_uri(SigMethURI), % Next: make sure we have a valid public key signing this payload. case get_validated_pubkey(Opts, SigAlgo, KeyInfo) of E = {error, _Why} -> E; {ok, PubKey} -> % Now we can verify the signature on the SignedInfo. SigInfo = [_SigInfoElem] = extract_siginfo(SigDoc), CanonSignedInfo = xmlrat_c14n:string(SigInfo), {_, SigHash} = SigAlgo, Signature = base64:decode(SignatureB64), Result = public_key:verify(CanonSignedInfo, SigHash, Signature, PubKey), case Result of false -> {error, invalid_signature}; true -> % And finally, we verify the digest of each % reference. #signedinfo{refs = Refs} = SignedInfo, ValidRefs = lists:all(fun (Ref) -> verify_reference(SignedDoc, Ref) end, Refs), case ValidRefs of false -> {error, invalid_digest}; true -> RefSub = referenced_subset(SignedDoc, Refs), {ok, RefSub} end end end end. % HACK: we look for any attributes named like "id" rather than actually looking % at the DTD or XSD. this is kinda hacky, but works fine for pretty much % all reasonable users of dsig (and very few generate a useful DTD). -xpath({match_id, "//*[@ID = $id or @Id = $id or @id = $id]"}). %% Returns the referenced subset of a document %% This consists of the elements covered by a reference, plus all of their %% children, plus their parents (but parents will have attributes removed if %% the element is not covered by any reference) -spec referenced_subset(xmlrat:document(), [#reference{}]) -> xmlrat:document(). referenced_subset([], _Refs) -> []; referenced_subset([E0 = #xml_element{} | Rest], Refs) -> MatchesRef = lists:any(fun (Ref) -> matches_ref(E0, Ref) end, Refs), case MatchesRef of true -> [E0 | referenced_subset(Rest, Refs)]; false -> #xml_element{content = C0} = E0, C1 = referenced_subset(C0, Refs), case C1 of [] -> referenced_subset(Rest, Refs); _ -> E1 = E0#xml_element{attributes = [], content = C1}, [E1 | referenced_subset(Rest, Refs)] end end; referenced_subset([_ | Rest], Refs) -> referenced_subset(Rest, Refs). -spec matches_ref(xmlrat:element(), #reference{}) -> boolean(). matches_ref(E = #xml_element{}, #reference{uri = URI}) -> case URI of undefined -> true; <<>> -> true; <<"#", ID/binary>> -> case match_id([E], #{<<"id">> => ID}) of [E] -> true; _ -> false end end. %% Verify a reference against (part of, or the entire) signed document given. -spec verify_reference(xmlrat:document(), #reference{}) -> boolean(). verify_reference(SignedDoc, Ref) -> #reference{uri = URI, xforms = XForms, digest_meth = DigestMethURI, digest = TheirDigestB64} = Ref, % If the URI is empty or not given, that means the outer document % (enveloped) or the whole message document (separate). DigestDoc = case URI of undefined -> SignedDoc; <<>> -> SignedDoc; <<"#", ID/binary>> -> match_id(SignedDoc, #{<<"id">> => ID}) % TODO: support other forms of the URI? there are other ways to specify % an ID? end, % Usually the transforms include a c14n variant, so we'll get a binary back % from run_xforms/2. If it doesn't, though, this is still valid, and we % should just do regular c14n (no comments) with no options on the result. CanonMsg0 = run_xforms(DigestDoc, XForms), CanonMsg1 = case CanonMsg0 of _ when is_binary(CanonMsg0) -> CanonMsg0; [_|_] -> xmlrat_c14n:string(CanonMsg0) end, DigestAlgo = hash_algo_from_uri(DigestMethURI), OurDigest = crypto:hash(DigestAlgo, CanonMsg1), TheirDigest = base64:decode(TheirDigestB64), hash_compare(OurDigest, TheirDigest). %% Runs a list of transforms. -spec run_xforms(xmlrat:document() | binary(), [#transform{}]) -> xmlrat:document() | binary(). run_xforms(Doc, []) -> Doc; run_xforms(Doc0, [Next | Rest]) -> Doc1 = run_xform(Doc0, Next), run_xforms(Doc1, Rest). -spec run_xform(xmlrat:document(), #transform{}) -> xmlrat:document() | binary(). run_xform(Doc, #transform{algo = ?dsig_enveloped}) -> Doc; run_xform(Doc, #transform{algo = Alg, force_ns = undefined}) when (Alg =:= ?dsig_XML_c14n_exc) or (Alg =:= ?dsig_XML_c14n_10) or (Alg =:= ?dsig_XML_c14n_11) -> xmlrat_c14n:string(Doc); run_xform(Doc, #transform{algo = Alg, force_ns = ForceNS0}) when (Alg =:= ?dsig_XML_c14n_exc) or (Alg =:= ?dsig_XML_c14n_10) or (Alg =:= ?dsig_XML_c14n_11) -> ForceNS1 = binary:split(ForceNS0, [<<" ">>, <<"\t">>, <<",">>], [global, trim_all]), ForceNS2 = lists:foldl(fun (<<"default">>, Acc) -> Acc#{default => true}; (NS, Acc) -> Acc#{NS => true} end, #{}, ForceNS1), xmlrat_c14n:string(Doc, #{force_namespaces => ForceNS2}); run_xform(Doc, #transform{algo = ?dsig_XML_c14n_11}) -> xmlrat_c14n:string(Doc); run_xform(_Doc, #transform{algo = URI}) -> error({unsupported_transform, URI}). -type keyvalue() :: #keyvalue_rsa{} | #keyvalue_ec{}. -spec keyvalue_to_pubkey(keyvalue()) -> pubkey(). keyvalue_to_pubkey(#keyvalue_rsa{modulus = ModB64, exponent = ExpB64}) -> Mod = base64:decode(ModB64), Exp = base64:decode(ExpB64), <> = Mod, <> = Exp, #'RSAPublicKey'{modulus = Modulus, publicExponent = Exponent}; keyvalue_to_pubkey(#keyvalue_ec{curve = CurveURI, pubkey = PointB64}) -> CurveOID = case CurveURI of <<"urn:oid:", OidString/binary>> -> list_to_tuple([binary_to_integer(X) || X <- binary:split(OidString, <<".">>, [global])]); _ -> error({unsupported_ec_curve, CurveURI}) end, CurveName = case CurveOID of ?'secp256r1' -> secp256r1; ?'secp384r1' -> secp384r1; ?'secp521r1' -> secp521r1; _ -> CurveOID end, Point = base64:decode(PointB64), {#'ECPoint'{point = Point}, {namedCurve, CurveName}}. -spec pubkey_to_keyvalue(pubkey()) -> keyvalue(). pubkey_to_keyvalue(#'RSAPublicKey'{modulus = Modulus, publicExponent = Exponent}) -> Mod = binary:encode_unsigned(Modulus), Exp = binary:encode_unsigned(Exponent), #keyvalue_rsa{modulus = base64:encode(Mod), exponent = base64:encode(Exp)}; pubkey_to_keyvalue({#'ECPoint'{point = Point}, {namedCurve, CurveName}}) -> CurveOID = case CurveName of prime256v1 -> ?'secp256r1'; secp256r1 -> ?'secp256r1'; secp384r1 -> ?'secp384r1'; secp521r1 -> ?'secp521r1'; _ when is_tuple(CurveName) -> CurveName end, OIDString = iolist_to_binary(lists:join($., [integer_to_binary(X) || X <- tuple_to_list(CurveOID)])), CurveURI = <<"urn:oid:", OIDString/binary>>, #keyvalue_ec{curve = CurveURI, pubkey = base64:encode(Point)}. -spec cert_to_pubkey(#'OTPCertificate'{}) -> pubkey(). cert_to_pubkey(#'OTPCertificate'{} = Cert) -> #'OTPCertificate'{ tbsCertificate = #'OTPTBSCertificate'{subjectPublicKeyInfo = SPKI} } = Cert, #'OTPSubjectPublicKeyInfo'{ algorithm = #'PublicKeyAlgorithm'{ algorithm = AlgoOID, parameters = Params}, subjectPublicKey = PubKey} = SPKI, case AlgoOID of ?'rsaEncryption' -> PubKey; ?'id-ecPublicKey' -> {namedCurve, CurveOID} = Params, case CurveOID of A when is_atom(A) -> {PubKey, {namedCurve, CurveOID}}; ?'secp256r1' -> {PubKey, {namedCurve, secp256r1}}; ?'secp384r1' -> {PubKey, {namedCurve, secp384r1}}; ?'secp521r1' -> {PubKey, {namedCurve, secp521r1}}; _ -> {PubKey, {namedCurve, CurveOID}} end end. -spec key_type(pubkey()) -> xmlrat_dsig_signer:pubkey_algo(). key_type(#'RSAPublicKey'{}) -> rsa; key_type({#'ECPoint'{}, {namedCurve, _}}) -> ecdsa; key_type({I, #'Dss-Parms'{}}) when is_integer(I) -> dsa; key_type(B) when is_binary(B) -> hmac. -spec get_validated_pubkey(verify_options(), xmlrat_dsig_signer:algo(), #keyinfo{}) -> {ok, pubkey()} | {error, term()}. get_validated_pubkey(Opts, SigAlgo = {KeyType, _}, KeyInfo) -> VerMod = maps:get(verifier, Opts, xmlrat_dsig_verifier_stdlib), VerOpts = maps:get(verifier_options, Opts, #{}), case KeyInfo of #keyinfo{name = undefined, certs = undefined, key = undefined} -> {error, insufficient_key_info}; #keyinfo{name = N, certs = undefined, key = undefined} when is_binary(N) -> case VerMod:retrieve_key(VerOpts, #{name => N}, SigAlgo) of {ok, PubKey} -> case key_type(PubKey) of KeyType -> R = VerMod:validate_key(VerOpts, PubKey, #{}, SigAlgo), case R of ok -> {ok, PubKey}; E = {error, _} -> E end; _ -> {error, key_type_mismatch} end; E = {error, _} -> E end; #keyinfo{certs = [CertB64 | _]} -> CertBin = base64:decode(CertB64), Cert = #'OTPCertificate'{} = public_key:pkix_decode_cert(CertBin, otp), PubKey = cert_to_pubkey(Cert), case key_type(PubKey) of KeyType -> case VerMod:validate_cert(VerOpts, Cert, SigAlgo) of ok -> {ok, PubKey}; E = {error, _} -> E end; _ -> {error, key_type_mismatch} end; #keyinfo{name = N, key = KeyValue} -> Details = case N of undefined -> #{}; _ -> #{name => N} end, PubKey = keyvalue_to_pubkey(KeyValue), case key_type(PubKey) of KeyType -> case VerMod:validate_key(VerOpts, PubKey, Details, SigAlgo) of ok -> {ok, PubKey}; E = {error, _} -> E end; _ -> {error, key_type_mismatch} end end. -spec algo_from_uri(uri()) -> xmlrat_dsig_signer:algo(). algo_from_uri(?dsig_RSAwithSHA1) -> {rsa, sha}; algo_from_uri(?dsig_RSAwithSHA256) -> {rsa, sha256}; algo_from_uri(?dsig_RSAwithSHA384) -> {rsa, sha384}; algo_from_uri(?dsig_RSAwithSHA512) -> {rsa, sha512}; algo_from_uri(?dsig_DSAwithSHA1) -> {dsa, sha}; algo_from_uri(?dsig_DSAwithSHA256) -> {dsa, sha256}; algo_from_uri(?dsig_ECDSAwithSHA1) -> {ecdsa, sha1}; algo_from_uri(?dsig_ECDSAwithSHA256) -> {ecdsa, sha256}; algo_from_uri(?dsig_ECDSAwithSHA384) -> {ecdsa, sha384}; algo_from_uri(?dsig_ECDSAwithSHA512) -> {ecdsa, sha512}; algo_from_uri(?dsig_HMAC_SHA1) -> {hmac, sha}; algo_from_uri(?dsig_HMAC_SHA256) -> {hmac, sha256}; algo_from_uri(?dsig_HMAC_SHA384) -> {hmac, sha384}; algo_from_uri(?dsig_HMAC_SHA512) -> {hmac, sha512}; algo_from_uri(URI) -> error({unsupported_algo, URI}). -spec uri_from_algo(xmlrat_dsig_signer:algo()) -> uri(). uri_from_algo({rsa, sha}) -> ?dsig_RSAwithSHA1; uri_from_algo({rsa, sha256}) -> ?dsig_RSAwithSHA256; uri_from_algo({rsa, sha384}) -> ?dsig_RSAwithSHA384; uri_from_algo({rsa, sha512}) -> ?dsig_RSAwithSHA512; uri_from_algo({dsa, sha}) -> ?dsig_DSAwithSHA1; uri_from_algo({dsa, sha256}) -> ?dsig_DSAwithSHA256; uri_from_algo({ecdsa, sha1}) -> ?dsig_ECDSAwithSHA1; uri_from_algo({ecdsa, sha256}) -> ?dsig_ECDSAwithSHA256; uri_from_algo({ecdsa, sha384}) -> ?dsig_ECDSAwithSHA384; uri_from_algo({ecdsa, sha512}) -> ?dsig_ECDSAwithSHA512; uri_from_algo({hmac, sha}) -> ?dsig_HMAC_SHA1; uri_from_algo({hmac, sha256}) -> ?dsig_HMAC_SHA256; uri_from_algo({hmac, sha384}) -> ?dsig_HMAC_SHA384; uri_from_algo({hmac, sha512}) -> ?dsig_HMAC_SHA512; uri_from_algo(Algo) -> error({unsupported_algo, Algo}). -spec hash_algo_from_uri(uri()) -> xmlrat_dsig_signer:hash_algo(). hash_algo_from_uri(?dsig_SHA1) -> sha; hash_algo_from_uri(?dsig_SHA256) -> sha256; hash_algo_from_uri(?dsig_SHA384) -> sha384; hash_algo_from_uri(?dsig_SHA512) -> sha512; hash_algo_from_uri(URI) -> error({unsupported_hash_algo, URI}). -spec uri_from_hash_algo(xmlrat_dsig_signer:hash_algo()) -> uri(). uri_from_hash_algo(sha) -> ?dsig_SHA1; uri_from_hash_algo(sha256) -> ?dsig_SHA256; uri_from_hash_algo(sha384) -> ?dsig_SHA384; uri_from_hash_algo(sha512) -> ?dsig_SHA512; uri_from_hash_algo(Algo) -> error({unsupported_hash_algo, Algo}). %% Performs a comparison between two binaries by computing their SHA512 %% hashes and checking equality between those. This avoids revealing anything %% about matching bits/bytes between the raw binaries in the timing of the %% comparison. -spec hash_compare(binary(), binary()) -> boolean(). hash_compare(A, B) -> DigestA = crypto:hash(sha512, A), DigestB = crypto:hash(sha512, B), (DigestA =:= DigestB). -type id() :: binary(). -type sign_options() :: #{ signer_options => map(), signer => module(), detached => boolean(), signed_elements => [id()], hash_preferences => [xmlrat_dsig_signer:hash_algo()] }. -define(sign_defaults, #{ hash_preferences => [sha512, sha384, sha256, sha], signer => xmlrat_dsig_signer_stdlib, signer_options => #{}, detached => false }). %% @doc Signs an XML document. %% %% Returns either the complete enveloped document with signature (if %% detached is false, the default); or just the %% detached signature document. -spec sign(xmlrat:document(), sign_options()) -> {ok, xmlrat:document()} | {error, term()}. sign(Doc, Opts0) -> Opts1 = maps:merge(?sign_defaults, Opts0), #{signer := SignerMod, signer_options := SignerOpts, hash_preferences := HashPrefs} = Opts1, % First, calculate our references. RefURIs = case Opts1 of #{signed_elements := IDs} -> [<<"#",ID/binary>> || ID <- IDs]; _ -> [<<>>] end, Refs = generate_references(Doc, RefURIs, Opts1), case SignerMod:algorithms(SignerOpts) of {ok, KeyAlgos} -> case select_algo(HashPrefs, KeyAlgos) of {ok, Algo} -> SignedInfo = #signedinfo{c14n_meth = ?dsig_XML_c14n_exc, sig_meth = uri_from_algo(Algo), refs = Refs}, SIDoc = encode_siginfo(SignedInfo), CanonSI = xmlrat_c14n:string(SIDoc), case SignerMod:sign(SignerOpts, CanonSI, Algo) of {ok, Signature} -> {ok, KeyDetailsMap} = SignerMod:key_details( SignerOpts), KeyInfo = generate_keyinfo(KeyDetailsMap, Opts1), Sig = #signature{ info = SignedInfo, sig = base64:encode(Signature), key = KeyInfo }, SigDoc = encode_sig(Sig), #{detached := Detached} = Opts1, case Detached of true -> {ok, SigDoc}; false -> EnvDoc = lists:map(fun (E = #xml_element{content = C0}) -> C1 = C0 ++ SigDoc, E#xml_element{content = C1}; (Other) -> Other end, Doc), {ok, EnvDoc} end; E = {error, _} -> E end; E = {error, _} -> E end; E = {error, _} -> E end. generate_keyinfo(Details, _Opts) -> KI0 = #keyinfo{}, KI1 = case Details of #{name := Name} when is_binary(Name) -> KI0#keyinfo{name = Name}; _ -> KI0 end, KI2 = case Details of #{certificate := #'OTPCertificate'{} = Cert} -> Der = public_key:pkix_encode('Certificate', Cert, otp), KI1#keyinfo{certs = [base64:encode(Der)]}; #{certificate := #'Certificate'{} = Cert} -> Der = public_key:pkix_encode('Certificate', Cert, plain), KI1#keyinfo{certs = [base64:encode(Der)]}; _ -> KI1 end, _KI3 = case Details of #{public_key := PubKey} -> KI2#keyinfo{key = pubkey_to_keyvalue(PubKey)}; _ -> KI2 end. select_algo([], KeyAlgos) -> {error, {no_key_algo_matched_hash_prefs, KeyAlgos}}; select_algo([Hash | Rest], KeyAlgos) -> case first_algo_with_hash(Hash, KeyAlgos) of none -> select_algo(Rest, KeyAlgos); Alg -> {ok, Alg} end. first_algo_with_hash(_Hash, []) -> none; first_algo_with_hash(Hash, [A = {_, AHash} | _]) when (Hash =:= AHash) -> A; first_algo_with_hash(Hash, [_ | Rest]) -> first_algo_with_hash(Hash, Rest). -spec generate_references(xmlrat:document(), [uri() | undefined], sign_options()) -> [#reference{}]. generate_references(_Doc, [], _Opts) -> []; generate_references(Doc, [URI | Rest], Opts) -> #{detached := Detached} = Opts, [RootElem] = [X || X = #xml_element{} <- Doc], {DigestDoc, Enveloped} = case URI of undefined -> {Doc, not Detached}; <<>> -> {Doc, not Detached}; <<"#", ID/binary>> -> SubDoc = match_id(Doc, #{<<"id">> => ID}), % If an ID ref specifies the root element (which will end up as an % ancestor of the Signature element if not detached), then we may % need the enveloped transform. For a random element within it (a % sibling of the Signature element or lower) we don't need it. case SubDoc of [RootElem] -> {SubDoc, not Detached}; _ -> {SubDoc, false} end % TODO: support other forms of the URI? there are other ways to specify % an ID? end, CanonMsg = xmlrat_c14n:string(DigestDoc), XForms = case Enveloped of true -> [#transform{algo = ?dsig_enveloped}]; false -> [] end ++ [#transform{algo = ?dsig_XML_c14n_exc}], #{hash_preferences := [DigestAlgo | _]} = Opts, DigestMethURI = uri_from_hash_algo(DigestAlgo), Digest = crypto:hash(DigestAlgo, CanonMsg), DigestB64 = base64:encode(Digest), [#reference{uri = URI, xforms = XForms, digest_meth = DigestMethURI, digest = DigestB64} | generate_references(Doc, Rest, Opts)]. -define(namespaces, #{ <<"ds">> => ?NS_dsig, <<"ec">> => ?dsig_XML_c14n_exc }). -xpath_record({decode_xform, transform, #{ algo => "/ds:Transform/@Algorithm", force_ns => "/ds:Transform/ec:InclusiveNamespaces/@PrefixList" }, ?namespaces}). -xml_record({encode_xform, transform, "" "" "" "" "", ?namespaces}). -xpath_record({decode_ref, reference, #{ uri => "/ds:Reference/@URI", xforms => "/ds:Reference/ds:Transforms/ds:Transform", digest_meth => "/ds:Reference/ds:DigestMethod/@Algorithm", digest => "/ds:Reference/ds:DigestValue" }, ?namespaces}). -xml_record({encode_ref, reference, "" "" "&xforms;" "" "" "&digest;" "", ?namespaces}). -xpath_record({decode_keyvalue_rsa, keyvalue_rsa, #{ modulus => "/ds:RSAKeyValue/ds:Modulus", exponent => "/ds:RSAKeyValue/ds:Exponent" }, ?namespaces}). -xml_record({encode_keyvalue_rsa, keyvalue_rsa, "" "&modulus;" "&exponent;" "", ?namespaces}). -xpath_record({decode_keyvalue_ec, keyvalue_ec, #{ curve => "/ds:ECKeyValue/ds:NamedCurve/@URI", pubkey => "/ds:ECKeyValue/ds:PublicKey" }, ?namespaces}). -xml_record({encode_keyvalue_ec, keyvalue_ec, "" "" "&pubkey;" "", ?namespaces}). -xpath_record({decode_keyinfo, keyinfo, #{ name => "/ds:KeyInfo/ds:KeyName", certs => "/ds:KeyInfo/ds:X509Data/ds:X509Certificate", key => "/ds:KeyInfo/ds:KeyValue/ds:*" }, ?namespaces}). -xml_record({encode_keyinfo, keyinfo, "" "" "&name;" "" "" "" "" "" "" "" "" "" "" "" "" "" "", ?namespaces}). -xpath_record({decode_siginfo, signedinfo, #{ c14n_meth => "/ds:SignedInfo/ds:CanonicalizationMethod/@Algorithm", sig_meth => "/ds:SignedInfo/ds:SignatureMethod/@Algorithm", refs => "/ds:SignedInfo/ds:Reference" }, ?namespaces}). -xml_record({encode_siginfo, signedinfo, "" "" "" "&refs;" ""}). -xpath_record({decode_sig, signature, #{ id => "/ds:Signature/@ds:Id", info => "/ds:Signature/ds:SignedInfo", sig => "/ds:Signature/ds:SignatureValue", key => "/ds:Signature/ds:KeyInfo" }, ?namespaces}). -xml_record({encode_sig, signature, "" "&info;" "&sig;" "&key;" ""}). -xpath({extract_sig, "/*/ds:Signature", ?namespaces}). -xpath({extract_siginfo, "/ds:Signature/ds:SignedInfo", ?namespaces}). -xpath({strip_sig_kids, "/*/*[not(self::ds:Signature)]", ?namespaces}). strip_sig(Doc) -> NewRootKids = strip_sig_kids(Doc), lists:map(fun (Root = #xml_element{content = _}) -> Root#xml_element{content = NewRootKids}; (Other) -> Other end, Doc). -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). verify_valid_sha1_key_test() -> {ok, Doc} = xmlrat_parse:string(<< "\n" "\n" " \n" " Olá mundo\n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " UWuYTYug10J1k5hKfonxthgrAR8=\n" " \n" " \n" " \n" " TSQUoVrQ0kg1eiltNwIhKPrIdsi1VhWjYNJlXvfQqW2EKk3X37X862SCfrz7v8IYJ7OorWwlFpGDStJDSR6saO\n" " ScqSvmesCrGEEq+U6zegR9nH0lvcGZ8Rvc/y7U9kZrE4fHqEiLyfpmzJyPmWUT9Uta14nPJYsl3cmdThHB8Bs=\n" " \n" " \n" " \n" " \n" " \n" " 4IlzOY3Y9fXoh3Y5f06wBbtTg94Pt6vcfcd1KQ0FLm0S36aGJtTSb6pYKfyX7PqCUQ8wgL6xUJ5GRPEsu9gyz8\n" " ZobwfZsGCsvu40CWoT9fcFBZPfXro1Vtlh/xl/yYHm+Gzqh0Bw76xtLHSfLfpVOrmZdwKmSFKMTvNXOFd0V18=\n" " \n" " AQAB\n" " \n" " \n" " \n" " \n" "\n"/utf8>>), ?assertMatch({ok, _}, verify(Doc, #{ verifier_options => #{ danger_trust_any_key => true } })). verify_valid_sha256_test() -> {ok, Doc} = xmlrat_parse:string(<< "\n" "\n" " \n" " Olá mundo\n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " XmEzFTF6w33nhHfeQqIZKwITz3H2mbBvShxWn+ML/7s=\n" " \n" " \n" " C/NE8YqqNcySFxRazwbxZSUsoAoeqgbEDiVn+yg1zZY1Evb7VMV1MdoDJM39f7L26e9H//br6sjHZn7s+LGCJp9F2ZmCgiJxSOxqy2yCt6perxoKF3MDQmDRnMtglKeWNSBfYZRWEcA64PMMHz5WS5DCIVTcgU7lFzgMpUfYLOs=\n" " \n" " 4IlzOY3Y9fXoh3Y5f06wBbtTg94Pt6vcfcd1KQ0FLm0S36aGJtTSb6pYKfyX7PqCUQ8wgL6xUJ5GRPEsu9gyz8ZobwfZsGCsvu40CWoT9fcFBZPfXro1Vtlh/xl/yYHm+Gzqh0Bw76xtLHSfLfpVOrmZdwKmSFKMTvNXOFd0V18=AQAB\n" " \n" " \n" "\n"/utf8>>), ?assertMatch({ok, _}, verify(Doc, #{ verifier_options => #{ danger_trust_any_key => true } })). verify_valid_sha256_ssh_fp_test() -> {ok, Doc} = xmlrat_parse:string(<< "\n" "\n" " \n" " Olá mundo\n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " XmEzFTF6w33nhHfeQqIZKwITz3H2mbBvShxWn+ML/7s=\n" " \n" " \n" " C/NE8YqqNcySFxRazwbxZSUsoAoeqgbEDiVn+yg1zZY1Evb7VMV1MdoDJM39f7L26e9H//br6sjHZn7s+LGCJp9F2ZmCgiJxSOxqy2yCt6perxoKF3MDQmDRnMtglKeWNSBfYZRWEcA64PMMHz5WS5DCIVTcgU7lFzgMpUfYLOs=\n" " \n" " 4IlzOY3Y9fXoh3Y5f06wBbtTg94Pt6vcfcd1KQ0FLm0S36aGJtTSb6pYKfyX7PqCUQ8wgL6xUJ5GRPEsu9gyz8ZobwfZsGCsvu40CWoT9fcFBZPfXro1Vtlh/xl/yYHm+Gzqh0Bw76xtLHSfLfpVOrmZdwKmSFKMTvNXOFd0V18=AQAB\n" " \n" " \n" "\n"/utf8>>), ?assertMatch({ok, _}, verify(Doc, #{ verifier_options => #{ fingerprints => [ {ssh, sha256, base64:decode(<<"bRnkjhD610D4zSpRu/jV3Wfi4tYZ0r2eO82GbDBwW5Y=">>)}] } })). verify_valid_sha256_spki_fp_test() -> {ok, Doc} = xmlrat_parse:string(<< "\n" "\n" " \n" " Olá mundo\n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " XmEzFTF6w33nhHfeQqIZKwITz3H2mbBvShxWn+ML/7s=\n" " \n" " \n" " C/NE8YqqNcySFxRazwbxZSUsoAoeqgbEDiVn+yg1zZY1Evb7VMV1MdoDJM39f7L26e9H//br6sjHZn7s+LGCJp9F2ZmCgiJxSOxqy2yCt6perxoKF3MDQmDRnMtglKeWNSBfYZRWEcA64PMMHz5WS5DCIVTcgU7lFzgMpUfYLOs=\n" " \n" " 4IlzOY3Y9fXoh3Y5f06wBbtTg94Pt6vcfcd1KQ0FLm0S36aGJtTSb6pYKfyX7PqCUQ8wgL6xUJ5GRPEsu9gyz8ZobwfZsGCsvu40CWoT9fcFBZPfXro1Vtlh/xl/yYHm+Gzqh0Bw76xtLHSfLfpVOrmZdwKmSFKMTvNXOFd0V18=AQAB\n" " \n" " \n" "\n"/utf8>>), ?assertMatch({ok, _}, verify(Doc, #{ verifier_options => #{ fingerprints => [ {spki, sha256, binary:decode_hex(<<"1cfc3d1a7b49c9ebd22cd091ec060397b53a489845ed5bdb976ee638b50ca1b3">>)}] } })). verify_valid_sha1_test() -> {ok, Doc} = xmlrat_parse:string(<< "" "" "" "" "" "" "" "" "" "" "" "" "xPVYXCs5uMMmIbfTiTZ5R5DVhTU=" "" "" "rYk+WAghakHfR9VtpLz3AkMD1xLD1wISfNgch9+i+PC72RqhmfeMCZMkBaw0EO+CTKEoFBQIQaJYlEj8rIG+XN+8HyBV75BrMKZs1rdN+459Rpn2FOOJuHVb2jLDPecC9Ok/DGaNu6lol60hG9di66EZkL8ErQCuCeZqiw9tiXMUPQyVa2GxqT2UeXvJ5YtkNMDweUc3HhEnTG3ovYt1vOZt679w4N0HAwUa9rk40Z12fOTx77BbMICZ9Q4N2m3UbaFU24YHYpHR+WUTiwzXcmdkrHiE5IF37h7rTKAEixD2bTojaefmrobAz0+mBhCqBPcbfNLhLrpT43xhMenjpA==" "" "" "MIIDfTCCAmWgAwIBAgIJANCSQXrTqpDjMA0GCSqGSIb3DQEBBQUAMFUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApRdWVlbnNsYW5kMREwDwYDVQQHDAhCcmlzYmFuZTEMMAoGA1UECgwDRm9vMRAwDgYDVQQDDAdzYW1saWRwMB4XDTEzMDQyOTA2MTAyOVoXDTIzMDQyOTA2MTAyOVowVTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClF1ZWVuc2xhbmQxETAPBgNVBAcMCEJyaXNiYW5lMQwwCgYDVQQKDANGb28xEDAOBgNVBAMMB3NhbWxpZHAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDFhBuEO3fX+FlyT2YYzozxmXNXEmQjksigJSKD4hvsgsyGyl1iLkqNT6IbkuMXoyJG6vXufMNVLoktcLBd6eu6LQwwRjSU62AVCWZhIJP8U6lHqVsxiP90h7/b1zM7Hm9uM9RHtG+nKB7W0xNRihG8BUQOocSaLIMZZXqDPW1h/UvUqmpEzCtT0kJyXX0UAmDHzTYWHt8dqOYdcO2RAlJX0UKnwG1bHjTAfw01lJeOZiF66kH777nStYSElrHXr0NmCO/2gt6ouEnnUqJWDWRzaLbzhMLmGj83lmPgwZCBbIbnbQWLYPQ438EWfEYELq9nSQrgfUmmDPb4rtsQOXqZAgMBAAGjUDBOMB0GA1UdDgQWBBT64y2JSqY96YTYv1QbFyCPp3To/zAfBgNVHSMEGDAWgBT64y2JSqY96YTYv1QbFyCPp3To/zAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQAecr+C4w3LYAU4pCbLAW2BbFWGZRqBAr6ZKZKQrrqSMUJUiRDoKc5FYJrkjl/sGHAe3b5vBrU3lb/hpSiKXVf4/zBP7uqAF75B6LwnMwYpPcXlnRyPngQcdTL5EyQT5vwqv+H3zB64TblMYbsvqm6+1ippRNq4IXQX+3NGTEkhh0xgH+e3wE8BjjiygDu0MqopaIVPemMVQIm3HI+4jmf60bz8GLD1J4dj5CvyW1jQCXu2K2fcS1xJS0FLrxh/QxR0+3prGkYiZeOWE/dHlTTvQLB+NftyamUthVxMFe8dvXMTix/egox+ps2NuO2XTkDaeeRFjUhPhS8SvZO9l0lZ" "" "" "" "blah" "">>), ?assertMatch({ok, _}, verify(Doc, #{ verifier_options => #{ danger_trust_any_cert => true } })). verify_valid_sha1_spki_fp_test() -> {ok, Doc} = xmlrat_parse:string(<< "" "" "" "" "" "" "" "" "" "" "" "" "xPVYXCs5uMMmIbfTiTZ5R5DVhTU=" "" "" "rYk+WAghakHfR9VtpLz3AkMD1xLD1wISfNgch9+i+PC72RqhmfeMCZMkBaw0EO+CTKEoFBQIQaJYlEj8rIG+XN+8HyBV75BrMKZs1rdN+459Rpn2FOOJuHVb2jLDPecC9Ok/DGaNu6lol60hG9di66EZkL8ErQCuCeZqiw9tiXMUPQyVa2GxqT2UeXvJ5YtkNMDweUc3HhEnTG3ovYt1vOZt679w4N0HAwUa9rk40Z12fOTx77BbMICZ9Q4N2m3UbaFU24YHYpHR+WUTiwzXcmdkrHiE5IF37h7rTKAEixD2bTojaefmrobAz0+mBhCqBPcbfNLhLrpT43xhMenjpA==" "" "" "MIIDfTCCAmWgAwIBAgIJANCSQXrTqpDjMA0GCSqGSIb3DQEBBQUAMFUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApRdWVlbnNsYW5kMREwDwYDVQQHDAhCcmlzYmFuZTEMMAoGA1UECgwDRm9vMRAwDgYDVQQDDAdzYW1saWRwMB4XDTEzMDQyOTA2MTAyOVoXDTIzMDQyOTA2MTAyOVowVTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClF1ZWVuc2xhbmQxETAPBgNVBAcMCEJyaXNiYW5lMQwwCgYDVQQKDANGb28xEDAOBgNVBAMMB3NhbWxpZHAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDFhBuEO3fX+FlyT2YYzozxmXNXEmQjksigJSKD4hvsgsyGyl1iLkqNT6IbkuMXoyJG6vXufMNVLoktcLBd6eu6LQwwRjSU62AVCWZhIJP8U6lHqVsxiP90h7/b1zM7Hm9uM9RHtG+nKB7W0xNRihG8BUQOocSaLIMZZXqDPW1h/UvUqmpEzCtT0kJyXX0UAmDHzTYWHt8dqOYdcO2RAlJX0UKnwG1bHjTAfw01lJeOZiF66kH777nStYSElrHXr0NmCO/2gt6ouEnnUqJWDWRzaLbzhMLmGj83lmPgwZCBbIbnbQWLYPQ438EWfEYELq9nSQrgfUmmDPb4rtsQOXqZAgMBAAGjUDBOMB0GA1UdDgQWBBT64y2JSqY96YTYv1QbFyCPp3To/zAfBgNVHSMEGDAWgBT64y2JSqY96YTYv1QbFyCPp3To/zAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQAecr+C4w3LYAU4pCbLAW2BbFWGZRqBAr6ZKZKQrrqSMUJUiRDoKc5FYJrkjl/sGHAe3b5vBrU3lb/hpSiKXVf4/zBP7uqAF75B6LwnMwYpPcXlnRyPngQcdTL5EyQT5vwqv+H3zB64TblMYbsvqm6+1ippRNq4IXQX+3NGTEkhh0xgH+e3wE8BjjiygDu0MqopaIVPemMVQIm3HI+4jmf60bz8GLD1J4dj5CvyW1jQCXu2K2fcS1xJS0FLrxh/QxR0+3prGkYiZeOWE/dHlTTvQLB+NftyamUthVxMFe8dvXMTix/egox+ps2NuO2XTkDaeeRFjUhPhS8SvZO9l0lZ" "" "" "" "blah" "">>), ?assertMatch({ok, _}, verify(Doc, #{ verifier_options => #{ fingerprints => [{spki, sha512, base64:decode(<<"zl1Hj2NNr68sGkFHsTPNCPc/P6afxuvZl7NXqECqwLrXgzX7fDuvUhVCLqtzq4NQ3w95QdARO+wwKDOywleWiQ==">>)}] } })). verify_valid_sha1_x509_fp_test() -> {ok, Doc} = xmlrat_parse:string(<< "" "" "" "" "" "" "" "" "" "" "" "" "xPVYXCs5uMMmIbfTiTZ5R5DVhTU=" "" "" "rYk+WAghakHfR9VtpLz3AkMD1xLD1wISfNgch9+i+PC72RqhmfeMCZMkBaw0EO+CTKEoFBQIQaJYlEj8rIG+XN+8HyBV75BrMKZs1rdN+459Rpn2FOOJuHVb2jLDPecC9Ok/DGaNu6lol60hG9di66EZkL8ErQCuCeZqiw9tiXMUPQyVa2GxqT2UeXvJ5YtkNMDweUc3HhEnTG3ovYt1vOZt679w4N0HAwUa9rk40Z12fOTx77BbMICZ9Q4N2m3UbaFU24YHYpHR+WUTiwzXcmdkrHiE5IF37h7rTKAEixD2bTojaefmrobAz0+mBhCqBPcbfNLhLrpT43xhMenjpA==" "" "" "MIIDfTCCAmWgAwIBAgIJANCSQXrTqpDjMA0GCSqGSIb3DQEBBQUAMFUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApRdWVlbnNsYW5kMREwDwYDVQQHDAhCcmlzYmFuZTEMMAoGA1UECgwDRm9vMRAwDgYDVQQDDAdzYW1saWRwMB4XDTEzMDQyOTA2MTAyOVoXDTIzMDQyOTA2MTAyOVowVTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClF1ZWVuc2xhbmQxETAPBgNVBAcMCEJyaXNiYW5lMQwwCgYDVQQKDANGb28xEDAOBgNVBAMMB3NhbWxpZHAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDFhBuEO3fX+FlyT2YYzozxmXNXEmQjksigJSKD4hvsgsyGyl1iLkqNT6IbkuMXoyJG6vXufMNVLoktcLBd6eu6LQwwRjSU62AVCWZhIJP8U6lHqVsxiP90h7/b1zM7Hm9uM9RHtG+nKB7W0xNRihG8BUQOocSaLIMZZXqDPW1h/UvUqmpEzCtT0kJyXX0UAmDHzTYWHt8dqOYdcO2RAlJX0UKnwG1bHjTAfw01lJeOZiF66kH777nStYSElrHXr0NmCO/2gt6ouEnnUqJWDWRzaLbzhMLmGj83lmPgwZCBbIbnbQWLYPQ438EWfEYELq9nSQrgfUmmDPb4rtsQOXqZAgMBAAGjUDBOMB0GA1UdDgQWBBT64y2JSqY96YTYv1QbFyCPp3To/zAfBgNVHSMEGDAWgBT64y2JSqY96YTYv1QbFyCPp3To/zAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQAecr+C4w3LYAU4pCbLAW2BbFWGZRqBAr6ZKZKQrrqSMUJUiRDoKc5FYJrkjl/sGHAe3b5vBrU3lb/hpSiKXVf4/zBP7uqAF75B6LwnMwYpPcXlnRyPngQcdTL5EyQT5vwqv+H3zB64TblMYbsvqm6+1ippRNq4IXQX+3NGTEkhh0xgH+e3wE8BjjiygDu0MqopaIVPemMVQIm3HI+4jmf60bz8GLD1J4dj5CvyW1jQCXu2K2fcS1xJS0FLrxh/QxR0+3prGkYiZeOWE/dHlTTvQLB+NftyamUthVxMFe8dvXMTix/egox+ps2NuO2XTkDaeeRFjUhPhS8SvZO9l0lZ" "" "" "" "blah" "">>), ?assertMatch({ok, _}, verify(Doc, #{ verifier_options => #{ fingerprints => [{x509, sha, base64:decode(<<"xlYKtnfxFAPGWCMqkUz7cTQV9pw=">>)}] } })). verify_valid_sha1_x509_ca_test() -> {ok, Doc} = xmlrat_parse:string(<< "" "" "" "" "" "" "" "" "" "" "" "" "xPVYXCs5uMMmIbfTiTZ5R5DVhTU=" "" "" "rYk+WAghakHfR9VtpLz3AkMD1xLD1wISfNgch9+i+PC72RqhmfeMCZMkBaw0EO+CTKEoFBQIQaJYlEj8rIG+XN+8HyBV75BrMKZs1rdN+459Rpn2FOOJuHVb2jLDPecC9Ok/DGaNu6lol60hG9di66EZkL8ErQCuCeZqiw9tiXMUPQyVa2GxqT2UeXvJ5YtkNMDweUc3HhEnTG3ovYt1vOZt679w4N0HAwUa9rk40Z12fOTx77BbMICZ9Q4N2m3UbaFU24YHYpHR+WUTiwzXcmdkrHiE5IF37h7rTKAEixD2bTojaefmrobAz0+mBhCqBPcbfNLhLrpT43xhMenjpA==" "" "" "MIIDfTCCAmWgAwIBAgIJANCSQXrTqpDjMA0GCSqGSIb3DQEBBQUAMFUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApRdWVlbnNsYW5kMREwDwYDVQQHDAhCcmlzYmFuZTEMMAoGA1UECgwDRm9vMRAwDgYDVQQDDAdzYW1saWRwMB4XDTEzMDQyOTA2MTAyOVoXDTIzMDQyOTA2MTAyOVowVTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClF1ZWVuc2xhbmQxETAPBgNVBAcMCEJyaXNiYW5lMQwwCgYDVQQKDANGb28xEDAOBgNVBAMMB3NhbWxpZHAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDFhBuEO3fX+FlyT2YYzozxmXNXEmQjksigJSKD4hvsgsyGyl1iLkqNT6IbkuMXoyJG6vXufMNVLoktcLBd6eu6LQwwRjSU62AVCWZhIJP8U6lHqVsxiP90h7/b1zM7Hm9uM9RHtG+nKB7W0xNRihG8BUQOocSaLIMZZXqDPW1h/UvUqmpEzCtT0kJyXX0UAmDHzTYWHt8dqOYdcO2RAlJX0UKnwG1bHjTAfw01lJeOZiF66kH777nStYSElrHXr0NmCO/2gt6ouEnnUqJWDWRzaLbzhMLmGj83lmPgwZCBbIbnbQWLYPQ438EWfEYELq9nSQrgfUmmDPb4rtsQOXqZAgMBAAGjUDBOMB0GA1UdDgQWBBT64y2JSqY96YTYv1QbFyCPp3To/zAfBgNVHSMEGDAWgBT64y2JSqY96YTYv1QbFyCPp3To/zAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQAecr+C4w3LYAU4pCbLAW2BbFWGZRqBAr6ZKZKQrrqSMUJUiRDoKc5FYJrkjl/sGHAe3b5vBrU3lb/hpSiKXVf4/zBP7uqAF75B6LwnMwYpPcXlnRyPngQcdTL5EyQT5vwqv+H3zB64TblMYbsvqm6+1ippRNq4IXQX+3NGTEkhh0xgH+e3wE8BjjiygDu0MqopaIVPemMVQIm3HI+4jmf60bz8GLD1J4dj5CvyW1jQCXu2K2fcS1xJS0FLrxh/QxR0+3prGkYiZeOWE/dHlTTvQLB+NftyamUthVxMFe8dvXMTix/egox+ps2NuO2XTkDaeeRFjUhPhS8SvZO9l0lZ" "" "" "" "blah" "">>), Der = base64:decode(<<"MIIDfTCCAmWgAwIBAgIJANCSQXrTqpDjMA0GCSqGSIb3DQEBBQUAMFUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApRdWVlbnNsYW5kMREwDwYDVQQHDAhCcmlzYmFuZTEMMAoGA1UECgwDRm9vMRAwDgYDVQQDDAdzYW1saWRwMB4XDTEzMDQyOTA2MTAyOVoXDTIzMDQyOTA2MTAyOVowVTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClF1ZWVuc2xhbmQxETAPBgNVBAcMCEJyaXNiYW5lMQwwCgYDVQQKDANGb28xEDAOBgNVBAMMB3NhbWxpZHAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDFhBuEO3fX+FlyT2YYzozxmXNXEmQjksigJSKD4hvsgsyGyl1iLkqNT6IbkuMXoyJG6vXufMNVLoktcLBd6eu6LQwwRjSU62AVCWZhIJP8U6lHqVsxiP90h7/b1zM7Hm9uM9RHtG+nKB7W0xNRihG8BUQOocSaLIMZZXqDPW1h/UvUqmpEzCtT0kJyXX0UAmDHzTYWHt8dqOYdcO2RAlJX0UKnwG1bHjTAfw01lJeOZiF66kH777nStYSElrHXr0NmCO/2gt6ouEnnUqJWDWRzaLbzhMLmGj83lmPgwZCBbIbnbQWLYPQ438EWfEYELq9nSQrgfUmmDPb4rtsQOXqZAgMBAAGjUDBOMB0GA1UdDgQWBBT64y2JSqY96YTYv1QbFyCPp3To/zAfBgNVHSMEGDAWgBT64y2JSqY96YTYv1QbFyCPp3To/zAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQAecr+C4w3LYAU4pCbLAW2BbFWGZRqBAr6ZKZKQrrqSMUJUiRDoKc5FYJrkjl/sGHAe3b5vBrU3lb/hpSiKXVf4/zBP7uqAF75B6LwnMwYpPcXlnRyPngQcdTL5EyQT5vwqv+H3zB64TblMYbsvqm6+1ippRNq4IXQX+3NGTEkhh0xgH+e3wE8BjjiygDu0MqopaIVPemMVQIm3HI+4jmf60bz8GLD1J4dj5CvyW1jQCXu2K2fcS1xJS0FLrxh/QxR0+3prGkYiZeOWE/dHlTTvQLB+NftyamUthVxMFe8dvXMTix/egox+ps2NuO2XTkDaeeRFjUhPhS8SvZO9l0lZ">>), Cert = public_key:pkix_decode_cert(Der, otp), ?assertMatch({ok, _}, verify(Doc, #{ verifier_options => #{ ca_certs => [Cert] } })). verify_valid_sha1_no_fp_test() -> {ok, Doc} = xmlrat_parse:string(<< "" "" "" "" "" "" "" "" "" "" "" "" "xPVYXCs5uMMmIbfTiTZ5R5DVhTU=" "" "" "rYk+WAghakHfR9VtpLz3AkMD1xLD1wISfNgch9+i+PC72RqhmfeMCZMkBaw0EO+CTKEoFBQIQaJYlEj8rIG+XN+8HyBV75BrMKZs1rdN+459Rpn2FOOJuHVb2jLDPecC9Ok/DGaNu6lol60hG9di66EZkL8ErQCuCeZqiw9tiXMUPQyVa2GxqT2UeXvJ5YtkNMDweUc3HhEnTG3ovYt1vOZt679w4N0HAwUa9rk40Z12fOTx77BbMICZ9Q4N2m3UbaFU24YHYpHR+WUTiwzXcmdkrHiE5IF37h7rTKAEixD2bTojaefmrobAz0+mBhCqBPcbfNLhLrpT43xhMenjpA==" "" "" "MIIDfTCCAmWgAwIBAgIJANCSQXrTqpDjMA0GCSqGSIb3DQEBBQUAMFUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApRdWVlbnNsYW5kMREwDwYDVQQHDAhCcmlzYmFuZTEMMAoGA1UECgwDRm9vMRAwDgYDVQQDDAdzYW1saWRwMB4XDTEzMDQyOTA2MTAyOVoXDTIzMDQyOTA2MTAyOVowVTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClF1ZWVuc2xhbmQxETAPBgNVBAcMCEJyaXNiYW5lMQwwCgYDVQQKDANGb28xEDAOBgNVBAMMB3NhbWxpZHAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDFhBuEO3fX+FlyT2YYzozxmXNXEmQjksigJSKD4hvsgsyGyl1iLkqNT6IbkuMXoyJG6vXufMNVLoktcLBd6eu6LQwwRjSU62AVCWZhIJP8U6lHqVsxiP90h7/b1zM7Hm9uM9RHtG+nKB7W0xNRihG8BUQOocSaLIMZZXqDPW1h/UvUqmpEzCtT0kJyXX0UAmDHzTYWHt8dqOYdcO2RAlJX0UKnwG1bHjTAfw01lJeOZiF66kH777nStYSElrHXr0NmCO/2gt6ouEnnUqJWDWRzaLbzhMLmGj83lmPgwZCBbIbnbQWLYPQ438EWfEYELq9nSQrgfUmmDPb4rtsQOXqZAgMBAAGjUDBOMB0GA1UdDgQWBBT64y2JSqY96YTYv1QbFyCPp3To/zAfBgNVHSMEGDAWgBT64y2JSqY96YTYv1QbFyCPp3To/zAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQAecr+C4w3LYAU4pCbLAW2BbFWGZRqBAr6ZKZKQrrqSMUJUiRDoKc5FYJrkjl/sGHAe3b5vBrU3lb/hpSiKXVf4/zBP7uqAF75B6LwnMwYpPcXlnRyPngQcdTL5EyQT5vwqv+H3zB64TblMYbsvqm6+1ippRNq4IXQX+3NGTEkhh0xgH+e3wE8BjjiygDu0MqopaIVPemMVQIm3HI+4jmf60bz8GLD1J4dj5CvyW1jQCXu2K2fcS1xJS0FLrxh/QxR0+3prGkYiZeOWE/dHlTTvQLB+NftyamUthVxMFe8dvXMTix/egox+ps2NuO2XTkDaeeRFjUhPhS8SvZO9l0lZ" "" "" "" "blah" "">>), ?assertMatch({error, _}, verify(Doc, #{ verifier_options => #{ fingerprints => [{spki, sha256, <<>>}] } })). parse_key_rsa_test() -> {ok, Doc} = xmlrat_parse:string(<< "\n" "\n" "\n" "4IlzOY3Y9fXoh3Y5f06wBbtTg94Pt6vcfcd1KQ0FLm0S36aGJtTSb6pYKfyX7PqCUQ8wgL6xUJ5GRPEsu9gyz8\n" "ZobwfZsGCsvu40CWoT9fcFBZPfXro1Vtlh/xl/yYHm+Gzqh0Bw76xtLHSfLfpVOrmZdwKmSFKMTvNXOFd0V18=\n" "\n" "AQAB\n" "\n" "\n">>), R = parse_key(Doc), ?assertMatch({ok, _}, R), {ok, KD} = R, ?assertMatch(#{public_key := #'RSAPublicKey'{}}, KD). parse_key_ec_test() -> {ok, Doc} = xmlrat_parse:string(<< "\n" "some key\n" "\n" "\n" "j5sLNXdJgD7ye531XlyUb2exL8I=\n" "\n" "\n" "\n" "\n">>), R = parse_key(Doc), ?assertMatch({ok, _}, R), {ok, KD} = R, ?assertMatch(#{name := <<"some key">>, public_key := {#'ECPoint'{}, {namedCurve, secp256r1}}}, KD). parse_key_cert_test() -> {ok, Doc} = xmlrat_parse:string(<< "" "" "MIIDfTCCAmWgAwIBAgIJANCSQXrTqpDjMA0GCSqGSIb3DQEBBQUAMFUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApRdWVlbnNsYW5kMREwDwYDVQQHDAhCcmlzYmFuZTEMMAoGA1UECgwDRm9vMRAwDgYDVQQDDAdzYW1saWRwMB4XDTEzMDQyOTA2MTAyOVoXDTIzMDQyOTA2MTAyOVowVTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClF1ZWVuc2xhbmQxETAPBgNVBAcMCEJyaXNiYW5lMQwwCgYDVQQKDANGb28xEDAOBgNVBAMMB3NhbWxpZHAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDFhBuEO3fX+FlyT2YYzozxmXNXEmQjksigJSKD4hvsgsyGyl1iLkqNT6IbkuMXoyJG6vXufMNVLoktcLBd6eu6LQwwRjSU62AVCWZhIJP8U6lHqVsxiP90h7/b1zM7Hm9uM9RHtG+nKB7W0xNRihG8BUQOocSaLIMZZXqDPW1h/UvUqmpEzCtT0kJyXX0UAmDHzTYWHt8dqOYdcO2RAlJX0UKnwG1bHjTAfw01lJeOZiF66kH777nStYSElrHXr0NmCO/2gt6ouEnnUqJWDWRzaLbzhMLmGj83lmPgwZCBbIbnbQWLYPQ438EWfEYELq9nSQrgfUmmDPb4rtsQOXqZAgMBAAGjUDBOMB0GA1UdDgQWBBT64y2JSqY96YTYv1QbFyCPp3To/zAfBgNVHSMEGDAWgBT64y2JSqY96YTYv1QbFyCPp3To/zAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQAecr+C4w3LYAU4pCbLAW2BbFWGZRqBAr6ZKZKQrrqSMUJUiRDoKc5FYJrkjl/sGHAe3b5vBrU3lb/hpSiKXVf4/zBP7uqAF75B6LwnMwYpPcXlnRyPngQcdTL5EyQT5vwqv+H3zB64TblMYbsvqm6+1ippRNq4IXQX+3NGTEkhh0xgH+e3wE8BjjiygDu0MqopaIVPemMVQIm3HI+4jmf60bz8GLD1J4dj5CvyW1jQCXu2K2fcS1xJS0FLrxh/QxR0+3prGkYiZeOWE/dHlTTvQLB+NftyamUthVxMFe8dvXMTix/egox+ps2NuO2XTkDaeeRFjUhPhS8SvZO9l0lZ" "" "">>), R = parse_key(Doc), ?assertMatch({ok, _}, R), {ok, KD} = R, ?assertMatch(#{public_key := #'RSAPublicKey'{}, certificate := #'OTPCertificate'{}}, KD), {ok, Doc2} = generate_key(KD), ?assertMatch({ok, KD}, parse_key(Doc2)). verify_valid_complicated_test() -> {ok, Doc} = xmlrat_parse:string(<< "\n" "\n" "\n" "]>\n" "\n" "\txyz\n" "\tpqr\n" "\tabc\n" "\t456\n" "\t123\n" "\t789\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "d6Pzi4DPNG6uc9KqTxdnm1zdLSA=\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "j5sLNXdJgD7ye531XlyUb2exL8I=\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "yn5TDgwmUdYwr7PtVYsz2XuwUww=\n" "\n" "\n" "ErZz53i1O2IjsFg2RJKugaEYkwv8jAcjyu484bViJczBchVCwsKGCGQ830YCkHxDIGIWWcaiMYGUFmkndy9iTRUi4csn3TIpQTzQd/3tIUWQ8xiZRytriL5cMLWtjnhFRp7tuB28/h6DkjWKm5c0m7mlBhDOZVEk6Wu5PjhwD8E=\n" "\n" "\n" "\n" "4IlzOY3Y9fXoh3Y5f06wBbtTg94Pt6vcfcd1KQ0FLm0S36aGJtTSb6pYKfyX7PqCUQ8wgL6xUJ5GRPEsu9gyz8\n" "ZobwfZsGCsvu40CWoT9fcFBZPfXro1Vtlh/xl/yYHm+Gzqh0Bw76xtLHSfLfpVOrmZdwKmSFKMTvNXOFd0V18=\n" "\n" "AQAB\n" "\n" "\n" "\n" "\n">>), ?assertMatch({ok, _}, verify(Doc, #{ verifier_options => #{ danger_trust_any_key => true } })). verify_invalid_sha256_test() -> {ok, Doc} = xmlrat_parse:string(<< "\n" "\n" " \n" " Ola mundo\n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " XmEzFTF6w33nhHfeQqIZKwITz3H2mbBvShxWn+ML/7s=\n" " \n" " \n" " C/NE8YqqNcySFxRazwbxZSUsoAoeqgbEDiVn+yg1zZY1Evb7VMV1MdoDJM39f7L26e9H//br6sjHZn7s+LGCJp9F2ZmCgiJxSOxqy2yCt6perxoKF3MDQmDRnMtglKeWNSBfYZRWEcA64PMMHz5WS5DCIVTcgU7lFzgMpUfYLOs=\n" " \n" " 4IlzOY3Y9fXoh3Y5f06wBbtTg94Pt6vcfcd1KQ0FLm0S36aGJtTSb6pYKfyX7PqCUQ8wgL6xUJ5GRPEsu9gyz8ZobwfZsGCsvu40CWoT9fcFBZPfXro1Vtlh/xl/yYHm+Gzqh0Bw76xtLHSfLfpVOrmZdwKmSFKMTvNXOFd0V18=AQAB\n" " \n" " \n" "\n"/utf8>>), ?assertMatch({error, invalid_digest}, verify(Doc, #{ verifier_options => #{ danger_trust_any_key => true } })). end_to_end_test() -> Key = public_key:generate_key({rsa, 2048, 16#10001}), {ok, Doc} = xmlrat_parse:string(<< "\n" "\n" " 123\n", " \n\n" " abc123\n" " \n" "">>), [Root] = [E || E = #xml_element{} <- Doc], Res = xmlrat_dsig:sign(Doc, #{ signer_options => #{private_key => Key}}), ?assertMatch({ok, _}, Res), {ok, SigDoc} = Res, ?assertMatch([#xml_element{tag = {_, <<"Signature">>, _}}], extract_sig(SigDoc)), VRes = verify(SigDoc, #{ verifier_options => #{danger_trust_any_key => true}}), ?assertMatch({ok, [Root]}, VRes), SigDoc2 = lists:map(fun (E = #xml_element{attributes = A0}) -> A1 = A0 ++ [#xml_attribute{name = <<"test">>, value = <<"aaaa">>}], E#xml_element{attributes = A1}; (Other) -> Other end, SigDoc), VRes2 = verify(SigDoc2, #{ verifier_options => #{danger_trust_any_key => true}}), ?assertMatch({error, invalid_digest}, VRes2). partial_end_to_end_test() -> Key = public_key:generate_key({rsa, 2048, 16#10001}), {ok, Doc} = xmlrat_parse:string(<< "\n" "\n" " 123\n", " \n\n" " abc123\n" " \n" "">>), {ok, [Bar]} = xmlrat_xpath:run("/doc/bar", Doc), Res = xmlrat_dsig:sign(Doc, #{ signer_options => #{private_key => Key}, signed_elements => [<<"bar">>] }), ?assertMatch({ok, _}, Res), {ok, SigDoc} = Res, ?assertMatch([#xml_element{tag = {_, <<"Signature">>, _}}], extract_sig(SigDoc)), VRes = verify(SigDoc, #{ verifier_options => #{danger_trust_any_key => true}}), Root = #xml_element{tag = {default, <<"doc">>, <<"urn:doc:doc">>}, content = [Bar]}, ?assertMatch({ok, [Root]}, VRes), SigDoc2 = lists:map(fun (E = #xml_element{attributes = A0}) -> A1 = A0 ++ [#xml_attribute{name = <<"test">>, value = <<"aaaa">>}], E#xml_element{attributes = A1}; (Other) -> Other end, SigDoc), VRes2 = verify(SigDoc2, #{ verifier_options => #{danger_trust_any_key => true}}), ?assertMatch({ok, [Root]}, VRes2). -endif.