Skip to content

Conversation

@chrisd8088
Copy link
Member

Our t/t-clone.sh test 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.sh test script in subsequent PRs, once they are functional again.)

Resolves #5658.

Background

As described in #5658, the cloneSSL and clone ClientCert tests contain invalid checks for the Travis CI environment, which in practice cause the tests to trivially pass in all contexts. The if $TRAVIS shell statement, which lacks any test command or shell builtin like test or [, evaluates as true when the TRAVIS variable 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 certs test is skipped because the default TLS validation library on Windows does not accept the TLS certificate of our lfstest-gitserver test utility.

We also set the http.*.sslVerify Git configuration option to false for our entire test suite, which is a legacy of an attempt get the clone ClientCert test 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 TRAVIS variable, including two checks in the t/t-askpass.sh test script that do not have the same shell scripting bug as the ones in the t/t-clone.sh script.

Finally, we remove the command which sets the http.*.sslVerify Git configuration to false, thus allowing it to default to true again for all our tests.

Change Details

We make several revisions to our git-credential-lfstest test utility in order to permit the clone ClientCert test 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 the git-credential-store(1) command and other actual Git credential stores. One key change we make is to ensure the git-credential-lfstest utility accepts an empty username field, 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 ClientCert test pass on the Debian systems we currently run in CI (Debian 10, 11, and 12), we update our lfstest-gitserver utility so that it rewrites the DEK-Info header of the encrypted key it generates with uppercase A through F letters 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.sh so 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 our lfstest-gitserver program, 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 in t/t-clone.sh.

The last non-trivial change we make is to set the http.sslBackend Git configuration option to openssl when 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 our lfstest-gitserver program, but not for the same reason as libnss3 does. Instead, Schannel does not read the IPAddress fields 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 matching DNSName field, 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.

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.
@chrisd8088 chrisd8088 requested a review from a team as a code owner October 9, 2024 08:16
Copy link
Member

@larsxschneider larsxschneider left a 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}
}
Copy link
Member

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.

Copy link
Member Author

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]:

// 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:

  1. the credential record is of the AUTHTYPE::CREDENTIAL[:...] format (so both authtype and credential fields in the credential structure are non-empty), which we'll consider if the client advertised an authtype in its capability[] line, OR
  2. the credential record is of the :USERNAME:PASSWORD format (so authtype is empty and either or both of username and password are 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 username fields in credential records] 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,... the [Serialize()] 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.

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.
@chrisd8088 chrisd8088 force-pushed the tests-remove-travis-skips branch from 711b8f9 to d0d7d91 Compare October 29, 2024 20:39
@chrisd8088 chrisd8088 merged commit f7bd4fd into git-lfs:main Oct 29, 2024
10 checks passed
@chrisd8088 chrisd8088 deleted the tests-remove-travis-skips branch October 29, 2024 21:27
chrisd8088 added a commit to chrisd8088/git-lfs that referenced this pull request Nov 18, 2024
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.
chrisd8088 added a commit to chrisd8088/git-lfs that referenced this pull request Nov 18, 2024
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.
chrisd8088 added a commit to chrisd8088/git-lfs that referenced this pull request Jun 19, 2025
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Some tests are functionally disabled due to if $TRAVIS

2 participants