Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gracefully handle bad validity dates in public_key:pkix_path_validation #7515

Merged
merged 1 commit into from
Nov 17, 2023

Conversation

andreasanta
Copy link
Contributor

If a certificate contains an invalid date in the validity attribute, the function public_key:pkix_path_validation raises a function_clause error due to the invocation of calendar:date_to_gregorian_days with an invalid date.

In the attached unit test, the leaf certificate contains the invalid date string "19000100000000Z", and this would entirely fail the validation of the certificate chain.

In this PR, I am handling this edge case by delegating the decision to verify_fun and thus the caller. If verify_fun traps the error and decides to continue, validation is not interrupted, otherwise the function_clause error is raised.

This is an example error reproduced locally with the attached certificate:

{function_clause,
                     [{calendar,date_to_gregorian_days,
                          [1900,1,0],
                          [{file,"calendar.erl"},{line,132}]},
                      {calendar,datetime_to_gregorian_seconds,1,
                          [{file,"calendar.erl"},{line,155}]},
                      {pubkey_cert,validate_time,3,
                          [{file,"pubkey_cert.erl"},{line,147}]},

NOTE: I am aware invalid dates should never be passed to this function, but I am dealing with a hardware implementation that cannot be easily changed, thus I have to permit a more lenient parsing and recovery from error. Also, I do not believe a function_clause error is the right response, since it's too generic and cannot be handled accordingly or interpreted by the caller.

CC @IngelaAndin @potatosalad for visibility

@CLAassistant
Copy link

CLAassistant commented Jul 26, 2023

CLA assistant check
All committers have signed the CLA.

@github-actions
Copy link
Contributor

github-actions bot commented Jul 26, 2023

CT Test Results

No tests were run for this PR. This is either because the build failed, or the PR is based on a branch without GH actions tests configured.

Results for commit 97d4036

To speed up review, make sure that you have read Contributing to Erlang/OTP and that all checks pass.

See the TESTING and DEVELOPMENT HowTo guides for details about how to run test locally.

Artifacts

// Erlang/OTP Github Action Bot

@jhogberg jhogberg added the team:PS Assigned to OTP team PS label Jul 31, 2023
@IngelaAndin
Copy link
Contributor

I am not very fond of these kind of workarounds, but we have been forced to be pragmatic in other cases too, and it seems harmless enough solution. It is vacation time now so I would like to wait a little for final inclusion decision so that we can have a team review. And we would also need to add the new error to the documentation.

@IngelaAndin IngelaAndin self-assigned this Aug 1, 2023
@andreasanta
Copy link
Contributor Author

andreasanta commented Aug 6, 2023

Thanks @IngelaAndin - To be honest I would have also gladly glossed over this edge case, but we are getting plenty of hardware certificates not abiding to the RFCs and we do not have many alternatives.

Please let me know if you would like me to add the documentation commit to the PR as well.

Comment on lines 946 to 962
% First test exception throwing without validation function
try
public_key:pkix_path_validation(Root, CertificateChain, [])
catch
% We expect this function to throw if the date is wrong
error:function_clause ->
ok;
_ ->
throw("Expected function_clause error on bad date")
end,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is wrong:

Replace try catch with:
{error, {bad_cert, invalid_validity_dates}} = public_key:pkix_path_validation(Root, CertificateChain, []),

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the current behaviour if no verify fun is supplied. Why would you like to change it?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the test will be merged along with your changes there is no reason to test the behavior in the old code.

You shall test the new functionality, i.e. that {error, {bad_cert, invalid_validity_dates}} is returned if no verify_fun is provided.

Copy link
Contributor

@u3s u3s Sep 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andreasanta what about this one?

FYI: This code does not seem to verify what you probably intended (error:function_clause catch part is skipped, testcase does not notice).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dgud This diff does not alter the old functionality if you don't provide verify_fun. This is why I am leaving the testcase. In order to preserve backwards compatibility, I am still raising an error if no verify_fun is specified.

@u3s Sorry maybe I did not explain my diff correctly. The previous behaviour has not been altered on purpose. You can optionally alter if if you pass a verify_fun that intercepts the validation error. This testcase exists to ensure that the previous behaviour - i.e. throwing an error upon failed validation - still happens if you don't specify a verify_fun.

Please let me know if you'd like a different approach.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have altered the functionality if no verify_fun is used and you fail to test it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not see a reason to preserve the crash behavior for something that should not really happen. I think this PR is about adding a workaround | graceful handling of that it sometimes does due to mistakes that are out of users control.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@IngelaAndin Refactored as suggested, plus some other improvements, to improve readability.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have altered the functionality if no verify_fun is used and you fail to test it.

Sorry I do not understand it. I have written two tests, one that lets the code crash as expected, the other prevents the crash by supplying a custom verify_fun. What did I miss?

@IngelaAndin
Copy link
Contributor

Yes we want you to add a documentation commit where the invalid_validity_dates are added to the possible bad_cert_reason() type.

@IngelaAndin
Copy link
Contributor

Also if you base your patch on maint instead of master we can include it for OTP-26.1 instead of waiting for OTP-27. This I think should be ok as it does not really change any API behavior (only adds an error reason) and it is unlikely to change any existing verify_fun behavior but rather expands the workaround possibilities for the verify_fun, which is one of its uses although the main purpose is to be able to handle application specific extensions. But that is up to you.

@IngelaAndin
Copy link
Contributor

@andreasanta and adding the bad_cert_reason() to the documentation is actually adding it to the spec in public_key.erl

lib/public_key/src/pubkey_cert.erl Outdated Show resolved Hide resolved
lib/public_key/src/pubkey_cert.erl Outdated Show resolved Hide resolved
lib/public_key/src/pubkey_cert.erl Outdated Show resolved Hide resolved
@andreasanta andreasanta changed the base branch from master to maint August 23, 2023 16:45
@andreasanta andreasanta changed the base branch from maint to master August 23, 2023 16:47
@andreasanta andreasanta changed the base branch from master to maint August 23, 2023 17:25
@andreasanta
Copy link
Contributor Author

@IngelaAndin All done. Addressed comments, rebased on maint and added docs. Please confirm if ok.

@andreasanta andreasanta force-pushed the pk_bad_date branch 4 times, most recently from 6af8068 to e9adb6e Compare August 23, 2023 17:57
@IngelaAndin IngelaAndin added the testing currently being tested, tag is used by OTP internal CI label Aug 25, 2023
@u3s u3s removed the testing currently being tested, tag is used by OTP internal CI label Sep 5, 2023
Comment on lines 166 to 167
NewState ->
NewState
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have doubts about this catch everything clause.
Previous code didn't work that way.

Is there a reason for it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No reason, preventing crash in case of unexpected value. I can remove it if necessary.

Copy link
Contributor

@IngelaAndin IngelaAndin Sep 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually think that the last case is the case that the catch verify_fun returns a state. I think the following code could benefit readablility:

validate_time(OtpCert, UserState, VerifyFun) ->
    TBSCert = OtpCert#'OTPCertificate'.tbsCertificate,
    {'Validity', NotBeforeStr, NotAfterStr} 
	= TBSCert#'OTPTBSCertificate'.validity,
   Now = calendar:datetime_to_gregorian_seconds(calendar:universal_time()),

  case valid_dates(NotBeforeStr, NotAfterStr}) of
   {true, {NotBefore, NotAfter}} ->
         case ((NotBefore =< Now) and (Now =< NotAfter)) of
	    true ->
	      UserState;
	   false ->
	      verify_fun(OtpCert, {bad_cert, cert_expired}, UserState, VerifyFun)
           end;
   {false, Error} ->
        verify_fun(OtpCert, {bad_cert, Error}, UserState, VerifyFun)
  end.

valid_dates(NotBeforeStr, NotAfterStr) ->
  try
    NotBefore = time_str_2_gregorian_sec(notBefore, NotBeforeStr),
    NotAfter = time_str_2_gregorian_sec(notAfter, NotAfterStr),
    {true, {NotBefore, NotAfter}}
catch
    error:function_clause ->
    {false, invalid_validity_dates}
end. 



Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be related to comment on tests!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@IngelaAndin ah yes thank you for flagging this! Would you like me to refactor as suggested?

@IngelaAndin IngelaAndin added the waiting waiting for changes/input from author label Sep 18, 2023
@andreasanta andreasanta force-pushed the pk_bad_date branch 2 times, most recently from ea7b500 to 0e656af Compare October 22, 2023 19:34
% Parse and check validity of the certificate dates, and if it fails, invoke `verify_fun` to
% hand over control to the caller in order to decide what to do.
case parse_and_check_validity_dates(OtpCert) of

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adresanta I do not think we need these empty lines between the case clauses. No other cases in the application are written like that. Otherwise I think this is now looking good.

@IngelaAndin IngelaAndin added testing currently being tested, tag is used by OTP internal CI and removed waiting waiting for changes/input from author labels Oct 24, 2023
@IngelaAndin IngelaAndin added the waiting waiting for changes/input from author label Oct 25, 2023
Copy link
Contributor

@IngelaAndin IngelaAndin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should adhere to coding style of the application. Otherwise I think it looks good :)

@andreasanta
Copy link
Contributor Author

We should adhere to coding style of the application. Otherwise I think it looks good :)

Done away with the spacing within case clauses, thank you.

@IngelaAndin IngelaAndin added testing currently being tested, tag is used by OTP internal CI and removed waiting waiting for changes/input from author testing currently being tested, tag is used by OTP internal CI labels Nov 1, 2023
@IngelaAndin
Copy link
Contributor

So last testing round, should be able to merge tomorrow.

@IngelaAndin
Copy link
Contributor

@andreasanta some other changes has created a merge conflict, could you please rebase your branch on latest maint and resolve it so that we can use the merge button,

@@ -160,7 +160,7 @@
-type oid() :: tuple().
-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 | duplicate_cert_in_path | {revoked, crl_reason()} | atom().
-type bad_cert_reason() :: cert_expired | invalid_issuer | invalid_signature | name_not_permitted | missing_basic_constraint | invalid_key_usage | duplicate_cert_in_path | {revoked, crl_reason()} | invalid_validity_dates | atom().
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line above 200 chars. break line please, so there is =< 100 chars per line.

pkix_path_validation_bad_date() ->
[{doc, "Ensure bad date formats in `validity` are handled gracefully by verify fun"}].
pkix_path_validation_bad_date(Config) when is_list(Config) ->

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please fix trailing whitespaces (marked red on screenshot)

image

@@ -140,19 +140,47 @@ prepare_for_next_cert(OtpCert, ValidationState = #path_validation_state{
%% current time.
%%--------------------------------------------------------------------
validate_time(OtpCert, UserState, VerifyFun) ->

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please stick to convention in module and don't add empty line after ->

lib/public_key/test/public_key_SUITE.erl Show resolved Hide resolved
@IngelaAndin IngelaAndin added waiting waiting for changes/input from author and removed testing currently being tested, tag is used by OTP internal CI labels Nov 7, 2023
@IngelaAndin
Copy link
Contributor

@andreasanta ping

@andreasanta
Copy link
Contributor Author

Rebased and adapted to style reqs. Thank you.

@andreasanta andreasanta reopened this Nov 14, 2023
@IngelaAndin
Copy link
Contributor

@andreasanta Thanks, need one more test day (night) for sanity check of the rebase and small adjustments and then we should finally be able to merge.

@IngelaAndin IngelaAndin added the testing currently being tested, tag is used by OTP internal CI label Nov 16, 2023
@IngelaAndin IngelaAndin merged commit e1b6fcd into erlang:maint Nov 17, 2023
29 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
team:PS Assigned to OTP team PS testing currently being tested, tag is used by OTP internal CI waiting waiting for changes/input from author
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants