Skip to content

Allow the certificate check to be ignored - #4042

Closed
tiennou wants to merge 2 commits into
libgit2:masterfrom
tiennou:cert-cb-passthrough
Closed

Allow the certificate check to be ignored#4042
tiennou wants to merge 2 commits into
libgit2:masterfrom
tiennou:cert-cb-passthrough

Conversation

@tiennou

@tiennou tiennou commented Dec 20, 2016

Copy link
Copy Markdown
Contributor

Fixes #3440

@carlosmn

Copy link
Copy Markdown
Member

It looks like you've misunderstood the purpose of the GIT_PASSTHROUGH return value. Its purpose is not to ignore the check, but to ignore that the certificate check callback was set.

Some systems (my primary motivation are binidngs) might want to always set a certificate check callback for simplicity in their setup but if they themselves have users which have not set the certificate check, returning this lets them and the library behave as though it wasn't set.

What this code instead does is ignore the determination from the library about the validity of the certificate which is something we don't want to do in the library. If someone wants to ignore the certificate, we want them to return the all-ok return code.

@tiennou

tiennou commented Dec 21, 2016

Copy link
Copy Markdown
Contributor Author

I was actually worried that it would allow such easy circumventing of certificate checks 😆 .

I understand the motivation behind, but I'm not sure how to model that. IMHO the test I wrote was correct, in that the bindings certificate_check_cb, finding that there was no actual "checker function" would return E_PASSTHROUGH. If you have the time, could you describe (even in pseudo-code) what a correct test would be ? Then I'll arrange to fix the behavior.

@carlosmn

carlosmn commented Dec 21, 2016

Copy link
Copy Markdown
Member

The simplest way I think would be to store the previous error and then return that if the callback returns GIT_PASSTHROUGH, something like

cberror = callback(...)
if cberror < 0 && cberror != GIT_PASSTHROUGH
    error = cberror
    handle error message, return error

should be almost enough. The main point is that if we would have returned GIT_ECERTIFICATE if the pointer isn't set, then we must still return that. At least for the http code, that should still let if fall out of the if with GIT_ECERTIFICATE in error.

@tiennou

tiennou commented Dec 21, 2016

Copy link
Copy Markdown
Contributor Author

Should we allow the user to return anything he wants ? I think the http & ssh backend differ already, so I'm considering supporting only the following cases :

  • user returns 0 : GIT_OK
  • user returns E_PASSTHROUGH: we return the last error if there was one, or we continue
  • user returns anything else: we error, returning GIT_EUSER

Opinions ?

@ethomson

Copy link
Copy Markdown
Member

user returns anything else: we error, returning GIT_EUSER

For anything non-zero, and not E_PASSTHROUGH, we should maintain the return code and bubble it back up to the user.

If I'm a user, and I return -42 from a callback and suddenly my call exits with -1 then that's surprising. I want to be able to propagate return values from my callback to myself.

Related: #2716

@tiennou

tiennou commented Dec 21, 2016

Copy link
Copy Markdown
Contributor Author

Thanks for the cross-ref, that clears it up. IMHO, I don't think it's sane because I don't think there's a way to discern a libgit2 error code from one of my (user) error codes, but that's beside the point. What might make it cleaner is stash the user error code inside git_error and return GIT_EUSER.

@ethomson

Copy link
Copy Markdown
Member

I don't think it's sane because I don't think there's a way to discern a libgit2 error code from one of my (user) error codes

There is, that's what GIT_EUSER is for.

My callbacks want to be able to round trip error codes; if my callback runs out of memory, I want to return GITERR_NOMEMORY. That way my error handling code will handle it in a single case, whether libgit2 is the one that had a failed malloc or whether it was my code. I don't really care where I ran out of memory, just that I did run out of memory.

Otherwise, I would have to set some flag in my callback, out_of_memory, then return GIT_EUSER. Then all my code that does some networking that might invoke the callback has to if (error == GIT_ENOMEMORY || (error == GIT_EUSER && out_of_memory)). Which is pretty disappointing.

This is obviously a contrived example, but I think that it illustrates that error handling is simplified if you don't try to coalesce all callback returns to a single error code.

This doesn't mean that anybody has to use these error codes, and generally they shouldn't, but it allows them to use them when it simplifies their lives.

@tiennou

tiennou commented Dec 22, 2016

Copy link
Copy Markdown
Contributor Author

It seems I might not understand the relation between GIT_EUSER and the GITERR_* classes... I was under the impression that I shouldn't need to touch those GITERR_* values (and the docs warn that they might not be preserved, as per giterr_set_str). IMHO it also doesn't work in the face of error code conflicts. For example, let's say I'm wrapping another library as a smart transport, I'll have to remap every of its error codes to something that's not already used by us so I don't accidentally trigger error handling within libgit2 by reusing -16 (GIT_EAUTH). API-wise this would prevent us from adding more negative GIT_E* values since there might be any number of remaps out there.

My (original) proposal would be to do have internals calls to client code handled like this :

int user_error = ...
if (user_error < 0) {
  giterr_set_user(user_error);
  return GIT_EUSER;
}

so the client can do

int err = git_function();
if (err == GIT_USER) {
  /* grab the underlying error from giterr_last() */
} else if (err  != GIT_OK) {
  /* libgit2 error */
}

Another way would be to make a new GITERR_USER error class. That would allow the error to be preserved as well as providing a nicer string message.

Sorry, I have a knack for nice error handling 😉.

@ethomson

Copy link
Copy Markdown
Member

It seems I might not understand the relation between GIT_EUSER and the GITERR_* classes...

You probably do, I wrote GITERR_NOMEMORY without really thinking about it. That was my mistake and I apologize for accidentally being misleading. It was meant to be an example; consider GIT_EAMBIGUOUS, as a more accurate one.

IMHO it also doesn't work in the face of error code conflicts.

There shouldn't be any, because you shouldn't be returning your own error codes. You need to return libgit2 error codes. If an odb backend returns GIT_ELOCKED, then that should get sent all the way back to the caller. The bindings will then convert this to a LockedException or what not.

Custom backends do this today and these semantics are already well agreed upon. Your example of converting everything to GIT_EUSER breaks these sorts of semantics.

For example, let's say I'm wrapping another library as a smart transport, I'll have to remap every of its error codes to something that's not already used by us so I don't accidentally trigger error handling within libgit2 by reusing -16 (GIT_EAUTH).

Yes, this is exactly what you need to do. These error values are completely meaningless to libgit2 and you shouldn't return them to the library. You need to return a libgit2 error code that the library understand.

If you're writing a custom backend (be it transport, odb, refdb, etc) and you need to communicate with it, then you should set a flag in the backend that your library consumer can interrogate later. That's what GIT_EUSER is for, generally speaking.

This looks like what you're trying to do with giterr_set_user, but this sort of custom error storage doesn't belong in the library, it's your business logic.

@tiennou

tiennou commented Dec 22, 2016

Copy link
Copy Markdown
Contributor Author

At least I'm green on the error class vs error code nuance 😉 .

You need to return libgit2 error codes.

That's new to me, and I don't think it's clearly documented anywhere ? So callbacks are expected to use GIT_EUSER on any "unmappable" git_error_code ? That clears it up.

we should maintain the return code and bubble it back up to the user.

you should set a flag in the backend

This looks like what you're trying to do with giterr_set_user

It is, as a way of preserving user error codes without conflating them with our own. So a user can know whether it's a libgit2 failure, or something caused by "external" code called by libgit2, and still know what was the underlying error without having to jump though hoops (I'm not familiar with the custom backend code).

Using the custom backends as an example, apart from the redis one, none of them use giterr_set_str, and none return GIT_EUSER. The way I envision it, GIT_E* codes would be reserved to the library, and anything failing external functions would either return != GIT_EUSER and might be clobbered, or return GIT_EUSER (and optionally call giterr_set_user(code, msg)). At the outer layer, I would be able to discern a generic custom odb failure/libgit2 (using != GIT_EUSER), or something like "the database server is unhappy because whatever" (using GIT_EUSER and the optional user error code).

Again, I'm just thinking out loud. If you think it's out of scope, fine (at least the current rules are now clear to me, even though IMHO they're ambiguous).

@tiennou
tiennou force-pushed the cert-cb-passthrough branch from 979be48 to 2b35ecd Compare January 13, 2017 20:33
@tiennou

tiennou commented Jan 13, 2017

Copy link
Copy Markdown
Contributor Author

Rebased, now that the security fixes are in. I had shuffled all cert-related tests around (I should have included that, sorry). I hope I got the tests right, because they pass 😉.

Comment thread tests/online/cert.c Outdated
{
GIT_UNUSED(cert);

struct cert_options *options = payload;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The windows builds fail because this isn't declared at the top of the block.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Oops, will fix ASAP !

@carlosmn

Copy link
Copy Markdown
Member

It looks like the test doesn't quite work for WinHTTP. There we seem to be going all the way through to requesting the repository and thus getting a 404 instead of our own error.

@tiennou

tiennou commented Apr 17, 2017

Copy link
Copy Markdown
Contributor Author

Should be good now. The failure comes from a HTTP 502 from bitbucket.

@tiennou
tiennou force-pushed the cert-cb-passthrough branch 2 times, most recently from d5ea513 to 77ed35c Compare July 7, 2017 00:36
@tiennou

tiennou commented Jul 7, 2017

Copy link
Copy Markdown
Contributor Author

Rebased. I don't think the failures are related, though I might be wrong.

@pks-t

pks-t commented Jul 10, 2017

Copy link
Copy Markdown
Member

Failures are unrelated, yes. They are due to our recently broken master branch when our bundled zlib is used, which I've fixed a few days ago. The other errors are due to the usual segfaults with poxyproxy... sigh Time to finally get our test suite polished up again.

@tiennou
tiennou force-pushed the cert-cb-passthrough branch from 77ed35c to dc2fb1d Compare September 16, 2017 22:13
Comment thread tests/online/cert.c Outdated
g_cert_options.return_code = GIT_EPEEL;

cl_git_fail_with(GIT_EPEEL,
git_clone(&g_repo, "https://wrong.host.badssl.com/fake.git", "./fake", &g_options));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

GIT_EPEEL is a very confusing error to use here. GIT_EUSER would be much better as it's the one we have set up for people to return and have it passed all the way back.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ok, will fix. The intent was to make sure we preserved whatever the users' callback replied to us (hence the non-sensical GIT_EPEEL).

Comment thread tests/online/cert.c
g_cert_options.return_code = GIT_PASSTHROUGH;

cl_git_fail_with(GIT_ERROR,
git_clone(&g_repo, "https://wrong.host.badssl.com/fake.git", "./fake", &g_options));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This should be returning GIT_ECERTIFICATE as the passthrough means we want to use the result that the library was going to use if we didn't have the callback.

I believe this GIT_ERROR/-1 actually comes from there not actually being a git repository at the given location.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'll have to recheck what's actually going on w.r.t to the first part. The second part is correct, we're actually succeeding the cert check, but the clone machinery obviously fails down the line, returning GIT_ERROR.

Comment thread tests/online/cert.c Outdated
git_clone(&g_repo, "https://expired.badssl.com/fake.git", "./fake", NULL));

cl_git_fail_with(GIT_ECERTIFICATE,
git_clone(&g_repo, "https://expired.badssl.com/fake.git", "./fake", &opts));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This isn't testing anything other than us bubbling up the return code, does it? We've already tested this elsewhere.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm merging all cert-related test cases in this one, so the "elsewhere" is this one now, thus it's testing that the TLS layer doesn't accept invalid certificates by default.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm merging all cert-related test cases in this one,

And I wish you hadn't done that, because I'm now reading the badssl test cases as though they're testing how we handle the feature you're writing instead of what they're for.

Comment thread tests/online/cert.c Outdated
cl_git_fail_with(GIT_ECERTIFICATE,
git_clone(&g_repo, "https://wrong.host.badssl.com/fake.git", "./fake", NULL));
cl_git_fail_with(GIT_ECERTIFICATE,
git_clone(&g_repo, "https://wrong.host.badssl.com/fake.git", "./fake", &opts));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It's the same here as above. Adding the callback here doesn't add anything to any of these tests which are about making sure we do reject a bunch of common certificate errors.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Adding the callback is there to check we don't have a difference in behavior w.r.t invalid certificates whether there's a callback or not. I can remove if you feel like it's too much though.

Comment thread tests/online/cert.c Outdated
git_clone(&g_repo, "https://github.com/libgit2/TestGitRepository", "./fake", &g_options));
}

void test_online_cert__certificate_invalid_ssh(void)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This test name (and __certificate_invalid above) is misleading. These certificates are valid, but we're testing here the ability to abort a clone in the certificate callback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed, would __valid_certificate_user_invalidated be somewhat clearer that we respect the users' decision ?

@tiennou
tiennou force-pushed the cert-cb-passthrough branch 2 times, most recently from cf87b14 to 546f4bd Compare November 14, 2017 23:04
@tiennou

tiennou commented Nov 14, 2017

Copy link
Copy Markdown
Contributor Author

Rebased, with a focus on only passthrough (so the test cases aren't merged anymore) and squashed everything into one commit, with some more testing on top (and comments). I'm surprised how confusing this code can become when looking at it too much 😫.

I have been tempted to move that whole certificate check thing into a helper function somewhere, for DRY reasons, but didn't find a satisfying location.

@tiennou
tiennou force-pushed the cert-cb-passthrough branch from 546f4bd to 8be2da2 Compare February 2, 2018 23:41
@tiennou
tiennou force-pushed the cert-cb-passthrough branch from 8be2da2 to a4ac903 Compare April 11, 2018 20:21
Comment thread src/transports/winhttp.c Outdated
}

giterr_clear();
giterr_state_capture(&git_error_state, GIT_ERROR);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Shouldn't this be giterr_state_capture(&error_state, GIT_ERROR);?

@tiennou
tiennou force-pushed the cert-cb-passthrough branch 2 times, most recently from 66943e4 to a16670e Compare April 19, 2018 08:45
@tiennou

tiennou commented Apr 19, 2018

Copy link
Copy Markdown
Contributor Author

I don't understand the linking failure MSVC reports. I've tried both using the GIT_EXTERN macro and not marking those function as extern, and it doesn't want to link, even though those same function are used in git_clone and I haven't seen conflicting GIT_WIN32 defines.

For reference, function-not-found error, link error.

@ethomson Any insight on what might be wrong ?

@pks-t pks-t left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm surprised this could've passed on Travis

Comment thread src/transports/winhttp.c Outdated
if (error < 0 && !giterr_last())
if (error == GIT_PASSTHROUGH) {
/* Nothing could have failed previously, return success */
git_error_restore(&error_state);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This has to be giterr_state_restore

Comment thread src/transports/winhttp.c Outdated
} else if (error < 0 && !giterr_last()) {
giterr_set(GITERR_NET, "user cancelled certificate check");
}
git_error_free(&error_state);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This has to be giterr_state_free

@tiennou

tiennou commented Apr 20, 2018

Copy link
Copy Markdown
Contributor Author

🙄 Good grief, thanks @pks-t. Travis doesn't care about WinHTTP so that explains it.

@tiennou
tiennou force-pushed the cert-cb-passthrough branch from a16670e to 6bf7a9b Compare April 20, 2018 16:37
@tiennou
tiennou force-pushed the cert-cb-passthrough branch 2 times, most recently from 6a1c87e to 99acd3f Compare April 21, 2018 22:16
Now transports just need to call `git_transport_smart_certificate_check`, and the user's GIT_PASSTHROUGH reply will be handled for them.
@tiennou
tiennou force-pushed the cert-cb-passthrough branch from 99acd3f to 0735656 Compare April 22, 2018 14:08
Comment thread src/transports/smart.c

return t->certificate_check_cb(cert, valid, hostname, t->message_cb_payload);
return error;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

All this code shouldn't go here. This function is a way for code external to the library to execute the certificate check callback, The error you want to insert here may not exist in the code implementing a transport. They may be catching an exception several languages apart, and there isn't going to be any error state to capture.

If we need common code inside the library to do this repeated code, that needs its own function.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I really wanted to make that passthrough-dance generic across all transports, and having it here removes the need for an external transport implementation to handle this case on their side. I can easily move it in a private function, but IMHO that kinda defeat the purpose, since it will no longer be "externally usable". I also like the eat-your-own-dogfood aspect of it.

The usage pattern I had in mind was that the external transport would "check the certificate" while connecting (whatever that means), call this with cert_error set to GIT_OK for valid, GIT_ECERTIFICATE for invalid (that's the "non-existing error case" from your point above, which our SSH transport demonstrates), anything else for spurious unrelated error, and this function would automatically preserve the callback error (which might not even be aware that an external transport is in use) or restore the transport's original success/failure state on a passthrough.

I'm not sure I understand your point about exceptions being several languages apart : should it be safe for an higher-level (say Ruby) external transport to raise an exception and catch that around the (say git_remote_download) call that started the activity, without going through our code at all 😨 ?

As an API sidenote, I was actually wondering about the usage pattern of those functions : the external code might end up calling a NULL function pointer, and I don't think there's a way for external users to know they're about to SEGV (git_transport_smart_credentials shares the same problem).

@tiennou

tiennou commented Jan 29, 2019

Copy link
Copy Markdown
Contributor Author

This has been tackled as part of #4879.

@tiennou tiennou closed this Jan 29, 2019
@tiennou
tiennou deleted the cert-cb-passthrough branch January 29, 2019 20:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants