Darwin: Git LFS Improperly Adds Certificates That Cause SSL Validation To Fail
Describe the Bug
In our organization (example.org
), we use the Jamf endpoint management system, which adds a self-signed SSL certificate with the CN:
CN=example.org JSS Built-in Certificate Authority
This causes SSL validation to fail when accessing Git LFS on example.org
:
Post "https://example.org/group/project/info/lfs/locks/verify": x509: certificate signed by unknown authority
This issue arises due to the following reasons:
- When accessing
example.org
endpoints, Git LFS checks if there are any custom certificates to trust for that host. - It calls
appendRootCAsForHostFromKeychain
with the hostnameexample.org
. - This function executes
/usr/bin/security find-certificate -a -p -c example.org <keychain>
, which performs a substring match on the Common Name. - If the System keychain contains a certificate with CN
example.org JSS Built-in Certificate Authority
, this gets returned by the fuzzy match. - Git LFS then adds this CA certificate to the trust store for connections to
example.org
. - When connecting to the real
example.org
, Git LFS is now trying to validate the official TLS certificate against this self-signed JAMF CA certificate, which will always fail.
To Reproduce
To reproduce this behavior, follow these steps:
- Install a self-signed cert that has a matching Git LFS server hostname in the CN.
- Attempt to pull or push LFS files.
System Environment
macOS
Output of git lfs env
git-lfs/3.6.1 (GitHub; darwin arm64; go 1.23.4)
git version 2.49.0
Endpoint=https://github.com/git-lfs/git-lfs.git/info/lfs (auth=none)
LocalWorkingDir=/Users/stanhu/github/git-lfs
LocalGitDir=/Users/stanhu/github/git-lfs/.git
LocalGitStorageDir=/Users/stanhu/github/git-lfs/.git
LocalMediaDir=/Users/stanhu/github/git-lfs/.git/lfs/objects
LocalReferenceDirs=
TempDir=/Users/stanhu/github/git-lfs/.git/lfs/tmp
ConcurrentTransfers=8
TusTransfers=false
BasicTransfersOnly=false
SkipDownloadErrors=false
FetchRecentAlways=false
FetchRecentRefsDays=7
FetchRecentCommitsDays=0
FetchRecentRefsIncludeRemotes=true
PruneOffsetDays=3
PruneVerifyRemoteAlways=false
PruneVerifyUnreachableAlways=false
PruneRemoteName=origin
LfsStorageDir=/Users/stanhu/github/git-lfs/.git/lfs
AccessDownload=none
AccessUpload=none
DownloadTransfers=basic,lfs-standalone-file,ssh
UploadTransfers=basic,lfs-standalone-file,ssh
GIT_APPEND_BUILD_OPTIONS=LIBPCREDIR=/opt/homebrew/opt/pcre2
GIT_EXEC_PATH=/opt/homebrew/opt/git/libexec/git-core
git config filter.lfs.process = "git-lfs filter-process"
git config filter.lfs.smudge = "git-lfs smudge -- %f"
git config.lfs.clean = "git-lfs clean -- %f"
Additional Context
It is not possible to make security find-certificate
return a strict match, but we can make the Git LFS code perform filtering. The following code snippet demonstrates how to achieve this:
diff --git a/lfshttp/certs_darwin.go b/lfshttp/certs_darwin.go
index 232e9871..547ff4f1 100644
--- a/lfshttp/certs_darwin.go
+++ b/lfshttp/certs_darwin.go
@@ -1,7 +1,9 @@
package lfshttp
import (
+ "bytes"
"crypto/x509"
+ "encoding/pem"
"regexp"
"strings"
@@ -57,8 +59,8 @@ func appendRootCAsForHostFromPlatform(pool *x509.CertPool, host string) *x509.Ce
return pool
}
-func appendRootCAsFromKeychain(pool *x509.CertPool, name, keychain string) *x509.CertPool {
- cmd, err := subprocess.ExecCommand("/usr/bin/security", "find-certificate", "-a", "-p", "-c", name, keychain)
+func appendRootCAsFromKeychain(pool *x509.CertPool, hostname, keychain string) *x509.CertPool {
+ cmd, err := subprocess.ExecCommand("/usr/bin/security", "find-certificate", "-a", "-p", "-c", hostname, keychain)
if err != nil {
tracerx.Printf("Error getting command to read keychain %q: %v", keychain, err)
return pool
@@ -68,5 +70,39 @@ func appendRootCAsFromKeychain(pool *x509.CertPool, name, keychain string) *x509
tracerx.Printf("Error reading keychain %q: %v", keychain, err)
return pool
}
- return appendCertsFromPEMData(pool, data)
+
+ // Parse PEM data into individual certificates
+ pemBlocks := []byte(data)
+ var validCerts [][]byte
+
+ for len(pemBlocks) > 0 {
+ var block *pem.Block
+ block, pemBlocks = pem.Decode(pemBlocks)
+ if block == nil {
+ break
+ }
+ if block.Type != "CERTIFICATE" {
+ continue
+ }
+
+ cert, err := x509.ParseCertificate(block.Bytes)
+ if err != nil {
+ continue
+ }
+
+ // Only include certificates with an exact CN match
+ if strings.ToLower(cert.Subject.CommonName) == strings.ToLower(hostname) {
+ validCerts = append(validCerts, pem.EncodeToMemory(block))
+ } else {
+ tracerx.Printf("Skipping certificate with non-exact CN match: %s", cert.Subject.CommonName)
+ }
+ }
+
+ // Add only the filtered certificates
+ if len(validCerts) > 0 {
+ allCerts := bytes.Join(validCerts, []byte{})
+ appendCertsFromPEMData(pool, allCerts)
+ }
+
+ return pool
}
Q&A
Q: What is the issue with Git LFS on macOS?
A: The issue arises when Git LFS attempts to access a self-signed SSL certificate with a matching hostname in the Common Name (CN) field. This causes SSL validation to fail, resulting in errors when pulling or pushing LFS files.
Q: What is the root cause of this issue?
A: The root cause lies in the appendRootCAsForHostFromKeychain
function, which performs a substring match on the Common Name field. This allows the self-signed JAMF CA certificate to be added to the trust store, causing SSL validation to fail.
Q: How can I reproduce this issue?
A: To reproduce this issue, follow these steps:
- Install a self-signed cert that has a matching Git LFS server hostname in the CN.
- Attempt to pull or push LFS files.
Q: What is the output of git lfs env
?
A: The output of git lfs env
will display the Git LFS environment variables, including the endpoint, local working directory, and local Git directory.
Q: How can I modify the Git LFS code to filter certificates?
A: You can modify the appendRootCAsFromKeychain
function to filter certificates based on an exact CN match. The modified code will parse the PEM data into individual certificates and only include those with an exact CN match.
Q: What is the modified code for filtering certificates?
A: The modified code is as follows:
diff --git a/lfshttp/certs_darwin.go b/lfshttp/certs_darwin.go
index 232e9871..547ff4f1 100644
--- a/lfshttp/certs_darwin.go
+++ b/lfshttp/certs_darwin.go
@@ -1,7 +1,9 @@
package lfshttp
import (
+ "bytes"
"crypto/x509"
+ "encoding/pem"
"regexp"
"strings"
@@ -57,8 +59,8 @@ func appendRootCAsForHostFromPlatform(pool *x509.CertPool, host string) *x509.Ce
return pool
}
-func appendRootCAsFromKeychain(pool *x509.CertPool, name, keychain string) *x509.CertPool {
- cmd, err := subprocess.ExecCommand("/usr/bin/security", "find-certificate", "-a", "-p", "-c", name, keychain)
+func appendRootCAsFromKeychain(pool *x509.CertPool, hostname, keychain string) *x509.CertPool {
+ cmd, err := subprocess.ExecCommand("/usr/bin/security", "find-certificate", "-a", "-p", "-c", hostname, keychain)
if err != nil {
tracerx.Printf("Error getting command to read keychain %q: %v", keychain, err)
return pool
@@ -68,5 +70,39 @@ func appendRootCAsFromKeychain(pool *x509.CertPool, name, keychain string) *x509
tracerx.Printf("Error reading keychain %q: %v", keychain, err)
pool
}
- return appendCertsFromPEMData(pool, data)
+
+ // Parse PEM data into individual certificates
+ pemBlocks := []byte(data)
+ var validCerts [][]byte
+
+ for len(pemBlocks) > 0 {
+ var block *pem.Block
+ block, pemBlocks = pem.Decode(pemBlocks)
+ if block == nil {
+ break
+ }
+ if block.Type != "CERTIFICATE" {
+ continue
+ }
+
+ cert, err := x509.ParseCertificate(block.Bytes)
+ if err != nil {
+ continue
+ }
+
+ // Only include certificates with an exact CN match
+ if strings.ToLower(cert.Subject.CommonName) == strings.ToLower(hostname) {
+ validCerts = append(validCerts, pem.EncodeToMemory(block))
+ } else {
+ tracerx.Printf("Skipping certificate with non-exact CN match: %s", cert.Subject.CommonName)
+ }
+ }
+
+ // Add only the filtered certificates
+ if len(validCerts) > 0 {
+ allCerts := bytes.Join(validCerts, []byte{})
+ appendCertsFromPEMData(pool, allCerts)
+ }
+
+ return pool
}
Q: How can I resolve this issue?
A: To resolve this issue, you can modify the Git LFS code to filter certificates based on an exact CN match, as shown in the modified code above. Alternatively, you can remove the self-signed JAMF CA certificate from the trust store.