diff --git a/lib/public_key/src/pubkey_cert.erl b/lib/public_key/src/pubkey_cert.erl index be9a5969069b..85c457295271 100644 --- a/lib/public_key/src/pubkey_cert.erl +++ b/lib/public_key/src/pubkey_cert.erl @@ -924,6 +924,20 @@ validate_extensions(OtpCert, [#'Extension'{extnID = ?'id-ce-policyConstraints', validate_extensions(OtpCert, Rest, NewValidationState, ExistBasicCon, SelfSigned, UserState, VerifyFun); +validate_extensions(OtpCert, [#'Extension'{extnID = ?'id-ce-extKeyUsage', + critical = true, + extnValue = KeyUse} = Extension | Rest], + #path_validation_state{last_cert = false} = ValidationState, ExistBasicCon, + SelfSigned, UserState0, VerifyFun) -> + UserState = + case ext_keyusage_includes_any(KeyUse) of + true -> %% CA cert that specifies ?anyExtendedKeyUsage should not be marked critical + verify_fun(OtpCert, {bad_cert, invalid_ext_key_usage}, UserState0, VerifyFun); + false -> + verify_fun(OtpCert, {extension, Extension}, UserState0, VerifyFun) + end, + validate_extensions(OtpCert, Rest, ValidationState, ExistBasicCon, SelfSigned, + UserState, VerifyFun); validate_extensions(OtpCert, [#'Extension'{} = Extension | Rest], ValidationState, ExistBasicCon, SelfSigned, UserState0, VerifyFun) -> @@ -1502,3 +1516,9 @@ verify_options(#'RSASSA-PSS-params'{saltLength = SaltLen, [{rsa_padding, rsa_pkcs1_pss_padding}, {rsa_pss_saltlen, SaltLen}, {rsa_mgf1_md, HashAlgo}]. + + +ext_keyusage_includes_any(KeyUse) when is_list(KeyUse) -> + lists:member(?anyExtendedKeyUsage, KeyUse); +ext_keyusage_includes_any(_) -> + false. diff --git a/lib/public_key/test/public_key_SUITE.erl b/lib/public_key/test/public_key_SUITE.erl index 7cb8ceddd416..f65971a9a17f 100644 --- a/lib/public_key/test/public_key_SUITE.erl +++ b/lib/public_key/test/public_key_SUITE.erl @@ -95,6 +95,8 @@ pkix_path_validation/1, pkix_path_validation_root_expired/0, pkix_path_validation_root_expired/1, + pkix_ext_key_usage/0, + pkix_ext_key_usage/1, pkix_verify_hostname_cn/1, pkix_verify_hostname_subjAltName/1, pkix_verify_hostname_options/1, @@ -156,6 +158,7 @@ all() -> pkix_decode_cert, pkix_path_validation, pkix_path_validation_root_expired, + pkix_ext_key_usage, pkix_iso_rsa_oid, pkix_iso_dsa_oid, pkix_dsa_sha2_oid, @@ -934,6 +937,35 @@ pkix_path_validation_root_expired(Config) when is_list(Config) -> Peer = proplists:get_value(cert, Conf), {error, {bad_cert, cert_expired}} = public_key:pkix_path_validation(Root, [ICA, Peer], []). +pkix_ext_key_usage() -> + [{doc, "Extended key usage is usually in end entity certs, may be in CA but should not be critical in such case"}]. +pkix_ext_key_usage(Config) when is_list(Config) -> + SRootSpec = public_key:pkix_test_root_cert("OTP test server ROOT", []), + CRootSpec = public_key:pkix_test_root_cert("OTP test client ROOT", []), + + FailCAExt = [#'Extension'{extnID = ?'id-ce-extKeyUsage', + extnValue = [?'anyExtendedKeyUsage'], + critical = true}], + CAExt = [#'Extension'{extnID = ?'id-ce-extKeyUsage', + extnValue = [?'anyExtendedKeyUsage'], + critical = false}], + + #{server_config := SConf, + client_config := CConf} = public_key:pkix_test_data(#{server_chain => #{root => SRootSpec, + intermediates => [[{extensions, FailCAExt}]], + peer => []}, + client_chain => #{root => CRootSpec, + intermediates => [[{extensions, CAExt}]], + peer => []}}), + [_STRoot, SICA, SRoot] = proplists:get_value(cacerts, SConf), + [_CTRoot, CICA, CRoot] = proplists:get_value(cacerts, CConf), + SPeer = proplists:get_value(cert, SConf), + CPeer = proplists:get_value(cert, CConf), + + {error, {bad_cert, invalid_ext_key_usage}} = public_key:pkix_path_validation(SRoot, [SICA, SPeer], []), + + {ok, _} = public_key:pkix_path_validation(CRoot, [CICA, CPeer], []). + %%-------------------------------------------------------------------- %% To generate the PEM file contents: %% diff --git a/lib/ssl/src/ssl_certificate.erl b/lib/ssl/src/ssl_certificate.erl index ec1816fdde79..34e4e8b0aee5 100644 --- a/lib/ssl/src/ssl_certificate.erl +++ b/lib/ssl/src/ssl_certificate.erl @@ -74,7 +74,6 @@ file_to_certificats/2, file_to_crls/2, validate/3, - is_valid_extkey_usage/2, is_valid_key_usage/2, select_extension/2, extensions_list/1, @@ -207,12 +206,14 @@ file_to_crls(File, DbHandle) -> %% Description: Validates ssl/tls specific extensions %%-------------------------------------------------------------------- validate(_,{extension, #'Extension'{extnID = ?'id-ce-extKeyUsage', - extnValue = KeyUse}}, UserState = #{role := Role}) -> - case is_valid_extkey_usage(KeyUse, Role) of + critical = Critical, + extnValue = KeyUse}}, #{path_len := 1} = UserState) -> + %% If extension in peer, check for TLS server/client usage + case is_valid_extkey_usage(KeyUse, Critical, UserState) of true -> {valid, UserState}; false -> - {fail, {bad_cert, invalid_ext_key_usage}} + {unknown, UserState} end; validate(_, {extension, _}, UserState) -> {unknown, UserState}; @@ -220,14 +221,14 @@ validate(Issuer, {bad_cert, cert_expired}, #{issuer := Issuer}) -> {fail, {bad_cert, root_cert_expired}}; validate(_, {bad_cert, _} = Reason, _) -> {fail, Reason}; -validate(Cert, valid, UserState) -> +validate(Cert, valid, #{path_len := N} = UserState) -> case verify_sign(Cert, UserState) of true -> case maps:get(cert_ext, UserState, undefined) of undefined -> - {valid, UserState}; + {valid, UserState#{path_len => N-1}}; _ -> - verify_cert_extensions(Cert, UserState) + verify_cert_extensions(Cert, UserState#{path_len => N-1}) end; false -> {fail, {bad_cert, invalid_signature}} @@ -498,13 +499,20 @@ do_find_issuer(IssuerFun, CertDbHandle, CertDb) -> throw:{ok, _} = Return -> Return end. - -is_valid_extkey_usage(KeyUse, client) -> + +is_valid_extkey_usage(KeyUse, true, #{role := Role}) when is_list(KeyUse) -> + is_valid_key_usage(KeyUse, ext_keysage(Role)); +is_valid_extkey_usage(KeyUse, true, #{role := Role}) -> + is_valid_key_usage([KeyUse], ext_keysage(Role)); +is_valid_extkey_usage(_, false, _) -> + false. + +ext_keysage(client) -> %% Client wants to verify server - is_valid_key_usage(KeyUse,?'id-kp-serverAuth'); -is_valid_extkey_usage(KeyUse, server) -> + ?'id-kp-serverAuth'; +ext_keysage(server) -> %% Server wants to verify client - is_valid_key_usage(KeyUse, ?'id-kp-clientAuth'). + ?'id-kp-clientAuth'. verify_cert_signer(BinCert, SignerTBSCert) -> PublicKey = public_key(SignerTBSCert#'OTPTBSCertificate'.subjectPublicKeyInfo), diff --git a/lib/ssl/src/ssl_handshake.erl b/lib/ssl/src/ssl_handshake.erl index ac62231a5180..1249ed55b979 100644 --- a/lib/ssl/src/ssl_handshake.erl +++ b/lib/ssl/src/ssl_handshake.erl @@ -3843,7 +3843,9 @@ path_validation(TrustedCert, Path, ServerName, Role, CertDbHandle, CertDbRef, CR cert_ext => CertExt, issuer => TrustedCert, ocsp_responder_certs => OcspResponderCerts, - ocsp_state => OcspState}, + ocsp_state => OcspState, + path_len => length(Path) + }, Path, Level), Options = [{max_path_length, maps:get(depth, Opts, ?DEFAULT_DEPTH)}, {verify_fun, ValidationFunAndState}], diff --git a/lib/ssl/src/tls_handshake_1_3.erl b/lib/ssl/src/tls_handshake_1_3.erl index 95520d7a35f7..7796afd7ec7e 100644 --- a/lib/ssl/src/tls_handshake_1_3.erl +++ b/lib/ssl/src/tls_handshake_1_3.erl @@ -1881,7 +1881,8 @@ path_validation(TrustedCert, Path, ServerName, Role, CertDbHandle, CertDbRef, CR issuer => TrustedCert, cert_ext => CertExt, ocsp_responder_certs => OcspResponderCerts, - ocsp_state => OcspState + ocsp_state => OcspState, + path_len => length(Path) }, Path, LogLevel), Options = [{max_path_length, maps:get(depth, Opts, ?DEFAULT_DEPTH)},