public inbox for git-commits@fedoraproject.org
help / color / mirror / Atom feed
From: Peter Lemenkov <lemenkov@gmail.com>
To: git-commits@fedoraproject.org
Subject: [rpms/erlang] f43: Backported fixes for CVE-2026-58227 and CVE-2026-59251
Date: Mon, 07 Sep 2026 18:40:40 GMT	[thread overview]
Message-ID: <178880644075.1.1612306384745076751.rpms-erlang-25259b955c41@fedoraproject.org> (raw)

            A new commit has been pushed.

            Repo   : rpms/erlang
            Branch : f43
            Commit : 25259b955c4124303090929fb3fac38f19d7e8b6
            Author : Peter Lemenkov <lemenkov@gmail.com>
            Date   : 2026-09-07T19:36:13+02:00
            Stats  : +955/-1 in 3 file(s)
            URL    : https://src.fedoraproject.org/rpms/erlang/c/25259b955c4124303090929fb3fac38f19d7e8b6?branch=f43

            Log:
            Backported fixes for CVE-2026-58227 and CVE-2026-59251

Signed-off-by: Peter Lemenkov <lemenkov@gmail.com>

---
diff --git a/erlang.spec b/erlang.spec
index c94ed0e..5481d2b 100644
--- a/erlang.spec
+++ b/erlang.spec
@@ -70,7 +70,7 @@
 
 Name:		erlang
 Version:	26.2.5.21
-Release:	6%{?dist}
+Release:	7%{?dist}
 Summary:	General-purpose programming language and runtime environment
 
 License:	Apache-2.0
@@ -117,6 +117,8 @@ Patch15: otp-0015-ssl-TLS-Client-hardening.patch
 Patch16: otp-0016-ssl-Add-PSK-parameter-check.patch
 Patch17: otp-0017-ssl-Add-pre-TLS-1.3-client-cipher-suite-check.patch
 Patch18: otp-0018-inets-Fix-rejection-of-invalid-chunk-sizes.patch
+Patch19: otp-0019-ssl-Use-digraph-to-ensure-robust-cert-chain-building.patch
+Patch20: otp-0020-public_key-Cap-policy-tree-growth-to-prevent-DoS.patch
 # end of autogenerated patch tag list
 
 BuildRequires:	gcc
@@ -1965,6 +1967,9 @@ ERL_TOP=${ERL_TOP} make TARGET=${TARGET} release_tests
 
 
 %changelog
+* Mon Sep  7 2026 Peter Lemenkov <lemenkov@gmail.com> - 26.2.5.21-7
+- Backported fixes for CVE-2026-58227 and CVE-2026-59251
+
 * Mon Sep  7 2026 Peter Lemenkov <lemenkov@gmail.com> - 26.2.5.21-6
 - Backport fix for CVE-2026-55953
 

diff --git a/otp-0019-ssl-Use-digraph-to-ensure-robust-cert-chain-building.patch b/otp-0019-ssl-Use-digraph-to-ensure-robust-cert-chain-building.patch
new file mode 100644
index 0000000..a01725b
--- /dev/null
+++ b/otp-0019-ssl-Use-digraph-to-ensure-robust-cert-chain-building.patch
@@ -0,0 +1,368 @@
+From: Ingela Anderton Andin <ingela@erlang.org>
+Date: Fri, 3 Jul 2026 17:16:55 +0200
+Subject: [PATCH] ssl: Use digraph to ensure robust cert chain building.
+
+Handling of extraneous and unordered certs could cause
+cycles causing a possible DoS attack. Also add an
+in depth length check of built chains.
+
+diff --git a/lib/ssl/src/ssl_certificate.erl b/lib/ssl/src/ssl_certificate.erl
+index 265d74d45e..b232967a6a 100644
+--- a/lib/ssl/src/ssl_certificate.erl
++++ b/lib/ssl/src/ssl_certificate.erl
+@@ -88,6 +88,8 @@
+ %% Tracing
+ -export([handle_trace/3]).
+ 
++-define(MAX_CHAIN, 12). %% In depth a little longer than default MAX_DEPTH
++
+ %%====================================================================
+ %% Internal application API
+ %%====================================================================
+@@ -399,8 +401,11 @@ chain_result(Root0, Chain0, both) ->
+     {DRoot, DChain} = decoded_chain(Root0, Chain0),
+     {ok, {ERoot, EChain}, {DRoot, DChain}}.
+ 
+-build_certificate_chain(#cert{otp=OtpCert}=Cert, CertDbHandle, CertsDbRef, Chain, ListDb) ->
+-    IssuerAndSelfSigned = 
++
++build_certificate_chain(_,_,_,Chain,_) when length(Chain) >= ?MAX_CHAIN->
++    {ok, undefined, lists:reverse(Chain)};
++build_certificate_chain(#cert{otp = OtpCert} = Cert, CertDbHandle, CertsDbRef, Chain, ListDb) ->
++    IssuerAndSelfSigned =
+ 	case public_key:pkix_is_self_signed(OtpCert) of
+ 	    true ->
+ 		{public_key:pkix_issuer_id(OtpCert, self), true};
+@@ -422,7 +427,7 @@ build_certificate_chain(#cert{otp=OtpCert}=Cert, CertDbHandle, CertsDbRef, Chain
+ 		    %% incorrect.
+ 		    {ok, undefined, lists:reverse(Chain)}
+ 	    end;
+-	{{ok, {SerialNr, Issuer}}, SelfSigned} -> 
++	{{ok, {SerialNr, Issuer}}, SelfSigned} ->
+ 	    do_certificate_chain(CertDbHandle, CertsDbRef, Chain, SerialNr, Issuer, SelfSigned, ListDb)
+     end.
+ 
+@@ -432,8 +437,13 @@ do_certificate_chain(_, _, [RootCert | _] = Chain, _, _, true, _) ->
+ do_certificate_chain(CertDbHandle, CertsDbRef, Chain, SerialNr, Issuer, _, ListDb) ->
+     case ssl_manager:lookup_trusted_cert(CertDbHandle, CertsDbRef,
+                                          SerialNr, Issuer) of
+-	{ok, Cert} ->
+-	    build_certificate_chain(Cert, CertDbHandle, CertsDbRef, [Cert | Chain], ListDb);
++	{ok, #cert{der = Der} = Cert} ->
++            case lists:any(fun(#cert{der = D}) -> D =:= Der end, Chain) of
++                true ->
++                    {ok, undefined, lists:reverse(Chain)};
++                false ->
++                    build_certificate_chain(Cert, CertDbHandle, CertsDbRef, [Cert | Chain], ListDb)
++	    end;
+ 	_ ->
+ 	    %% The trusted cert may be obmitted from the chain as the
+ 	    %% counter part needs to have it anyway to be able to
+@@ -482,7 +492,7 @@ find_issuer(#cert{der=DerCert, otp=OtpCert}, CertDbHandle, CertsDbRef, ListDb, I
+     Result = case is_reference(CertsDbRef) of
+ 		 true when ListDb == [] ->
+                      CertEntryList = ssl_pkix_db:select_certentries_by_ref(CertsDbRef, CertDbHandle),
+-		     do_find_issuer(IsIssuerFun, CertDbHandle, CertEntryList); 
++		     do_find_issuer(IsIssuerFun, CertDbHandle, CertEntryList);
+ 		 false when ListDb == [] ->
+ 		     {extracted, CertsData} = CertsDbRef,
+ 		     CertEntryList = [Entry || {decoded, Entry} <- CertsData],
+@@ -499,7 +509,7 @@ find_issuer(#cert{der=DerCert, otp=OtpCert}, CertDbHandle, CertsDbRef, ListDb, I
+ 
+ 
+ do_find_issuer(IssuerFun, CertDbHandle, CertDb) ->
+-    try 
++    try
+ 	foldl_db(IssuerFun, CertDbHandle, CertDb)
+     catch
+ 	throw:{ok, _} = Return ->
+@@ -664,27 +674,63 @@ paths([#cert{otp=C1}=Cert1, #cert{otp=C2}=Cert2 | Rest], Chain, CertDbHandle, Pa
+             %% Chain ordered so far
+             paths([Cert2 | Rest], Chain, CertDbHandle, [Cert1 | Path]);
+         false ->
+-            %% Chain is unorded and/or contains extraneous certificates
+-            unorded_or_extraneous(Chain, CertDbHandle)
++            %% Chain is unordered and/or contains extraneous certificates
++            unorded_or_extraneous(Chain)
+     end.
+ 
+-unorded_or_extraneous([Peer | UnorderedChain], CertDbHandle) ->
+-    ChainCandidates = extraneous_chains(UnorderedChain),
+-    lists:map(fun(Candidate) ->
+-                      path_candidate(Peer, Candidate, CertDbHandle)
++unorded_or_extraneous([Peer | ChainCerts]) ->
++    G = digraph:new([acyclic]),
++    try
++        Certs = [Peer | ChainCerts],
++        lists:foreach(fun(Cert) ->
++            digraph:add_vertex(G, cert_id(Cert), Cert)
++        end, Certs),
++
++        Add = fun(#cert{otp = C1, der = C1Der} = Cert1, #cert{otp = C2} = Cert2) ->
++                      case Cert1 =/= Cert2 andalso public_key:pkix_is_issuer(C1, C2) of
++                          true ->
++                              %% Claim: C2 issued C1 so verify C1's signature with C2's key
++                              Signer = C2#'OTPCertificate'.tbsCertificate,
++                              case verify_cert_signer(C1Der, Signer) of
++                                  true ->
++                                      digraph:add_edge(G, cert_id(Cert1), cert_id(Cert2));
++                                  false ->
++                                      false
++                              end;
++                          _ ->
++                              false
++                      end
+               end,
+-              ChainCandidates).
+-
+-path_candidate(Cert, ChainCandidateCAs, CertDbHandle) ->
+-    {ok,  ExtractedCerts} = ssl_pkix_db:extract_trusted_certs({der_otp, ChainCandidateCAs}),
+-    %% certificate_chain/4 will make sure the chain is ordered
+-    case build_certificate_chain(Cert, CertDbHandle, ExtractedCerts, [Cert], []) of
+-        {ok, undefined, Chain} ->
+-            lists:reverse(Chain);
+-        {ok, Root, Chain} ->
+-            [Root | lists:reverse(Chain)]
++
++        _ = [Add(C1, C2) || C1 <- Certs, C2 <- Certs],
++
++        %% Path endpoints: certs with no issuer in the sent chain
++        %% (either self-signed or issuer in trust store — handle_partial_chain
++        %% resolves which case applies downstream)
++        Endpoints = [V || V <- digraph:vertices(G),
++                          digraph:out_degree(G, V) =:= 0],
++        PeerId = cert_id(Peer),
++        Paths = lists:filtermap(
++                  fun(RootId) ->
++                          case digraph:get_path(G, PeerId, RootId) of
++                              false ->
++                                  false;
++                              VPath ->
++                                  RevPath = [element(2, digraph:vertex(G, V)) || V <- VPath],
++                                  {true, lists:reverse(RevPath)}
++                          end
++                  end, Endpoints),
++
++        %% Return candidate paths
++        Paths
++    after
++        digraph:delete(G)
+     end.
+ 
++cert_id(#cert{der = Der}) ->
++    %% Use a hash as vertex ID for efficient comparison
++    crypto:hash(sha256, Der).
++
+ handle_partial_chain([#cert{der=DERIssuerCert, otp=OtpIssuerCert}=Cert| Rest] = Path, PartialChainHandler,
+                      CertDbHandle, CertDbRef) ->
+     case public_key:pkix_is_self_signed(OtpIssuerCert) of
+@@ -760,59 +806,6 @@ handle_incomplete_chain([#cert{}=Peer| _] = Chain0, PartialChainHandler, Default
+             Default
+     end.
+ 
+-extraneous_chains(Certs) ->
+-    %% If some certs claim to be the same cert that is have the same
+-    %% subject field we should create a list of possible chain certs
+-    %% for each such cert. Only one chain, if any, should be
+-    %% verifiable using available ROOT certs.
+-    Subjects = [{subject(OTP), Cert} || #cert{otp=OTP} = Cert <- Certs],
+-    Duplicates = find_duplicates(Subjects),
+-    %% Number of certs with duplicates (same subject) has been limited
+-    %% to 4 and the maximum number of combinations is limited to 16.
+-    build_candidates(Duplicates, 4, 16).
+-
+-build_candidates(Map, Duplicates, Combinations) ->
+-    Subjects = maps:keys(Map),
+-    build_candidates(Subjects, Map, Duplicates, 1, Combinations, []).
+-%%
+-build_candidates([], _, _, _, _, Acc) ->
+-    Acc;
+-build_candidates([H|T], Map, Duplicates, Combinations, Max, Acc0) ->
+-    case maps:get(H, Map) of
+-	{Certs, Counter} when Counter > 1 andalso
+-                              Duplicates > 0 andalso
+-                              Counter * Combinations =< Max ->
+-	    case Acc0 of
+-		[] ->
+-		    Acc = [[Cert] || Cert <- Certs],
+-		    build_candidates(T, Map, Duplicates - 1, Combinations * Counter, Max, Acc);
+-		_Else ->
+-		    Acc = [[Cert|L] || Cert <- Certs, L <- Acc0],
+-		    build_candidates(T, Map, Duplicates - 1, Combinations * Counter, Max, Acc)
+-            end;
+-	{[Cert|_Throw], _Counter} ->
+-	    case Acc0 of
+-		[] ->
+-		    Acc = [[Cert]],
+-		    build_candidates(T, Map, Duplicates, Combinations, Max, Acc);
+-		_Else ->
+-		    Acc = [[Cert|L] || L <- Acc0],
+-		    build_candidates(T, Map, Duplicates, Combinations, Max, Acc)
+-	    end
+-    end.
+-
+-find_duplicates(Chain) ->
+-    find_duplicates(Chain, #{}).
+-%%
+-find_duplicates([], Acc) ->
+-    Acc;
+-find_duplicates([{Subject, Cert}|T], Acc) ->
+-    case maps:get(Subject, Acc, none) of
+-	none ->
+-	    find_duplicates(T, Acc#{Subject => {[Cert], 1}});
+-	{Certs, Counter} ->
+-	    find_duplicates(T, Acc#{Subject => {[Cert|Certs], Counter + 1}})
+-    end.
+ 
+ subject(Cert) ->
+     {_Serial,Subject} = public_key:pkix_subject_id(Cert),
+diff --git a/lib/ssl/test/ssl_cert_SUITE.erl b/lib/ssl/test/ssl_cert_SUITE.erl
+index a009e38bb9..d396798604 100644
+--- a/lib/ssl/test/ssl_cert_SUITE.erl
++++ b/lib/ssl/test/ssl_cert_SUITE.erl
+@@ -117,6 +117,12 @@
+          cross_signed_chain/1,
+          expired_root_with_cross_signed_root/0,
+          expired_root_with_cross_signed_root/1,
++         malicious_cycle_in_peer_chain/0,
++         malicious_cycle_in_peer_chain/1,
++         max_chain_depth_buildup/0,
++         max_chain_depth_buildup/1,
++         duplicate_issuer_in_trust_store/0,
++         duplicate_issuer_in_trust_store/1,
+          key_auth_ext_sign_only/0,
+          key_auth_ext_sign_only/1,
+          hello_retry_request/0,
+@@ -225,7 +231,10 @@ rsa_tests() ->
+    [
+     longer_chain,
+     cross_signed_chain,
+-    expired_root_with_cross_signed_root
++    expired_root_with_cross_signed_root,
++    malicious_cycle_in_peer_chain,
++    max_chain_depth_buildup,
++    duplicate_issuer_in_trust_store
+    ].
+ 
+ tls_1_3_rsa_tests() ->
+@@ -1270,6 +1279,120 @@ expired_root_with_cross_signed_root(Config) when is_list(Config) ->
+                              {cacerts, [AltCrossRoot | ClientCas0]} | proplists:delete(cacerts, ClientOpts)],
+                             ServerOpts, Config).
+ 
++%%--------------------------------------------------------------------
++malicious_cycle_in_peer_chain() ->
++    [{doc, "A malicious client sends an unordered chain to a server. "
++      "The server processes it through unorded_or_extraneous/2 which "
++      "uses an acyclic digraph. Verify that path construction from "
++      "the unordered chain terminates and does not loop."}].
++malicious_cycle_in_peer_chain(Config) when is_list(Config) ->
++    Key1 = ssl_test_lib:hardcode_rsa_key(1),
++    Key2 = ssl_test_lib:hardcode_rsa_key(2),
++    Key3 = ssl_test_lib:hardcode_rsa_key(3),
++    Key4 = ssl_test_lib:hardcode_rsa_key(4),
++    Key5 = ssl_test_lib:hardcode_rsa_key(5),
++
++    %% Client chain with cross-key intermediates
++    #{client_config := ClientOpts0} =
++        public_key:pkix_test_data(
++          #{server_chain => #{root => [{key, Key4}],
++                              peer => [{key, Key5}]},
++            client_chain => #{root => [{key, Key1}],
++                              intermediates => [[{key, Key2}], [{key, Key1}]],
++                              peer => [{key, Key3}]}}),
++
++    %% Build the client's ordered chain
++    ClientCert = proplists:get_value(cert, ClientOpts0),
++    ClientCAs = proplists:get_value(cacerts, ClientOpts0),
++    {ok, ExtractedCAs} = ssl_pkix_db:extract_trusted_certs({der, ClientCAs}),
++    {ok, _, [Peer, CA1, CA2, Root]} =
++        ssl_certificate:certificate_chain(ClientCert, ets:new(foo, []),
++                                          ExtractedCAs, [], encoded),
++
++    %% Shuffle chain so it's unordered — triggers unorded_or_extraneous
++    MaliciousChain = [Peer, Root, CA2, CA1],
++    CertRecs = [#cert{der=D, otp=public_key:pkix_decode_cert(D, otp)}
++                || D <- MaliciousChain],
++
++    %% Call trusted_cert_and_paths directly — this is the code path
++    %% that would hang without the digraph fix
++    %% Use empty trust store so no path can be validated
++    Result = ssl_certificate:trusted_cert_and_paths(
++               CertRecs, ets:new(foo, []), {extracted, []},
++               fun(_) -> unknown_ca end),
++
++    %% Must return (not hang) with unknown_ca for all paths
++    lists:foreach(fun({unknown_ca, _}) -> ok;
++                     ({#cert{}, _}) -> ok
++                  end, Result).
++
++%%--------------------------------------------------------------------
++max_chain_depth_buildup() ->
++    [{doc, "Chain building stops at MAX_CHAIN (12) even when the trust "
++      "store contains a longer valid chain. Guards against resource "
++      "exhaustion from very deep chains."}].
++max_chain_depth_buildup(Config) when is_list(Config) ->
++    %% Create chain with 15 intermediates — exceeds MAX_CHAIN (12)
++    Keys = [ssl_test_lib:hardcode_rsa_key((N rem 6) + 1)
++            || N <- lists:seq(1, 17)],
++    [RootKey, PeerKey | CAKeys] = Keys,
++    IntermediateOpts = [[{key, K}] || K <- CAKeys],
++
++    #{server_config := ServerOpts} =
++        public_key:pkix_test_data(
++          #{server_chain => #{root => [{key, RootKey}],
++                              intermediates => IntermediateOpts,
++                              peer => [{key, PeerKey}]},
++            client_chain => #{root => [{key, RootKey}],
++                              peer => [{key, PeerKey}]}}),
++
++    SCert = proplists:get_value(cert, ServerOpts),
++    SCerts = proplists:get_value(cacerts, ServerOpts),
++    {ok, ExtractedCAs} = ssl_pkix_db:extract_trusted_certs({der, SCerts}),
++
++    %% Build chain — must terminate and respect the MAX_CHAIN limit
++    {ok, _Root, Chain} =
++        ssl_certificate:certificate_chain(SCert, ets:new(foo, []),
++                                          ExtractedCAs, [], encoded),
++    %% MAX_CHAIN is 12: chain must not exceed that
++    true = (length(Chain) =< 12).
++
++%%--------------------------------------------------------------------
++duplicate_issuer_in_trust_store() ->
++    [{doc, "Trust store lookup returns a cert already in the chain. "
++      "The duplicate check in do_certificate_chain must detect this "
++      "and terminate instead of looping. Tests the DER-based "
++      "duplicate guard added in OTP-20245."}].
++duplicate_issuer_in_trust_store(Config) when is_list(Config) ->
++    Key1 = ssl_test_lib:hardcode_rsa_key(1),
++    Key2 = ssl_test_lib:hardcode_rsa_key(2),
++    Key3 = ssl_test_lib:hardcode_rsa_key(3),
++
++    #{server_config := ServerOpts0} =
++        public_key:pkix_test_data(
++          #{server_chain => #{root => [{key, Key1}],
++                              intermediates => [[{key, Key2}]],
++                              peer => [{key, Key3}]},
++            client_chain => #{root => [{key, Key1}],
++                              peer => [{key, Key3}]}}),
++
++    SCert = proplists:get_value(cert, ServerOpts0),
++    SCerts = proplists:get_value(cacerts, ServerOpts0),
++
++    %% Add peer cert to trust store — creates potential for
++    %% lookup_trusted_cert to return a cert already in chain
++    PoisonedCAs = [SCert | SCerts],
++    {ok, ExtractedCAs} = ssl_pkix_db:extract_trusted_certs({der, PoisonedCAs}),
++
++    %% Must terminate (not hang) and produce a valid chain
++    {ok, _Root, Chain} =
++        ssl_certificate:certificate_chain(SCert, ets:new(foo, []),
++                                          ExtractedCAs, [], encoded),
++    %% No duplicates in result
++    true = (length(Chain) =:= length(lists:usort(Chain))),
++    %% Reasonable length (normal: peer + CA + root = 3)
++    true = (length(Chain) =< 4).
++
+ %%--------------------------------------------------------------------
+ %% TLS 1.3 Test cases  -----------------------------------------------
+ %%--------------------------------------------------------------------

diff --git a/otp-0020-public_key-Cap-policy-tree-growth-to-prevent-DoS.patch b/otp-0020-public_key-Cap-policy-tree-growth-to-prevent-DoS.patch
new file mode 100644
index 0000000..029fbd1
--- /dev/null
+++ b/otp-0020-public_key-Cap-policy-tree-growth-to-prevent-DoS.patch
@@ -0,0 +1,581 @@
+From: Jakub Witczak <kuba@erlang.org>
+Date: Thu, 16 Jul 2026 17:20:06 +0200
+Subject: [PATCH] public_key: Cap policy tree growth to prevent DoS
+
+Add a monotonic node counter (never decremented on prune) that
+rejects certificate chains exceeding 1000 policy tree nodes with
+{bad_cert, policy_tree_exceeded}. Prevents exponential growth via
+crafted policyMappings cross-references.
+
+GHSA-622p-qfh6-c352
+
+(cherry picked from commit f04c6bba38de1cf1b1836a7d9a9fbe239bd939e8)
+
+Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
+Signed-off-by: Peter Lemenkov <lemenkov@gmail.com>
+
+diff --git a/lib/public_key/doc/src/public_key.xml b/lib/public_key/doc/src/public_key.xml
+index 7adbcd59ac..5b40575c3c 100644
+--- a/lib/public_key/doc/src/public_key.xml
++++ b/lib/public_key/doc/src/public_key.xml
+@@ -647,6 +647,11 @@ fun(OtpCert :: #'OTPCertificate'{},
+   <tag>invalid_validity_dates</tag>
+   <item><p>The validity section of the X.509 certificate(s) contains invalid date formats not matching the RFC.</p></item>
+ 
++  <tag>policy_tree_exceeded</tag>
++  <item><p>The certificate chain's policy tree exceeded the maximum allowed node count.
++  This indicates a malformed or malicious chain with exponentially expanding policy mappings.
++  This error cannot be overridden by the <c>verify_fun</c>.</p></item>
++
+ 	<tag>atom()</tag>
+ 	<item><p>Application-specific error reason that is to be checked by the <c>verify_fun</c>.</p></item>
+       </taglist>
+diff --git a/lib/public_key/include/public_key.hrl b/lib/public_key/include/public_key.hrl
+index 6670e0e524..696a5f74a8 100644
+--- a/lib/public_key/include/public_key.hrl
++++ b/lib/public_key/include/public_key.hrl
+@@ -44,6 +44,8 @@
+ -record(path_validation_state,
+         {
+          valid_policy_tree,
++         %% Monotonic count of nodes ever added to policy tree (never decremented by pruning).
++         policy_tree_node_count = 1 :: non_neg_integer(),
+          user_initial_policy_set,
+          explicit_policy,
+          inhibit_any_policy,
+diff --git a/lib/public_key/src/pubkey_cert.erl b/lib/public_key/src/pubkey_cert.erl
+index 42e1b8bc76..5119c91c04 100644
+--- a/lib/public_key/src/pubkey_cert.erl
++++ b/lib/public_key/src/pubkey_cert.erl
+@@ -53,7 +53,7 @@
+          x509_pkix_sign_types/1,
+          root_cert/2]).
+ 
+--define(NULL, 0).
++-define(MAX_POLICY_TREE_NODES, 1000).
+ 
+ %%====================================================================
+ %% Internal application APIs
+@@ -803,13 +803,14 @@ validate_extensions(OtpCert, [#'Extension'{extnID = ?'id-ce-certificatePolicies'
+ 			      | Rest],
+                     ValidationState,
+ 		    ExistBasicCon, SelfSigned, UserState, VerifyFun) ->
+-    Tree = process_policy_tree(Info, SelfSigned, ValidationState),
++    {Tree, NodeCount} = process_policy_tree(Info, SelfSigned, ValidationState),
+     validate_extensions(OtpCert, Rest,
+ 			ValidationState#path_validation_state{
+                           policy_ext_present = true,
+                           current_any_policy_qualifiers =
+                               current_any_policy_qualifiers(Info),
+-			  valid_policy_tree = Tree},
++			  valid_policy_tree = Tree,
++                          policy_tree_node_count = NodeCount},
+ 			ExistBasicCon, SelfSigned, UserState, VerifyFun);
+ validate_extensions(OtpCert, [#'Extension'{extnID = ?'id-ce-policyConstraints'} = Ext
+ 			      | Rest], ValidationState, ExistBasicCon,
+@@ -856,8 +857,9 @@ validate_extensions(OtpCert, [#'Extension'{} = Extension | Rest],
+ 			UserState, VerifyFun).
+ 
+ handle_last_cert(OtpCert, #path_validation_state{last_cert = true,
+-                                                 user_initial_policy_set = PolicySet,
+-                                                 valid_policy_tree = Tree} = ValidationState0) ->
++                                              user_initial_policy_set = PolicySet,
++                                              valid_policy_tree = Tree,
++                                              policy_tree_node_count = NodeCount0} = ValidationState0) ->
+     TBSCert = OtpCert#'OTPCertificate'.tbsCertificate,
+     Extensions =
+         extensions_list(TBSCert#'OTPTBSCertificate'.extensions),
+@@ -871,9 +873,13 @@ handle_last_cert(OtpCert, #path_validation_state{last_cert = true,
+             _  ->
+                 ValidationState0
+     end,
+-    ValidTree = policy_tree_intersection(PolicySet, Tree),
++    %% No assert needed — growth here is bounded by UserPolicySet size
++    %% (relying-party config, not attacker-controlled). Terminal operation
++    %% with no subsequent amplification possible.
++    {ValidTree, NodeCount} = policy_tree_intersection(PolicySet, Tree, NodeCount0),
+     validate_policy_tree(OtpCert,
+-                         ValidationState#path_validation_state{valid_policy_tree = ValidTree});
++                         ValidationState#path_validation_state{valid_policy_tree = ValidTree,
++                                                               policy_tree_node_count = NodeCount});
+ handle_last_cert(_, ValidationState) ->
+     ValidationState.
+ 
+@@ -939,24 +945,28 @@ assert_valid_policy_tree(false, _Tree) -> % 6.1.3 e
+ %% certificate and the valid_policy_tree is not NULL, process the
+ %% policy information by performing the following steps in order:
+ process_policy_tree(PolicyInformation, SelfSigned,
+-                    #path_validation_state{valid_policy_tree = Tree0} =
+-                        ValidationState) ->  
++                    #path_validation_state{valid_policy_tree = Tree0,
++                                           policy_tree_node_count = NodeCount0} =
++                        ValidationState) ->
+     case pubkey_policy_tree:is_empty(Tree0) of
+         true ->
+-            Tree0;
++            {Tree0, NodeCount0};
+         false ->
+             %% Step 1 & 2
+-            Tree = add_policy_children(PolicyInformation,
++            {Tree, NodeCount} = add_policy_children(PolicyInformation,
+                                        SelfSigned, ValidationState),
++            assert_policy_tree_node_count(NodeCount),
+             %% Step 3: If there is a node in the valid_policy_tree of depth i-1 or
+             %% less without any child nodes, delete that node.  Repeat this step
+             %% until there are no nodes of depth i-1 or less without children.
+-            pubkey_policy_tree:prune_tree(Tree) 
++            PrunedTree = pubkey_policy_tree:prune_tree(Tree),
++            {PrunedTree, NodeCount}
+     end.
+ 
+ %% 6.1.3 d
+ add_policy_children(PolicyInfoList0, SelfSigned,
+                     #path_validation_state{valid_policy_tree = Tree0,
++                                           policy_tree_node_count = NodeCount0,
+                                            inhibit_any_policy = AnyPolicyConstraint,
+                                            cert_num = CertNum,
+                                            max_path_length = PathLen
+@@ -975,17 +985,16 @@ add_policy_children(PolicyInfoList0, SelfSigned,
+         fun(#{expected_policy_set := ExpPolicySet}) ->
+                 policy_children(ExpPolicySet, PolicyInfoList)
+         end,
+-    Tree1 = pubkey_policy_tree:add_leaves(Tree0, LeafFun),
+-    
++    {Tree1, NodeCount1} = pubkey_policy_tree:add_leaves(Tree0, NodeCount0, LeafFun),
+     %% posibly ii
+     AllLeaves = pubkey_policy_tree:all_leaves(Tree1),
+     Siblings = fun(#{valid_policy := ?anyPolicy}) ->
+                        any_policy_children(AllLeaves, PolicyInfoList);
+                   (_) -> []
+                end,
+-    Tree = pubkey_policy_tree:add_leaf_siblings(Tree1, Siblings),
++    {Tree, NodeCount} = pubkey_policy_tree:add_leaf_siblings(Tree1, NodeCount1, Siblings),
+     %% Step 2
+-    handle_any_ext(Tree, AnyExt, AnyPolicyConstraint, SelfSigned, CertNum, PathLen).
++    handle_any_ext({Tree, NodeCount}, AnyExt, AnyPolicyConstraint, SelfSigned, CertNum, PathLen).
+ 
+ %% 6.1.3 - d 1 i
+ %% Step 1: For each policy P not equal to anyPolicy in the certificate
+@@ -1037,9 +1046,9 @@ any_policy_children(_, _) ->
+ %%   expected_policy_set in the parent node, set the qualifier_set to
+ %%   AP-Q, and set the expected_policy_set to the value in the
+ %%   valid_policy from this node.
+-handle_any_ext(Tree, undefined, _, _, _,_) ->
+-    Tree;
+-handle_any_ext(Tree, #'PolicyInformation'{
++handle_any_ext({Tree, NodeCount}, undefined, _, _, _,_) ->
++    {Tree, NodeCount};
++handle_any_ext({Tree, NodeCount0}, #'PolicyInformation'{
+                            policyIdentifier = ?anyPolicy,
+                            policyQualifiers = Qualifiers}, AnyPolicyConstraint,
+                SelfSigned, CertNum, PathLen) ->
+@@ -1050,9 +1059,9 @@ handle_any_ext(Tree, #'PolicyInformation'{
+             Siblings = fun(Node) ->
+                                any_ext_policy_children(Node, Qualifiers, AllLeaves)
+                        end,
+-            pubkey_policy_tree:add_leaf_siblings(Tree, Siblings);
++            pubkey_policy_tree:add_leaf_siblings(Tree, NodeCount0, Siblings);
+         false ->
+-            Tree
++            {Tree, NodeCount0}
+     end.
+ 
+ any_ext_policy_children(#{expected_policy_set := ExpPolicySet}, Qualifiers, AllLeaves) ->
+@@ -1067,23 +1076,26 @@ any_ext_policy_children(#{expected_policy_set := ExpPolicySet}, Qualifiers, AllL
+ %% 6.1.4. b start:
+ handle_policy_mappings(OtpCert,
+                        #path_validation_state{valid_policy_tree = Tree0,
++                                              policy_tree_node_count = NodeCount0,
+                                               policy_mapping_ext =
+                                                   #'Extension'{extnID = ?'id-ce-policyMappings',
+                                                                extnValue = PolicyMappings}}
+                        = ValidationState) ->
+-    case handle_policy_mappings(PolicyMappings, OtpCert, Tree0, ValidationState) of
+-        {tree, Tree} ->
+-            ValidationState#path_validation_state{valid_policy_tree = Tree};
++    case handle_policy_mappings(PolicyMappings, OtpCert, Tree0, NodeCount0, ValidationState) of
++        {tree, Tree, NodeCount} ->
++            ValidationState#path_validation_state{valid_policy_tree = Tree,
++                                                  policy_tree_node_count = NodeCount};
+         {user_state, UState} ->
+             ValidationState#path_validation_state{user_state = UState}
+     end.
+ 
+-handle_policy_mappings([], _, Tree, _) ->
+-    {tree, Tree};
+-handle_policy_mappings([Mappings | Rest], OtpCert, Tree0, ValidationState) ->
+-    case handle_policy_mapping(Mappings, OtpCert, Tree0, ValidationState) of
+-        {tree, Tree} ->
+-            handle_policy_mappings(Rest, OtpCert, Tree, ValidationState);
++handle_policy_mappings([], _, Tree, NodeCount, _) ->
++    {tree, Tree, NodeCount};
++handle_policy_mappings([Mappings | Rest], OtpCert, Tree0, NodeCount0, ValidationState) ->
++    case handle_policy_mapping(Mappings, OtpCert, Tree0, NodeCount0, ValidationState) of
++        {tree, Tree, NodeCount} ->
++            assert_policy_tree_node_count(NodeCount),
++            handle_policy_mappings(Rest, OtpCert, Tree, NodeCount, ValidationState);
+         Other ->
+             Other
+     end.
+@@ -1092,24 +1104,20 @@ handle_policy_mappings([Mappings | Rest], OtpCert, Tree0, ValidationState) ->
+ %% special value anyPolicy does not appear as an issuerDomainPolicy or
+ %% a subjectDomainPolicy.
+ handle_policy_mapping(#'PolicyMappings_SEQOF'{
+-                         issuerDomainPolicy =
+-                             IssuerPolicy,
+-                         subjectDomainPolicy =
+-                             SubjectPolicy} = Ext,
+-                      OtpCert, Tree0,
+-                      #path_validation_state{inhibit_policy_mapping =
+-                                                 PolicyMappingConstraint,
+-                                             current_any_policy_qualifiers =
+-                                                 AnyQualifiers,
++                         issuerDomainPolicy = IssuerPolicy,
++                         subjectDomainPolicy = SubjectPolicy} = Ext,
++                      OtpCert, Tree0, NodeCount0,
++                      #path_validation_state{inhibit_policy_mapping = PolicyMappingConstraint,
++                                             current_any_policy_qualifiers = AnyQualifiers,
+                                              verify_fun = VerifyFun,
+-                                             user_state = UserState}
+-                     ) ->
++                                             user_state = UserState}) ->
+     case not (?anyPolicy == IssuerPolicy) andalso
+         not (?anyPolicy == SubjectPolicy) of
+         true ->
+-            Tree = handle_policy_mapping_ext(Ext, Tree0,
+-                                             PolicyMappingConstraint, AnyQualifiers),
+-            {tree, Tree};
++            {Tree, NodeCount} =
++                handle_policy_mapping_ext(Ext, Tree0, NodeCount0,
++                                          PolicyMappingConstraint, AnyQualifiers),
++            {tree, Tree, NodeCount};
+         false ->
+             UserState = verify_fun(OtpCert, {bad_cert, {invalid_policy_mapping, Ext}},
+                                    UserState, VerifyFun),
+@@ -1118,9 +1126,8 @@ handle_policy_mapping(#'PolicyMappings_SEQOF'{
+ 
+ %% 6.1.4. b continue:
+ handle_policy_mapping_ext(#'PolicyMappings_SEQOF'{
+-                         issuerDomainPolicy =
+-                             IssuerPolicy},
+-                         Tree0, 0, _) -> %% 6.1.4. b 2:
++                             issuerDomainPolicy = IssuerPolicy},
++                         Tree0, NodeCount, 0, _) -> %% 6.1.4. b 2:
+     %% (2) If the policy_mapping variable is equal to 0:
+ 
+     %% (i) delete each node of depth i in the valid_policy_tree where
+@@ -1132,11 +1139,11 @@ handle_policy_mapping_ext(#'PolicyMappings_SEQOF'{
+     %% children.
+ 
+     Tree = pubkey_policy_tree:prune_leaves(Tree0, IssuerPolicy),
+-    pubkey_policy_tree:prune_tree(Tree);
++    {pubkey_policy_tree:prune_tree(Tree), NodeCount};
+ handle_policy_mapping_ext(#'PolicyMappings_SEQOF'{
+                              issuerDomainPolicy = IssuerPolicy,
+                              subjectDomainPolicy = SubjectPolicy},
+-                          Tree, N, AnyQualifiers) when N > 0 -> %% 6.1.4. b 1:
++                          Tree, NodeCount0, N, AnyQualifiers) when N > 0 -> %% 6.1.4. b 1:
+    
+     %% (1) If the policy_mapping variable is greater than 0, for each
+     %% node in the valid_policy_tree of depth i where ID-P is the
+@@ -1187,9 +1194,9 @@ handle_policy_mapping_ext(#'PolicyMappings_SEQOF'{
+ 
+     case pubkey_policy_tree:map_leaves(Tree, MapPolicy) of
+         Tree -> %% If no policy was mapped!
+-            pubkey_policy_tree:add_leaf_siblings(Tree, AnySiblings);
++            pubkey_policy_tree:add_leaf_siblings(Tree, NodeCount0, AnySiblings);
+         NewTree ->
+-            NewTree
++            {NewTree, NodeCount0}
+     end.
+ 
+ %% 6.1.4 i
+@@ -1253,12 +1260,12 @@ maybe_decrement(N, true) ->
+ 
+ %% Step G from RFC
+ 
+-policy_tree_intersection([?anyPolicy], Tree) -> % (ii) from RFC
+-    Tree;
+-policy_tree_intersection(UserPolicySet, Tree0) ->
++policy_tree_intersection([?anyPolicy], Tree, NodeCount) -> % (ii) from RFC
++    {Tree, NodeCount};
++policy_tree_intersection(UserPolicySet, Tree0, NodeCount0) ->
+     case pubkey_policy_tree:is_empty(Tree0) of
+         true ->  % (i) from RFC
+-            Tree0;
++            {Tree0, NodeCount0};
+         false -> % (iii) from RFC
+             %% Step 1 from RFC
+             ValidPolicyNodeSet = pubkey_policy_tree:valid_policy_node_set(Tree0),
+@@ -1268,10 +1275,11 @@ policy_tree_intersection(UserPolicySet, Tree0) ->
+             Tree1 = pubkey_policy_tree:prune_invalid_nodes(Tree0, InvalidNodes),
+ 
+             %% Step 3 from RFC
+-            Tree = handle_any_policy_leaves(Tree1, ValidPolicyNodeSet, UserPolicySet),
++            {Tree, NodeCount} =
++                handle_any_policy_leaves(Tree1, NodeCount0, ValidPolicyNodeSet, UserPolicySet),
+ 
+             %% Step 4 from RFC
+-            pubkey_policy_tree:prune_tree(Tree)
++            {pubkey_policy_tree:prune_tree(Tree), NodeCount}
+     end.
+ 
+ apply_user_constraints(_, [?anyPolicy]) ->
+@@ -1293,21 +1301,22 @@ apply_user_constraints([#{valid_policy := Policy} = Node | Rest],
+             apply_user_constraints(Rest, UserPolicySet, [Node | Acc])
+     end.
+ 
+-handle_any_policy_leaves(Tree, _, [?anyPolicy]) ->
+-    Tree;
+-handle_any_policy_leaves(Tree0, ValidPolicyNodeSet, UserPolicySet) ->
++handle_any_policy_leaves(Tree, NodeCount, _, [?anyPolicy]) ->
++    {Tree, NodeCount};
++handle_any_policy_leaves(Tree0, NodeCount0, ValidPolicyNodeSet, UserPolicySet) ->
+     case pubkey_policy_tree:any_leaves(Tree0) of
+         [] ->
+-            Tree0;
++            {Tree0, NodeCount0};
+         AnyLeaves ->
+-            Tree = add_policy_nodes(AnyLeaves, Tree0, ValidPolicyNodeSet, UserPolicySet),
+-            pubkey_policy_tree:prune_leaves(Tree, ?anyPolicy)
++            {Tree, NodeCount} =
++                add_policy_nodes(AnyLeaves, Tree0, NodeCount0, ValidPolicyNodeSet, UserPolicySet),
++            {pubkey_policy_tree:prune_leaves(Tree, ?anyPolicy), NodeCount}
+     end.
+ 
+-add_policy_nodes([], Tree, _, _) ->
+-    Tree;
+-add_policy_nodes([#{qualifier_set := Qualifiers} | Rest], Tree0,
+-                 ValidPolicyNodeSet, UserPolicySet) ->
++add_policy_nodes([], Tree, NodeCount, _, _) ->
++    {Tree, NodeCount};
++add_policy_nodes([#{qualifier_set := Qualifiers} | Rest],
++                 Tree0, NodeCount0, ValidPolicyNodeSet, UserPolicySet) ->
+     PolicySet = [UPolicy ||  UPolicy <- UserPolicySet,
+                              not pubkey_policy_tree:in_set(UPolicy, ValidPolicyNodeSet)],
+     Children =
+@@ -1316,8 +1325,14 @@ add_policy_nodes([#{qualifier_set := Qualifiers} | Rest], Tree0,
+                        Children;
+                   (_) -> []
+                end,
+-    add_policy_nodes(Rest, pubkey_policy_tree:add_leaf_siblings(Tree0, Siblings),
+-                     ValidPolicyNodeSet, UserPolicySet).
++    {Tree, NodeCount} = pubkey_policy_tree:add_leaf_siblings(Tree0, NodeCount0, Siblings),
++    add_policy_nodes(Rest, Tree, NodeCount, ValidPolicyNodeSet, UserPolicySet).
++
++%% Monotonic counter — never decremented by pruning.
++assert_policy_tree_node_count(Count) when Count > ?MAX_POLICY_TREE_NODES ->
++    throw({bad_cert, policy_tree_exceeded});
++assert_policy_tree_node_count(_) ->
++    ok.
+ 
+ %% End Wrap Up Policy Handling -------------------------------------------------
+ 
+diff --git a/lib/public_key/src/pubkey_policy_tree.erl b/lib/public_key/src/pubkey_policy_tree.erl
+index bacdfd5137..7799c8aa69 100644
+--- a/lib/public_key/src/pubkey_policy_tree.erl
++++ b/lib/public_key/src/pubkey_policy_tree.erl
+@@ -22,8 +22,8 @@
+ -include("../include/public_key.hrl").
+ 
+ %% API
+--export([add_leaves/2,
+-         add_leaf_siblings/2,
++-export([add_leaves/3,
++         add_leaf_siblings/3,
+          any_leaves/1,
+          all_leaves/1,
+          collect_qualifiers/2,
+@@ -57,40 +57,49 @@
+ %%%===================================================================
+ 
+ %%--------------------------------------------------------------------
+--spec add_leaves(policy_tree(), LeafFun) -> policy_tree() when
++-spec add_leaves(policy_tree(), non_neg_integer(), LeafFun) ->
++          {policy_tree(), non_neg_integer()} when
+       LeafFun :: fun((policy_tree_node()) -> [policy_node()]).
+ 
+ %%
+ %% Add leaves specified by calling LeafFun with the current leaves
+ %% as input
+ %%--------------------------------------------------------------------
+-add_leaves({Parent, []}, LeafFun) ->
+-    {Parent, LeafFun(Parent)};
+-add_leaves(Tree, LeafFun0) ->
++add_leaves({Parent, []}, NodeCount0, LeafFun) ->
++    Leaves = LeafFun(Parent),
++    NodeCount = NodeCount0 + length(Leaves),
++    {{Parent, Leaves}, NodeCount};
++add_leaves(Tree0, NodeCount, LeafFun0) ->
+     LeafFun = fun(Leaf) ->
+                       NewLeaves = LeafFun0(Leaf),
+                       {Leaf, NewLeaves}
+               end,
+-    map_leaves(Tree, LeafFun).
++    {Tree, NodesAdded} = map_leaves_count(Tree0, LeafFun),
++    {Tree, NodeCount + NodesAdded}.
+ 
+ %%--------------------------------------------------------------------
+--spec add_leaf_siblings(policy_tree(), SiblingFun) -> policy_tree() when
++-spec add_leaf_siblings(policy_tree(), non_neg_integer(), SiblingFun) ->
++          {policy_tree(), non_neg_integer()} when
+       SiblingFun ::fun((policy_tree_node()) -> no_sibling | [policy_node()]).
+ 
+ %%
+ %% Add sibling leaves if SiblingFun returns a list of policy nodes
+ %% for the leaf parent.
+ %%--------------------------------------------------------------------
+-add_leaf_siblings({Parent,[{_, _}|_] = ChildNodes}, SiblingFun) ->
+-    {Parent, lists:map(fun(ChildNode)->
+-                               add_leaf_siblings(ChildNode, SiblingFun)
+-                       end, ChildNodes)};
+-add_leaf_siblings({Parent, Leaves} = Node, SiblingFun) ->
++add_leaf_siblings({Parent,[{_, _}|_] = ChildNodes}, NodeCount, SiblingFun) ->
++    {Leaves, NodesAdded} =
++        lists:mapfoldl(fun(ChildNode, Acc)->
++                               {Leaves, NodesAdded} =
++                                   add_leaf_siblings(ChildNode, 0, SiblingFun),
++                               {Leaves, Acc + NodesAdded}
++                       end, 0, ChildNodes),
++    {{Parent, Leaves}, NodeCount + NodesAdded};
++add_leaf_siblings({Parent, Leaves} = Node, NodeCount, SiblingFun) ->
+     case SiblingFun(Parent) of
+         no_sibling ->
+-            Node;
++            {Node, NodeCount};
+         Siblings ->
+-            {Parent, Leaves ++ Siblings}
++            {{Parent, Leaves ++ Siblings}, NodeCount + length(Siblings)}
+     end.
+ 
+ %%--------------------------------------------------------------------
+@@ -317,6 +326,21 @@ valid_policy_node_set(_) ->
+ %%%===================================================================
+ %%% Internal functions
+ %%%===================================================================
++map_leaves_count({Parent, [{_, _}|_] = ChildNodes}, LeafFun) ->
++    {Leaves, NodesAdded} =
++        lists:mapfoldl(fun(ChildNode, Acc)->
++                               {Node, Added} = map_leaves_count(ChildNode, LeafFun),
++                               {Node, Acc + Added}
++                       end, 0, ChildNodes),
++    {{Parent, Leaves}, NodesAdded};
++map_leaves_count({Parent, Leaves0}, LeafFun) ->
++    {Leaves, NodesAdded} =
++        lists:mapfoldl(fun(L, Acc) ->
++                               Node = {_, ChildNodes} = LeafFun(L),
++                               {Node, Acc + length(ChildNodes)}
++                       end, 0, Leaves0),
++    {{Parent, Leaves}, NodesAdded}.
++
+ any_policy_node() ->
+     policy_node(?anyPolicy, [], [?anyPolicy]).
+ 
+diff --git a/lib/public_key/src/public_key.erl b/lib/public_key/src/public_key.erl
+index 6c12c59e36..fe23d29a3d 100644
+--- a/lib/public_key/src/public_key.erl
++++ b/lib/public_key/src/public_key.erl
+@@ -163,7 +163,8 @@
+ -type cert_id()              :: {SerialNr::integer(), issuer_name()} .
+ -type issuer_name()          :: {rdnSequence,[[#'AttributeTypeAndValue'{}]]} .
+ -type bad_cert_reason()      :: cert_expired | invalid_issuer | invalid_signature | name_not_permitted | missing_basic_constraint | invalid_key_usage |{key_usage_mismatch, term()}  | duplicate_cert_in_path |
+-                                {'policy_requirement_not_met', term()} | {'invalid_policy_mapping', term()} | {revoked, crl_reason()} | invalid_validity_dates | atom().
++                                {'policy_requirement_not_met', term()} | {'invalid_policy_mapping', term()} | {revoked, crl_reason()} | invalid_validity_dates |
++                                policy_tree_exceeded | atom().
+ 
+ -type combined_cert()        :: #cert{}.
+ -type cert()                 :: der_cert() | otp_cert().
+diff --git a/lib/public_key/test/pubkey_policy_tree_SUITE.erl b/lib/public_key/test/pubkey_policy_tree_SUITE.erl
+index c152e7567e..b9b48d8532 100644
+--- a/lib/public_key/test/pubkey_policy_tree_SUITE.erl
++++ b/lib/public_key/test/pubkey_policy_tree_SUITE.erl
+@@ -1,3 +1,25 @@
++%%
++%% %CopyrightBegin%
++%%
++%% SPDX-License-Identifier: Apache-2.0
++%%
++%% Copyright Ericsson AB 2024-2026. All Rights Reserved.
++%%
++%% Licensed under the Apache License, Version 2.0 (the "License");
++%% you may not use this file except in compliance with the License.
++%% You may obtain a copy of the License at
++%%
++%%     http://www.apache.org/licenses/LICENSE-2.0
++%%
++%% Unless required by applicable law or agreed to in writing, software
++%% distributed under the License is distributed on an "AS IS" BASIS,
++%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
++%% See the License for the specific language governing permissions and
++%% limitations under the License.
++%%
++%% %CopyrightEnd%
++%%
++
+ -module(pubkey_policy_tree_SUITE).
+ -compile([export_all, nowarn_export_all]).
+ 
+@@ -173,9 +195,9 @@ add_leaves(_Config) ->
+            (_) ->
+                 []
+         end,
+-    Instructions = [{add_leaves, [AddLeavesFun1]},
+-                    {add_leaves, [AddLeavesFun1]},
+-                    {add_leaves, [AddLeavesFun2]}],
++    Instructions = [{add_leaves, 2, [AddLeavesFun1]},
++                    {add_leaves, 4, [AddLeavesFun1]},
++                    {add_leaves, 4, [AddLeavesFun2]}],
+     {ok, Tree} = explain(RootTree, Instructions),
+     ?assertEqual({?ROOT_PN,
+                   [{?PN("GOLD"),
+@@ -198,16 +220,18 @@ add_leaf_siblings(_Config) ->
+            (_) ->
+                 []
+         end,
+-    Instructions = [{add_leaf_siblings, [AddLeavesFun1]},
+-                    {add_leaf_siblings, [AddLeavesFun1]},
+-                    {add_leaf_siblings, [AddLeavesFun2]}
++    Instructions = [{add_leaf_siblings, 4, [AddLeavesFun1]},
++                    {add_leaf_siblings, 4, [AddLeavesFun1]},
++                    {add_leaf_siblings, 2, [AddLeavesFun2]}
+                    ],
+     {ok, Tree} = explain(tree_with_any_policy_node1(), Instructions),
+     ?assertEqual({?ROOT_PN,
+                   [{?PN(?anyPolicy),
+-                    [?PN("GOLD"), ?PN("GOLD"), ?PN("SILVER"), ?PN("GOLD"), ?PN("SILVER"), ?PN("PINK")]},
++                    [?PN("GOLD"), ?PN("GOLD"), ?PN("SILVER"), ?PN("GOLD"),
++                     ?PN("SILVER"), ?PN("PINK")]},
+                    {?PN("SILVER", ["A"]),
+-                    [?PN("SILVER", ["B"]), ?PN("GOLD"), ?PN("SILVER"), ?PN("GOLD"), ?PN("SILVER"), ?PN("PURPLE")]}]},
++                    [?PN("SILVER", ["B"]), ?PN("GOLD"), ?PN("SILVER"),
++                     ?PN("GOLD"), ?PN("SILVER"), ?PN("PURPLE")]}]},
+                  Tree),
+     ok.
+ 
+@@ -222,6 +246,13 @@ explain(InitTree, Instructions) ->
+ 
+ explain(Tree, [], _) ->
+     {ok, Tree};
++explain(Tree0, [{FunctionName, ExpectedTreeGrowth, Args} | Rest], N) ->
++    Title = io_lib:format("~p) pubkey_policy_tree:~p()", [N, FunctionName]),
++    ct:log("=============================================~nSTEP: ~s", [Title]),
++    {Tree, TreeGrowth} = apply(pubkey_policy_tree, FunctionName, [Tree0, 0 | Args]),
++    ?assertEqual(ExpectedTreeGrowth, TreeGrowth),
++    ?PAL_MMD(to_mmd(Title, Tree)),
++    explain(Tree, Rest, N+1);
+ explain(Tree0, [{FunctionName, Args} | Rest], N) ->
+     Title = io_lib:format("~p) pubkey_policy_tree:~p()", [N, FunctionName]),
+     ct:log("=============================================~nSTEP: ~s", [Title]),
+diff --git a/lib/ssl/src/ssl_handshake.erl b/lib/ssl/src/ssl_handshake.erl
+index 5b808fda9d..7e18803aa3 100644
+--- a/lib/ssl/src/ssl_handshake.erl
++++ b/lib/ssl/src/ssl_handshake.erl
+@@ -2168,6 +2168,8 @@ path_validation_alert({bad_cert, {ca_invalid_ext_keyusage, ExtKeyUses}}, _, _) -
+     ?ALERT_REC(?FATAL, ?UNSUPPORTED_CERTIFICATE, {ca_invalid_ext_keyusage, Uses});
+ path_validation_alert({bad_cert, {key_usage_mismatch, _} = Reason}, _, _) ->
+     ?ALERT_REC(?FATAL, ?UNSUPPORTED_CERTIFICATE, Reason);
++path_validation_alert({bad_cert, policy_tree_exceeded}, _, _) ->
++    ?ALERT_REC(?FATAL, ?BAD_CERTIFICATE, policy_tree_exceeded);
+ path_validation_alert(Reason, _,_) ->
+     ?ALERT_REC(?FATAL, ?HANDSHAKE_FAILURE, Reason).
+ 

                 reply	other threads:[~2026-09-07 18:40 UTC|newest]

Thread overview: [no followups] expand[flat|nested]  mbox.gz  Atom feed

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=178880644075.1.1612306384745076751.rpms-erlang-25259b955c41@fedoraproject.org \
    --to=lemenkov@gmail.com \
    --cc=git-commits@fedoraproject.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox