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