-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Repair and restore all tests of cloning over TLS #5882
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
Repair and restore all tests of cloning over TLS #5882
Conversation
In commit 8e6641b of PR git-lfs#5803 we refactored our git-credential-lfstest test utility program so as to use a new internal "credential" structure to store the details of a given credential, as part of that PR's changes to support multi-stage authentication in a manner similar to that introduced in Git version 2.46.0. The git-credential-lfstest program acts as a Git credential helper, and therefore accepts "get" and "store" command-line arguments along with input data as described in the Git documentation: https://github.com/git/git/blob/4590f2e9412378c61eac95966709c78766d326ba/Documentation/gitcredentials.txt#L261-L286 https://github.com/git/git/blob/4590f2e9412378c61eac95966709c78766d326ba/Documentation/git-credential.txt#L103-L267 The Serialize() method of our new "credential" structure now encapsulates the logic that determines which credential fields to output in response to a command invocation with the "get" argument. Before commit 8e6641b, this logic was implemented in the fill() function, which handles "get" requests. When handling "get" requests for which the "authtype" credential field was not supplied (and no "authtype" capability was announced by the caller), and the credential record file read by the helper program does not indicate a response should be skipped, the current logic of the Serialize() method only outputs "username" and "password" fields if they were both found in the record file and are non-empty strings. However, the "clone ClientCert" test in our t/t-clone.sh script depends on credential record files we create in the directory identified by the CREDSDIR variable and which have empty values for the "username" field. These are used to supply the password for the encrypted certificate key file generated by our gitserver-lfstest utility program. In particular, when this test is run to completion, it executes a "git lfs clone" command using the URL on which our gitserver-lfstest program requires a client TLS/SSL certificate. We use the http.<url>.ssl{Cert,Key} configuration options to specify the client certificate files to Git. To decrypt the certificate, Git makes a request to the configured credential helper, sending empty "host" and "username" fields, a "path" field with the path to the certificate file, and a "protocol" field with the "cert" scheme. It expects our git-credential-lfstest helper to then return a "password" field with the passphrase for the encrypted client certificate file. Because the Serialize() method in our helper program now only returns "username" and "password" fields if they are both non-empty, it returns only the "host", "protocol", and "path" fields, without a "password" field. This prevents the test from succeeding, as Git then requests a password from the terminal since no helper provided one. Note, though, that due to the issue described in git-lfs#5658, this test does not actually run to completion at the moment, so we don't experience this problem in our CI jobs. We will address this issue in a subsequent commit in this PR, but as a first step, we need to at least restore the behaviour of our git-credential-lfstest helper program in the case where a credential record's file contains an empty "username" field and a non-empty "password" field. The original version of this code was introduced in 2015 in commit 5914d7d of PR git-lfs#306; it simply returned a "username" field if the input fields contained a "username" field, and the same for the "password" field. Prior to commit 8e6641b of PR git-lfs#5803 the helper program had evolved to handle some credential record files with formats other than simple "username" and "password" fields, but when returning those fields, continued to function like the original code in that it would return each field if there was a matching field among the input key/value pairs. Neither this approach, nor the current one, actually reflects how Git's own basic git-credential-store helper functions, nor others such as the git-credential-osxkeychain helper program. The git-credential-store program uses Git's credential_match() function to determine whether to return "username" and "password" fields for a given request, passing a zero (false) value for the match_password flag. For each of the "protocol", "host", "path", and "username" fields (but not the "password" field, because match_password is false) the credential_match() function checks if the field was provided in the request, and if so, that it matches the corresponding field in the credential record under consideration, assuming that field is also defined. Note that these fields may be defined but empty (i.e., be a pointer to zero-length string). If all the possible matches are met, then the function returns a non-zero (true) value. https://github.com/git/git/blob/4590f2e9412378c61eac95966709c78766d326ba/builtin/credential-store.c#L34 https://github.com/git/git/blob/4590f2e9412378c61eac95966709c78766d326ba/credential.c#L87-L98 The git-credential-osxkeychain helper program is similar in that if a "username" field is provided, it must match the stored record before anything is returned, even if both are empty strings. But if a "username" field is not provided, both it and the "password" field will be returned, assuming the other required fields like "host" and "protocol" match. To make our git-credential-lfstest helper more accurately reflect the behaviour of these real credential helpers, and to resolve the problem with our "clone ClientCert" test and Git's inability to retrieve the passphrase for our encrypted test TLS/SSL client certificate, we therefore revise the Serialize() method of our "credential" Go structure so that before returning a "password" field, it checks whether a "username" field was provided, and if it was, that it matches the "username" field of the credential record (even if both are empty). As well, if a "username" field was not provided, then the one in the credential record is returned along with the "password" field. Further, we do not require that either the "username" or "password" fields in the credential record be non-empty. This brings our test credential helper program into closer alignment with the actions of Git's own helpers, and again allows a request for a credential with an empty "username" field in the input to match a credential record with an empty "username" field. One consequence of these changes on their own is that we might now return empty "username" and "password" fields even when the credential record file does not specify either, but had a leading non-empty field and so is a record we parse as one with fields for "authtype" and "credential" data. For example, if the credential record file contains the line "foo::bar", then we understand it has having an "authtype" field with the value "foo" and a "credential" field with the value of "bar". If a request is made which lacks "authtype" and "capability[]" fields, but whose "host" field (and possibly also "path" field) map to this credential record file, we will now return "host", "username", and "password" fields, with the latter two having empty values, and nothing else. This is because the Serialize() method will only return "authtype" and "credential" fields if the request contains an "authtype" field, and a "capability[]" field with the value "authtype", and if the credential record also contains non-empty "authtype" and "credential" fields. When these conditions are not met, the method falls through to the conditional governing whether "username" and "password" fields are output, and with our new conditions for handling those, we now allow for a missing "username" input field. Hence, without additional changes, we would output empty "username" and "password" fields in such a case as described above. To be clear, this is not something which occurs with our test suite as it stands now. Nevertheless, we should ensure that we only output "username" and "password" fields if we have parsed the credential record file as having them. Therefore we add a check to the final conditional in the Serialize() method's logic, to ensure that we require an empty "authtype" field (i.e, a record such as ":foo:bar", with a blank leading field) before we will output "username" and "password" fields. In other words, we must have parsed the credential record file as one containing "username" and "password" field data before we will send those fields back to a caller.
In commit 08998d6 of PR git-lfs#5779 we updated our Git credential helper test program to reflect the new support in Git v2.46.0 for authentication schemes other than those using usernames and passwords, specifically by accepting input lines with the "capability[]" key and replying with similar lines as appropriate. In particular, if the client sends one or more "capability[]" lines, we check their values against the list of capabilities our helper program supports, which at the moment is just the "authtype" and "state" capabilities. If any matches are found, we include those in "capability[]" lines in our reply, excluding any capabilities the client announced but which our program does not support. At the moment, we do this by first overwriting the "capability[]" entry in the map of key/value pairs we created from the client's input, and then copying that entry into the "return" map of key/value pairs we will send back to the client. Finally, we make sure to output the values from the "capability[]" entry in that map before sending the values of all the remaining entries. (Note that each entry in these maps may have multiple values. If a key is multi-valued, we send each value on a separate line in the output.) We can simplify our handling of these "capability[]" lines somewhat by skipping the step where we overwrite the entry in the map of input lines, and also by not making a copy of the values we intend to return into the "result" map. Instead, we can just return the filtered set of capabilities we support from our discoverCapabilities() function, and then output the values from that set first before sending the values from our "return" map. This does mean that we only pass the filtered set of capabilities to the Serialize() method of our "credential" structure, instead of the full set received from the client. But this change has no functional impact because that method only needs to check for the capability types we support ("authtype" and "state"), so those will always be in the filtered set of capabilities, assuming the client sent them to us.
At present, our credential helper test program echoes a number of identifying input lines back to the client, including the "host", "protocol", and "path" lines (as well as certain "capability[]" lines when those are appropriate). However, common credential helper programs such as Git's own git-credential-store and the git-credential-osxkeychain helper for macOS generally avoid echoing such input lines back to the client, with the exception of "capability[]" lines where the helper is confirming which of the client's announced capabilities it supports. This is roughly in line with the advice in Git's documentation, which recommends against helpers overriding the values sent by clients, other than for "username" and "password" fields. https://github.com/git/git/blob/4590f2e9412378c61eac95966709c78766d326ba/Documentation/gitcredentials.txt#L288-L290 Of course, just echoing the client's data back will cause no functional difference in the client's behaviour, and is not a case where the helper is overriding the data provided by the client with different values. Nevertheless, we can simplify our helper's logic and output, and bring its operation into closer alignment with that of actual credential helper programs, if we do not echo the "host", "protocol", and "path" fields back to the client. This also allows us to exit early in the case where there are no fields to return other than possibly some "capability[]" ones.
Our credential helper test program looks for a credential record file in the directory specified by the CREDSDIR environment variable, using the values of the "host" and, if provided, "path" fields from the input lines sent by a client. At the moment, the correct construction of the paths and names of the files which the program then tries to open depends on the path in the CREDSDIR environment variable ending with a slash character. While our test suite always supplies a path with a trailing slash in the CREDSDIR variable, the requirement to do this adds a small amount of friction when debugging or testing our helper program. By refactoring the credsForHostAndPath() function in the program, we can both remove the necessity for a trailing slash on the CREDSDIR path and clarify the logic by which the function constructs file names and paths, so we do that now.
Due to the issue described in git-lfs#5658, the "clone ClientCert" test in our t/t-clone.sh test script does not actually run to completion, because it exits early due to an improper check of the TRAVIS variable (which is, itself, no longer used since we migrated our test suite to GitHub Actions in PR git-lfs#3808). We will address this issue in a subsequent commit in this PR, at which point the test will run fully, exposing a number of long-standing problems with the test. One of these occurs on Windows when the test checks the use of a TLS/SSL client certificate with an encrypted client key file. This check was added in commit 706beca of PR git-lfs#3270 in 2018, but may never have been fully validated since then. Our Windows CI jobs run in the Git Bash environment provided by the Git for Windows project, which in turn is based on the MSYS2 environment. In this context, when we set the "http.<url>.sslCert" Git configuration value in our setup() shell function in t/testhelpers.sh, the command-line argument with the path to the certificate file is converted by the MSYS2 environment into a Windows-style path with a leading volume identifier. When Git later makes a request to our credential helper test program to retrieve the passphrase for the encrypted key, it passes this path in the input "path" field. Our helper program then converts this path into a filename by replacing slash characters with dashes, and looks for a matching credential record file. However, we have created this file in the setup_creds() shell function using the Unix-style path in the LFS_CLIENT_KEY_FILE_ENCRYPTED environment variable we populate in our t/testenv.sh script. As a result, our credential helper program is unable to find the record file with the passphrase for the encrypted key, and Git then attempts to prompt the user for it, which also fails because our CI jobs run in a context without a terminal prompt, and the "clone ClientCert" test fails as a consequence. In theory we might work around this problem by setting the MSYS2_ARG_CONV_EXCL environment variable with a "*" value before calling "git config" to set the "http.<url>.sslCert" configuration value, as this would prevent MSYS2 from rewriting command-line arguments which look like Unix-style paths. Unfortunately, this introduces other failures into our test suite on Windows. Instead, we work around the problem by performing a simple Windows-to-Unix path translation in our credential helper test program. As we note in a code comment, a more comprehensive solution might invoke the "cygpath" utility to perform the path conversion. But as we only need to perform this translation step in a single test in our Windows CI jobs, for the deprecated "git lfs clone" command, for now we just look for paths which start with an uppercase volume identifier such as "D:" and replace them with a lowercase version which starts with a leading slash, like "/d". This is sufficient to resolve the problem in our current Windows CI jobs on GitHub Actions runners.
Our lfstest-gitserver test utility program was updated in commit daba49a of PR git-lfs#1893 to help test Git LFS's support for the use of client TLS/SSL certificates, and as part of this change, the program was adjusted to write out several new files upon startup with the values of the client certificate, key, and URL to be used when testing this Git LFS feature. In practice, the names of these files are always supplied by our test scripts in LFSTEST_CLIENT_* environment variables, as defined in our t/testenv.sh script and then passed to our lfstest-count-tests test utility program in the setup() shell function that is run at the start of every test script. The lfstest-count-tests program then invokes the lfstest-gitserver program, when necessary, and so the lfstest-gitserver program inherits the LFSTEST_CLIENT_* environment variables and uses the filenames defined by them. However, should the lfstest-gitserver program be started manually, without all of these environment variables, it will use a set of default filenames for the files it creates after starting. Two of these default filenames are identical, specifically the default names of the files containing the URLs the tests should use to contact the test server when validating regular TLS/SSL behaviour and TLS/SSL behaviour with a client certificate. As such, the URL for the former would be unavailable, as the file containing it would be overwritten by the file containing the latter's URL. We correct this minor issue now by defining a unique default name for the file which contains the URL the tests should use when validating TLS/SSL behaviour with a client certificate. Note again, though, that this makes no difference in the context of our test suite as it always supplies unique, non-default values for all these files' names in its environment variables.
larsxschneider
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for figuring all these things out! 🙇
| } else if len(c.authtype) == 0 && (len(username) == 0 || username[0] == c.username) { | ||
| if len(username) == 0 { | ||
| creds["username"] = []string{c.username} | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TBH the only thing that I am still puzzled about is why we check for len(c.authtype) == 0 here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried to explain the intention here at the very end of the description of commit ecd33b9 from this PR, which I'll quote below for reference, but the key point is that since commit c8822a3 of PR #5803 our test credential record files have one of two basic line formats, either :USERNAME:PASSWORD or AUTHTYPE::CREDENTIAL[:MATCH:STATE:MULTISTAGE]:
git-lfs/t/cmd/git-credential-lfstest.go
Lines 199 to 212 in 53d69fd
| // Each line in a file is of the following form: | |
| // | |
| // skip:: | |
| // The literal word "skip" means to skip emitting credentials. | |
| // AUTHTYPE::CREDENTIAL | |
| // If the authtype is not empty, then this is an authtype and | |
| // credential. | |
| // AUTHTYPE::CREDENTIAL:MATCH:STATE:MULTISTAGE | |
| // Like above, but this matches only if MATCH is empty or if the | |
| // state[] entry is present and matches "lfstest:MATCH". If so, | |
| // the value "lfstest:STATE" is emitted as the new state[] entry. | |
| // If MULTISTAGE is set to "true", then the multistage flag is set. | |
| // :USERNAME:PASSWORD | |
| // This is a normal username and password. |
So our Serialize() method operates on a credential structure whose values have been parsed from lines of this format, meaning if authtype is non-empty, it's a credential that's not a username/password combination, or if authtype is empty, it's a username/password credential.
When we're comparing a credential record file's data with the values provided by a client, we only really have two conditions:
- the credential record is of the
AUTHTYPE::CREDENTIAL[:...]format (so bothauthtypeandcredentialfields in thecredentialstructure are non-empty), which we'll consider if the client advertised anauthtypein itscapability[]line, OR - the credential record is of the
:USERNAME:PASSWORDformat (soauthtypeis empty and either or both ofusernameandpasswordare non-empty).
It's the second of those two cases of which this len(c.authtype) == 0 conditional is a part.
Here's a lightly edited extract from my description of commit ecd33b9 in this PR:
One consequence of these changes [to permit empty
usernamefields in credential records] on their own is that we might now return emptyusernameandpasswordfields even when the credential record file does not specify either, but had a leading non-empty field and so is a record we parse as one with fields forauthtypeandcredentialdata.For example, if the credential record file contains the line
foo::bar, then we understand it has having anauthtypefield with the valuefooand acredentialfield with the value ofbar. If a request is made which lacksauthtypeandcapability[]fields,... the [Serialize()] method falls through to the conditional governing whetherusernameandpasswordfields are output, and with our new conditions for handling those, we now allow for a missingusernameinput field. Hence, without additional changes, we would output emptyusernameandpasswordfields in such a case as described above.To be clear, this is not something which occurs with our test suite as it stands now. Nevertheless, we should ensure that we only output
usernameandpasswordfields if we have parsed the credential record file as having them.Therefore we add a check to the final conditional in the
Serialize()method's logic, to ensure that we require an emptyauthtypefield (i.e, a record such as:foo:bar, with a blank leading field) before we will outputusernameandpasswordfields. In other words, we must have parsed the credential record file as one containingusernameandpasswordfield data before we will send those fields back to a caller.
Due to the issue described in git-lfs#5658, the "clone ClientCert" test in our t/t-clone.sh test script does not actually run to completion, because it exits early due to an improper check of the TRAVIS variable (which is, itself, no longer used since we migrated our test suite to GitHub Actions in PR git-lfs#3808). We will address this issue in a subsequent commit in this PR, at which point the test will run fully, exposing a number of long-standing problems with the test. One of these occurs on Debian and Ubuntu Linux systems where the libcurl library used by Git depends on a version of the GnuTLS library older than v3.8.0, which at the moment, includes all of our Debian build target platforms (Debian 10, 11, and 12). Version 3.8.0 of GnuTLS contains a bug fix which allows it to parse and accept DEK-Info PEM headers with lowercase hexadecimal strings. Prior to this change, it would reject these with an "invalid request" error: gnutls/gnutls@4604bbd https://gitlab.com/gnutls/gnutls/-/merge_requests/1655 As it happens, the encrypted TLS certificate key generated by our lfstest-gitserver test utility has its DEK-Info header formatted with lowercase "a" through "f" letters, as this is what the EncodeToString() function of the Go language "encoding/hex" package generates, and that function is used by the (deprecated) EncryptPEMBlock() function of the "crypto/x509" package which our test server calls to create the encrypted key: https://github.com/golang/go/blob/7529d09a11496a77ccbffe245607fbd256200991/src/encoding/hex/hex.go#L17 https://github.com/golang/go/blob/7529d09a11496a77ccbffe245607fbd256200991/src/crypto/x509/pem_decrypt.go#L228 When our "clone ClientCert" test runs on a Debian system, GnuTLS rejects this key, which means Git in turn rejects it, and the test fails. Our "clone ClientCert" test is the only one which validates the use of the encrypted key, so it alone is affected by this problem, and because the test always exits early at the moment (due to the issue described in git-lfs#5658), we have not noticed the failure until now. We can work around the problem simply by rewriting the DEK-Info header returned by the EncryptPEMBlock() function to use uppercase "A" through "F" letters, which in turns allows GnuTLS to validate the salt, so Git proceeds and our test succeeds.
Due to the issue described in git-lfs#5658, the "cloneSSL" and "clone ClientCert" tests in our t/t-clone.sh test script do not run to completion but exit early, as a consequence of an improper check of the TRAVIS variable (which is no longer used since we migrated our test suite to GitHub Actions in PR git-lfs#3808). We will address this issue in a subsequent commit in this PR, at which point the tests will run fully, exposing a number of long-standing problems. One of these occurs on the legacy CentOS 7 Linux system where the libcurl library used by Git still depends on the libnss3 library, which has several restrictions in the types of TLS certificates it accepts. Specifically, in our current builds on CentOS 7, libcurl version 7.29.0 is linked against version 3.90 of libnss3. The libnss3 library rejects certificates which have both an Extended Key Usage attribute for TLS Web Server Authentication and a Basic Constraint setting of "CA:TRUE", meaning the certificate is intended to be used as a Certificate Authority: https://security.stackexchange.com/questions/176177/why-does-curl-nss-encryption-library-not-allow-a-ca-with-the-extended-key-usage However, certificates with both these settings are accepted by most modern TLS libraries, and the self-signed certificate used by our lfstest-gitserver utility program has both attributes. Note that this certificate is the one provided by the Go language's "net/http/httptest" package, specifically the one from the "net/http/internal/testcert" package: https://github.com/golang/go/blob/7529d09a11496a77ccbffe245607fbd256200991/src/net/http/internal/testcert/testcert.go#L10-L33 Thus when we enable our "clone ClientCert" test, the second part of the test, where it checks the use of an encrypted client key, will fail on CentOS 7 because Git on that platform is dependent on the libnss3 library for TLS certificate validation. Moreover, we would also like to reverse the change made in commit 1cf7d6b of PR git-lfs#1893, when the "http.<url>.sslVerify" Git configuration option was set to "false" in the setup() shell function of our t/testhelpers.sh library. This was done, according to the commit description, to try to get the "clone ClientCert" test working in the Travis CI environment. We no longer use the Travis CI service, as noted above, but independent of that, we want to be able to run all our tests with "http.<url>.sslVerify" set to its default value of "true". When we set the "http.<url>.sslVerify" Git configuration option to "true", however, three tests in the t/t-clone.sh test script (the "cloneSSL", "clone ClientCert", and "clone ClientCert with homedir certs" tests) will fail on CentOS 7 because libnss3 rejects the TLS certificate provided by the Go language test server, for the reasons described above. In these tests, libnss3 will return a "Certificate type not approved for application" SEC_ERROR_INADEQUATE_CERT_TYPE error. We expect to drop support for CentOS 7 soon, given that it has reached the end of official support in all but one distribution (SUSE Linux Enterprise Server 12 SP5, for which general support ends on October 31 of 2024). As well, CentOS 8 and more recent related distributions have switched away from building libcurl against libnss3; in fact, support for that TLS library has been dropped by the curl project entirely as of v8.3.0, per commit curl/curl@7c8bae0. Therefore, rather than try to generate a different TLS certificate for our lfstest-gitserver utility program, we simply skip the three affected tests when we detect that they are running on a Linux platform where the git-remote-https binary depends on libnss3. We can remove these exceptions later once we drop support for CentOS 7.
Due to the issue described in git-lfs#5658, the "cloneSSL" and "clone ClientCert" tests in our t/t-clone.sh test script do not run to completion but exit early, as a consequence of an improper check of the TRAVIS variable (which is no longer used since we migrated our test suite to GitHub Actions in PR git-lfs#3808). As well, since the "clone ClientCert with homedir certs" test was added in commit c54c9da, as part of PR git-lfs#5657, we have skipped executing it on Windows. We do this to avoid an error when Git attempts to retrieve the encrypted key of the test's client TLS certificate from our git-credential-lfstest test utility program. We will address the first issue in subsequent commit in this PR, at which point the tests will run fully, exposing a number of long-standing problems with the "cloneSSL" and "clone ClientCert" tests. One of these problems occurs on Windows, where by default, Git uses the Secure Channel (SChannel) backend for TLS/SSL verification in libcurl. However, Schannel's CertGetNameString() function rejects the TLS certificate of our lfstest-gitserver program because we connect to the server on the local IP address 127.0.0.1, and although the certificate lists that address in an IPAddress field among its Subject Alternative Name (SAN) attributes, the Schannel library only looks for hostname matches with the DNSName fields and ignores any IPAddress fields. As a result, Git returns a "failed to match connection hostname (127.0.0.1) against server certificate names" error, as produced by libcurl. Since version 8.9.0 of libcurl the CertGetNameString() function is called with the CERT_NAME_DNS_TYPE option, which is documented to have this behaviour: https://github.com/curl/curl/blob/5040f7e94cd01decbe7ba8fdacbf489182d503dc/lib/vtls/schannel_verify.c#L577-L582 https://github.com/curl/curl/blob/5040f7e94cd01decbe7ba8fdacbf489182d503dc/lib/vtls/schannel_verify.c#L373-L378 https://learn.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-certgetnamestringw#cert_name_dns_type The TLS certificate presented by our lfstest-gitserver program is the one provided by the Go language's "net/http/httptest" package, specifically the one from the "net/http/internal/testcert" package: https://github.com/golang/go/blob/7529d09a11496a77ccbffe245607fbd256200991/src/net/http/internal/testcert/testcert.go#L10-L33 While it does include a DNSName field in its SAN list with the hostname "example.com", attempting to use this in our tests would involve forcing both Git and Git LFS to override the available system DNS resolution. For Git LFS in particular, there does not appear to be, at the moment, a straightforward way of overriding the DNS resolution of the Go language's "net" package in a cross-platform and convenient manner. Instead, with recent versions of Git and libcurl, we can bypass this problem by setting the "http.sslBackend" Git configuration option to "openssl", and thereby forcing the use of OpenSSL in place of Schannel in libcurl. This option has been available since commit git/git@21084e8 in Git version 2.20.0, and is supported so long as libcurl's version is 7.56.0 or above. Since our CI jobs on Windows are executed on GitHub Actions, where the pre-installed Git for Windows releases are kept current, we can make use of this Git option in our tests when TLS certificate validation is required. Note that this option does not affect how Git LFS behaves, because it uses the TLS validation provided by the Go "net/http" and "crypto/tls" packages. Also, because the intent of our tests is to confirm correct behaviour of Git LFS and not Git, using OpenSSL instead of Schannel to ensure Git accepts our test server's TLS certificate on Windows does not compromise the integrity of our tests. (Further, having our tests actually run to completion will be a significant improvement over the current state, where they trivially pass in all circumstances due to the bug in our conditional check of the TRAVIS variable.) As for the problem mentioned earlier regarding the reason we skip the "clone ClientCert with homedir certs" test on Windows, this also stems from the use of the Schannel backend. Specifically, Git reports the error "refusing to work with credential missing host field" when libcurl uses the Schannel library, and so we have always skipped this test on Windows. When we enable the "clone ClientCert" test, by removing the check on the TRAVIS variable, it too will report the same error. We can again resolve this problem for both tests by switching to the OpenSSL backend, as noted in a discussion on the Git mailing list: https://lore.kernel.org/git/TY0PR06MB54426B92E745B3035F0CC2DFD197A@TY0PR06MB5442.apcprd06.prod.outlook.com/ So for these tests we also set the "http.sslBackend" Git configuration option to "openssl" when executing them on Windows. Notably, this will allow the "clone ClientCert with homedir certs" test to run successfully on that platform for the first time since it was introduced.
In commit cdec9f2 of PR git-lfs#5699 we updated two tests in our t/t-clone.sh test script to ensure they add and commit the ".gitattributes" files created by "git lfs track" before proceeding to generate commit data with Git LFS objects. Per the commit description, this change was made so as to be explicit about the order of expected Git operations, and to avoid relying on the "git lfs clone" command to search for Git LFS pointer files in the current Git working tree. However, two other tests in the same test script have not run for a long time, due to the bug described in git-lfs#5658, where an improper check of the TRAVIS variable causes the tests to exit early and always declare success. Hence, the need to make corresponding changes to ensure these tests also explicitly add and commit their ".gitattributes" files was not clear at the time when commit cdec9f2 was written. We will enable these skipped tests in a subsequent commit in this PR, at which time they will fail unless we also adjust them to call "git add" and "git commit" on their ".gitattributes" files, so we do that now. We also take the opportunity to make matching changes to a third pair of tests, the "clone ClientCert with homedir certs" and "clone with flags" tests, although these do run fully and successfully now. While they do not depend on the ".gitattributes" file being committed to their respective test repositories, aligning their code with that of the other tests in the same test script may help avoid problems in the future.
In commit db4e3b2 of PR git-lfs#2811 we updated the progress meter text of the Git LFS client from "Git LFS" to the more explicit "Uploading LFS objects" or "Downloading LFS objects". A large number of tests were changed in the same commit because they check the progress meter output in logs they capture from Git LFS operations. However, the "cloneSSL" test in our t/t-clone.sh test script was not updated because it has not actually run to completion for a long time, as described in git-lfs#5658, so it did not fail when the progress meter text changed. Due to a bug in the conditional used to check the TRAVIS variable, the test instead always exits early and declares success. We will address this problem in a subsequent commit in this PR, at which point the test will fail unless we adjust it to check for the same progress meter text as all our other tests, so we do that now. We also take the opportunity to add a similar check of the same progress meter message to the "clone ClientCert" test. This brings our tests in the t/t-clone.sh script into slightly closer alignment with each other, which should help ensure they stay consistent in the future. (Note that this test does not run to completion at the moment either, for the same reason as the "cloneSSL" test.)
In commit caaa0f9 of PR git-lfs#2647 a number of tests of the "git lfs clone" and "git lfs pull" commands were enhanced so as to check that after those commands are invoked, the "git status" command returns a "working tree clean" message. To perform these checks, a call to our assert_clean_status() shell function was added to the tests. In the case of the "cloneSSL" test, an assert_clean_status() call was added, but left commented out; it was then uncommented in commit e0eede1 of the same PR. Unfortunately, the call is made when the current working directory has not yet been changed to that of the newly cloned repository's working tree, and so should fail as it stands now. However, as described in git-lfs#5658, the "cloneSSL" test and the "clone ClientCert" tests in our t/t-clone.sh test script do not actually run to completion, a consequence of an improper check of the TRAVIS variable (which is no longer used since we migrated our test suite to GitHub Actions in PR git-lfs#3808). Instead, the tests exit early and always declare success. We will address this problem in a subsequent commit in this PR, and when we do so, the "cloneSSL" test will fail because the misplaced assertion. Therefore we move the assertion into the block of checks performed after a "pushd" shell command changes the current directory to that of the new clone's working tree, which will allow the test to pass when we resolve the bug with the TRAVIS variable check. We also take the opportunity to add assert_clean_status() calls to a number of other tests in the t/t-clone.sh test script, so the tests are all performing similar sets of checks. This will help us keep the tests in that script more consistent with each other in the future.
In commit d2c1c0f of PR git-lfs#1067 the "cloneSSL" test in what is now our t/t-clone.sh test script was changed with the intent that the test should be skipped when it was run by the Travis CI service. The commit description from that time notes that we could not determine why the test was failing on just that platform, so the decision was made to simply ignore the test in the Travis CI environment. Subsequently, similar checks were added to three other tests. First, the "clone ClientCert" test was introduced in PR git-lfs#1893, and since commit b471566 in that PR it has included an initial check for a Travis CI environment with the intent of skipping the test when it is run there. Next, in commits 9da65c0 of PR git-lfs#2500 and e53e34e of PR git-lfs#2609 the "askpass: push with core.askPass" and "askpass: push with SSH_ASKPASS" tests in what is now our t/t-askpass.sh test script were amended to also skip executing in the Travis CI context. In the case of the latter two tests, this check works as designed; however, in the case of the two tests in t/t-clone.sh, the check is written as a simple "if $TRAVIS" shell statement, rather than one which uses a shell test command or builtin (i.e., "test" or "[" or "[["). As described in git-lfs#5658, the result is that when the TRAVIS variable is undefined, these statements always succeed, and so their conditional blocks run, meaning these tests are skipped on every platform and system. In previous commits in this PR we have addressed all the latent bugs that have accumulated in these tests since this problem was first introduced, and so we are now in a position to remove both the valid and invalid checks for the TRAVIS environment variable from all four of the tests. This will ensure that our CI jobs always run the entire set of tests from both of the t/t-askpass.sh and t/t-clone.sh test scripts.
In PR git-lfs#1893 we introduced support for TLS client certificates, and while that was largely successful, some of our CI test jobs ran on the Travis CI platform, and those encountered problems when validating the TLS certificate generated by our lfstest-gitserver test utility program. Several attempts were made to resolve this issue, but ultimately in commit 1cf7d6b the "http.<url>.sslVerify" Git configuration value was set to "false" for our entire test suite, just to allow the "clone ClientCert" test to pass in the Travis CI environment. However, we no longer use the Travis CI service, as we migrated our CI jobs to GitHub Actions in PR git-lfs#3808. Further, in a prior commit in this PR we revised several tests in our t/t-clone.sh test script so they do not run on the legacy CentOS 7 platform, where the libcurl library used by Git depends on the libnss3 library. The libnss3 library does not accept the TLS certificate generated by our lfstest-gitserver program, so these tests would otherwise fail on CentOS 7 if the "http.<url>.sslVerify" option is set to "true". As well, in another commit in this PR we altered the same tests in our t/t-clone.sh test script to require that Git and libcurl use OpenSSL for TLS verification on Windows, rather than the default Schannel backend. This will prevent the tests from failing if TLS verification is enabled, since Schannel rejects the TLS certificate that we use in our lfstest-gitserver test utility, but OpenSSL accepts it. With these changes, all our tests should now pass if we allow the "http.<url>.sslVerify" Git configuration option to default to "true". Therefore we now remove the command which set that option to "false" for our whole test suite.
In prior commits in this PR we addressed a number of problems in our test suite in order to fully enable all the tests in our t/t-clone.sh test script, which previously did not run to completion, as described in git-lfs#5658. In one commit we updated our git-credential-lfstest test utility program so it performs a simple Windows-to-Unix path translation on the path provided by Git when it makes a credential helper request to the program. In the case of the "clone ClientCert" test in our t/t-clone.sh test script, this path is the one we set in the "http.<url>.sslCert" Git configuration value, and in the Git Bash environment we use in our Windows CI jobs, the MSYS2 environment has converted this path into a Windows-style one. However, in order to locate the appropriate credential record file, our test credential helper requires a path that matches the Unix-style one we used when setting up those record files. We therefore adjusted the git-credential-lfstest program to simply look for a leading uppercase Windows volume identifier like "D:" and convert it into a lowercase version with a leading slash character like "/d". In another commit we revised our lfstest-gitserver test utility program so that after it generates the encrypted TLS certificate key that is used in our "clone ClientCert" test, it converts the DEK-Info header of the certificate key to use uppercase hexadecimal digits rather than lowercase ones. This resolves a problem in which the test fails on our Debian build target platforms (Debian 10, 11, and 12) because the GnuTLS library used by libcurl and therefore also by Git is older than v3.8.0 and lacks support for lowercase hexadecimal strings in its parsing of DEK-Info headers. As suggested by larsxschneider in PR review, we can clarify the intent of both of these sections of code where we convert bytes from lowercase to uppercase, or vice versa, by using the standard Go library functions to perform the conversions, so we do that now.
711b8f9 to
d0d7d91
Compare
In commit 365d615 of PR git-lfs#1478 a pair of tests in what is now our t/t-clone.sh test script were enhanced to check that the "git lfs clone" command did not create a directory named "lfs" within the top level of a cloned repository's working tree, as the "lfs" directory should instead be created within the ".git" directory. This check has since been replicated in a number of other tests in the t/t-clone.sh script, but not all of them, so we add this check in the relevant sections of the tests where it is not yet performed. We also adjust one check which dates from commit 365d615 to use the same -e shell test as all the other checks, rather than the -f test. These changes are not especially important, but in conjunction with the revisions we will make in other commits in this PR, along with those we made recently in PRs git-lfs#5882 and git-lfs#5906, they will help synchronize the tests in the t/t-clone.sh script so they are more consistent with each other and complete.
In commit c54c9da of PR git-lfs#5657 we added support for the use of client TLS/SSL certificate and key files in a user's home directory, specifically, when their paths are prefixed with a tilde character. This change aligned the Git LFS client's support of the "http.<url>.sslCert" and "http.<url>.sslKey" Git configuration options with that of Git itself. As part of this change, the "clone ClientCert with homedir certs" test was added to our t/t-clone.sh test script. This test sets the "http.<url>.sslCert" and "http.<url>.sslKey" configuration options to paths beginning with a tilde, in order to validate our support for such paths. As well, the setup() and setup_creds() functions in our t/testhelpers.sh shell library were revised to create credential record files for our git-credential-lfstest helper program, corresponding to the paths of the certificate file and private key file which the "clone ClientCert with homedir certs" test copies into the location specified by the HOME environment variable. That location is set by our t/testlib.sh script to a directory named "home" under the temporary directory path stored in our TRASHDIR variable, where all our tests generate their various artifacts. In practice, however, because the "clone ClientCert with homedir certs" test never sets the "http.sslCertPasswordProtected" Git configuration option or the GIT_SSL_CERT_PASSWORD_PROTECTED environment variable, and because it never sets the "http.<url>.sslKey" option to point to an encrypted version of the private key file, neither Git nor Git LFS ever invoke the "git credential fill" command. As a result, our credential helper program does not run, and so the record files created by the setup_creds() function for use by this test are never opened. Moreover, the "clone ClientCert with homedir certs" test never actually runs the "git lfs clone" command, unlike all the other tests in the t/t-clone.sh test script. In previous commits in this PR we have refactored the "clone ClientCert" test, which precedes the "clone ClientCert with homedir certs" test, so that it more clearly distinguishes between the use of encrypted and unencrypted private key files, and explains the conditions under which Git and Git LFS will retrieve a passphrase for an encrypted certificate or private key file. We therefore copy the main loop from the "clone ClientCert" test into the "clone ClientCert with homedir certs" test so that the latter now runs the "git lfs clone" command twice, once using the unencrypted version of the private key file, and once using the encrypted version. Like the "clone ClientCert" test, the "clone ClientCert with homedir certs" test validates the clone create by the "git lfs clone" command on each pass through its loop, and also runs a regular "git clone" command afterward to ensure it succeeds as well. The test now needs to copy the encrypted private key file specified by the LFS_CLIENT_KEY_FILE_ENCRYPTED variable into the test home directory, since that location is then used as the value of the "http.<url>.sslKey" configuration option during the second iteration of the test loop. We also slightly refactor the sequence of other initial steps performed by the "clone ClientCert with homedir certs" test to more closely align with the "clone ClientCert" test, such as by moving the definition of the "reponame" variable later in the test. We reorder the sequence in which we copy the certificate and key files and set the "http.<url>.sslCert" and "http.<url>.sslKey" configuration options so they follow the same sequence used in other tests and scripts. As well, we use quotation marks around the configuration option key names, which again aligns with our general (but not consistent) practice. In a prior commit in this PR we added a detailed set of code comments to the "clone ClientCert" test, which we replicate in the "clone ClientCert with homedir certs" test. These comments help explain the conditions under which Git and Git LFS query or do not query the credential helper via a "git credential fill" command, and why we need two record files for our credential helper, both with the same passphrase but each associated with a different file path. Finally, because the test now establishes the record files used by the git-credential-lfstest helper, akin to how the "clone ClientCert" test does also, there is no need to create these record files in the common setup_creds() function in the t/testhelpers.sh library. Thus we can eliminate the calls to the write_creds_file() function for these files from the setup_creds() function, and also remove the definition of the "homecertpath" and "homekeypath" variables from the library as well. Note that when we call the write_creds_file() function to create the credential record files we need to use paths which align with what our git-credential-lfstest helper will construct from the values it receives in the "path" fields of its input. On Windows these "path" fields will contain Windows-style paths with a leading volume identifier like "D:". As discussed in commit bc11a31 of PR git-lfs#5882, our git-credential-lfstest helper converts such paths so they begin with a lowercase letter preceded by a slash character, like "/d", in order to match the paths found in the variables used to create the filenames of the credential record files in our "clone ClientCert" test. In our CI jobs, which run in the Git Bash environment, which uses the MSYS2 environment, these LFS_CLIENT_CERT_FILE and LFS_CLIENT_KEY_FILE_ENCRYPTED variables contain paths of the "mixed" Cygwin form, so they have Unix-style slash characters but start with a leading volume identifier segment such as "/d". In the "clone ClientCert with homedir certs" test, because we do not use these variables to construct the filenames of the credential record files, but the HOME environment variable instead, we have to perform the same path translation using the "cygwin" program, and then converting its output so volume identifiers like "D:" are converted into the "/d" form. This ensures the record files we create have the same names as the git-credential-lfstest helper will construct. Note, too, that because the colon character is disallowed in Windows filenames (other than in a volume identifier), we can not simply use the "D:" form of the paths when constructing the record file names, so this technique resolves that problem as well.
In commit e4e5ada of PR git-lfs#5882 we added an initial check to the t/t-clone.sh test script which detects whether libcurl is linked against the legacy libnss3 library on Linux, and if so, sets a GIT_LIBNSS variable. We then check that variable at the start of several tests in the script, and if it is set, skip performing the tests. This workaround was introduced because the libnss3 library rejects TLS/SSL certificates which have both an Extended Key Usage attribute for TLS Web Server Authentication and a Basic Constraint setting of "CA:TRUE", and the self-signed certificate used by our lfstest-gitserver utility program has both attributes, so our tests would otherwise fail. The only platform on which our tests run which used the legacy libnss3 library was the one based on Red Hat Enterprise Linux (RHEL) 7. All the distribution versions based on RHEL 7, CentOS 7, and SUSE Linux Enterprise Server (SLES) 12 for which we previously built RPM packages have now reached the end of their standard LTS (Long-Term Support) periods, and so future releases of the Git LFS client will no longer build packages for them. We therefore removed the Dockerfile we used to build packages for the RHEL/CentOS 7 and SLES 12 platforms in commit git-lfs/build-dockers@cfde130 of PR git-lfs/build-dockers#71. As well, in a prior commit in this PR we removed the entries for these platforms from the list of our supported Linux distribution versions in the DistroMap Ruby class source file, which is utilized by several scripts run by our GitHub Actions CI and release workflow jobs. Since our CI and release workflows will no longer run on any platforms where libcurl is linked against the legacy libnss3 library, we can now also remove the checks for the libnss3 library from our t/t-clone.sh test script.
Our
t/t-clone.shtest script includes one test which is skipped on Windows and two tests that have been accidentally completely disabled for many years.This PR restores all three tests to full functionality on all our CI and build platforms, with the exception of the legacy CentOS 7 Linux distribution.
It should be easiest to review this PR on a commit-by-commit basis. Each commit contains a detailed description of its changes.
(Note that we expect to further revise the tests in the
t/t-clone.shtest script in subsequent PRs, once they are functional again.)Resolves #5658.
Background
As described in #5658, the
cloneSSLandclone ClientCerttests contain invalid checks for the Travis CI environment, which in practice cause the tests to trivially pass in all contexts. Theif $TRAVISshell statement, which lacks any test command or shell builtin liketestor[, evaluates as true when theTRAVISvariable is not defined. Since we migrated our CI jobs to GitHub Actions in PR #3808, this environment variable is never set, so the tests are always skipped.Further, the
clone ClientCert with homedir certstest is skipped because the default TLS validation library on Windows does not accept the TLS certificate of ourlfstest-gitservertest utility.We also set the
http.*.sslVerifyGit configuration option tofalsefor our entire test suite, which is a legacy of an attempt get theclone ClientCerttest to succeed in the Travis CI service in PR #1893.Change Summary
Before we can remove the broken checks for the Travis CI environment, we first resolve a number of problems which have accumulated in the tests over the years. Some of these are relatively trivial, but others are fairly involved, and sometimes only affect a single platform.
After making these fixes, we remove all the remaining checks of the
TRAVISvariable, including two checks in thet/t-askpass.shtest script that do not have the same shell scripting bug as the ones in thet/t-clone.shscript.Finally, we remove the command which sets the
http.*.sslVerifyGit configuration tofalse, thus allowing it to default totrueagain for all our tests.Change Details
We make several revisions to our
git-credential-lfstesttest utility in order to permit theclone ClientCerttest to pass, and also take the opportunity to refactor and simplify its handling of input and output fields, as well as to align its behaviour more closely with that of thegit-credential-store(1)command and other actual Git credential stores. One key change we make is to ensure thegit-credential-lfstestutility accepts an emptyusernamefield, as it did prior to PR #5803. We also make sure that it rewrites paths with Windows volume identifiers into Unix-style paths so that it can find its credential record files when running in the Git Bash environment on Windows.Next, to help the
clone ClientCerttest pass on the Debian systems we currently run in CI (Debian 10, 11, and 12), we update ourlfstest-gitserverutility so that it rewrites theDEK-Infoheader of the encrypted key it generates with uppercaseAthroughFletters in its hexadecimal value string. Without this workaround, the version of the GnuTLS library used by Git and libcurl on our Debian CI systems rejects the encrypted key file, due to a bug which has subsequently been fixed as of version 3.8.0 of GnuTLS by commit gnutls/gnutls@4604bbd in merge request 1655.We then revise the three tests in
t/t-clone.shso they are skipped when Git and libcurl have a dependency on the legacy libnss3 library, as is only the case for our builds on the legacy CentOS 7 platform. Support for this TLS library has been removed entirely from curl as of version 8.3.0, in commit curl/curl@7c8bae0. On CentOS 7, though, Git and libcurl are built with libnss3, and it rejects the TLS certificate presented by ourlfstest-gitserverprogram, due to an apparently overly-strict interpretation of RFC 5280. Note that we expect to drop support for CentOS 7 builds soon, as the only equivalent platform which has not reached the end of its support lifecycle is SUSE Linux Enterprise Server 12 SP5, and its general support ends this month. Once we no longer build on CentOS 7, we can remove this PR's checks for the libnss3 library from the tests int/t-clone.sh.The last non-trivial change we make is to set the
http.sslBackendGit configuration option toopensslwhen running our TLS cloning tests on Windows. The default TLS verification backend used by Git and libcurl on Windows is the Secure Channel (Schannel) library, which rejects the certificate presented by ourlfstest-gitserverprogram, but not for the same reason as libnss3 does. Instead, Schannel does not read theIPAddressfields of the certificate's Subject Alternative Name (SAN) attributes, one of which has the IPv4 address 127.0.0.1, and that is the address we use to connect to our test Git server. Schannel only looks for a matchingDNSNamefield, and when it does not find one, it rejects the certificate. As we only care that our tests validate the behaviour of Git LFS, not Git itself, changing Git and libcurl's TLS backend to OpenSSL does not compromise the integrity of our tests.