Skip to contentSkip to conversation

osxkeychain: bring in line with other credential helpers

PATCH In review Bo Anderson via GitGitGadget 7 participants 19 messages Feb 17, 2024 – Apr 2, 2024

lore
Share & tools
lore
#0 lore raw

cover osxkeychain: bring in line with other credential helpers

git-credential-osxkeychain has largely fallen behind other external credential helpers in the features it supports, and hasn't received any functional changes since 2013. As it stood, osxkeychain failed seven tests in the external credential helper test suite:

not ok 8 - helper (osxkeychain) overwrites on store
not ok 9 - helper (osxkeychain) can forget host
not ok 11 - helper (osxkeychain) does not erase a password distinct from input not ok 15 - helper (osxkeychain) erases all matching credentials not ok 18 - helper (osxkeychain) gets password_expiry_utc
not ok 19 - helper (osxkeychain) overwrites when password_expiry_utc changes not ok 21 - helper (osxkeychain) gets oauth_refresh_token

osxkeychain also made use of macOS APIs that had been deprecated since 2014. Replacement API was able to be used without regressing the minimum supported macOS established in 5747c8072b (contrib/credential: avoid fixed-size buffer in osxkeychain, 2023-05-01).

After this set of patches, osxkeychain passes all tests in the external credential helper test suite.

Bo Anderson (4):
osxkeychain: replace deprecated SecKeychain API
osxkeychain: erase all matching credentials
osxkeychain: erase matching passwords only
osxkeychain: store new attributes

 contrib/credential/osxkeychain/Makefile       |   3 +-
 .../osxkeychain/git-credential-osxkeychain.c  | 376 ++++++++++++++----
 2 files changed, 310 insertions(+), 69 deletions(-)

base-commit: 3e0d3cd5c7def4808247caf168e17f2bbf47892b
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1667%2FBo98%2Fosxkeychain-update-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1667/Bo98/osxkeychain-update-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/1667

Signature · 1 line
gitgitgadget
#1 lore raw

PATCH 1/4 osxkeychain: replace deprecated SecKeychain API

From: Bo Anderson <[email protected]>

The SecKeychain API was deprecated in macOS 10.10, nearly 10 years ago. The replacement SecItem API however is available as far back as macOS 10.6.

While supporting older macOS was perhaps prevously a concern, git-credential-osxkeychain already requires a minimum of macOS 10.7 since 5747c8072b (contrib/credential: avoid fixed-size buffer in osxkeychain, 2023-05-01) so using the newer API should not regress the range of macOS versions supported.

Adapting to use the newer SecItem API also happens to fix two test failures in osxkeychain:

8 - helper (osxkeychain) overwrites on store
9 - helper (osxkeychain) can forget host

The new API is compatible with credentials saved with the older API.

---
 contrib/credential/osxkeychain/Makefile       |   3 +-
 .../osxkeychain/git-credential-osxkeychain.c  | 265 +++++++++++++-----
 2 files changed, 199 insertions(+), 69 deletions(-)
contrib/credential/osxkeychain/Makefile +2 -1
index 4b3a08a2bac..238f5f8c36f 100644
@@ -8,7 +8,8 @@ CFLAGS = -g -O2 -Wall
 -include ../../../config.mak
 
 git-credential-osxkeychain: git-credential-osxkeychain.o
-	$(CC) $(CFLAGS) -o $@ $< $(LDFLAGS) -Wl,-framework -Wl,Security
+	$(CC) $(CFLAGS) -o $@ $< $(LDFLAGS) \
+		-framework Security -framework CoreFoundation
 
 git-credential-osxkeychain.o: git-credential-osxkeychain.c
 	$(CC) -c $(CFLAGS) $<
contrib/credential/osxkeychain/git-credential-osxkeychain.c +197 -68

360 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#5 lore raw

PATCH 2/4 osxkeychain: erase all matching credentials

In reply to Bo Anderson via GitGitGadget

From: Bo Anderson <[email protected]>

Other credential managers erased all matching credentials, as indicated by a test case that osxkeychain failed:

15 - helper (osxkeychain) erases all matching credentials

---
 contrib/credential/osxkeychain/git-credential-osxkeychain.c | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)
contrib/credential/osxkeychain/git-credential-osxkeychain.c +2 -1
index dc294ae944a..e9cee3aed45 100644
@@ -182,7 +182,8 @@ static OSStatus delete_internet_password(void)
 	if (!protocol || !host)
 		return -1;
 
-	attrs = CREATE_SEC_ATTRIBUTES(NULL);
+	attrs = CREATE_SEC_ATTRIBUTES(kSecMatchLimit, kSecMatchLimitAll,
+				      NULL);
 	result = SecItemDelete(attrs);
 	CFRelease(attrs);
 
Signature · 1 line
gitgitgadget
#6 lore raw

PATCH 3/4 osxkeychain: erase matching passwords only

In reply to Bo Anderson via GitGitGadget

From: Bo Anderson <[email protected]>

Other credential helpers support deleting credentials that match a specified password. See 7144dee3ec (credential/libsecret: erase matching creds only, 2023-07-26) and cb626f8e5c (credential/wincred: erase matching creds only, 2023-07-26).

Support this in osxkeychain too by extracting, decrypting and comparing the stored password before deleting.

Fixes the following test failure with osxkeychain:

11 - helper (osxkeychain) does not erase a password distinct from input

---
 .../osxkeychain/git-credential-osxkeychain.c  | 56 ++++++++++++++++++-
 1 file changed, 55 insertions(+), 1 deletion(-)
contrib/credential/osxkeychain/git-credential-osxkeychain.c +55 -1
index e9cee3aed45..9e742796336 100644
@@ -169,9 +169,55 @@ static OSStatus find_internet_password(void)
 	return result;
 }
 
+static OSStatus delete_ref(const void *itemRef)
+{
+	CFArrayRef item_ref_list;
+	CFDictionaryRef delete_query;
+	OSStatus result;
+
+	item_ref_list = CFArrayCreate(kCFAllocatorDefault,
+				      &itemRef,
+				      1,
+				      &kCFTypeArrayCallBacks);
+	delete_query = create_dictionary(kCFAllocatorDefault,
+					 kSecClass, kSecClassInternetPassword,
+					 kSecMatchItemList, item_ref_list,
+					 NULL);
+
+	if (password) {
+		/* We only want to delete items with a matching password */
+		CFIndex capacity;
+		CFMutableDictionaryRef query;
+		CFDataRef data;
+
+		capacity = CFDictionaryGetCount(delete_query) + 1;
+		query = CFDictionaryCreateMutableCopy(kCFAllocatorDefault,
+						      capacity,
+						      delete_query);
+		CFDictionarySetValue(query, kSecReturnData, kCFBooleanTrue);
+		result = SecItemCopyMatching(query, (CFTypeRef *)&data);
+		if (!result) {
+			if (CFEqual(data, password))
+				result = SecItemDelete(delete_query);
+
+			CFRelease(data);
+		}
+
+		CFRelease(query);
+	} else {
+		result = SecItemDelete(delete_query);
+	}
+
+	CFRelease(delete_query);
+	CFRelease(item_ref_list);
+
+	return result;
+}
+
 static OSStatus delete_internet_password(void)
 {
 	CFDictionaryRef attrs;
+	CFArrayRef refs;
 	OSStatus result;
 
 	/*
@@ -183,10 +229,18 @@ static OSStatus delete_internet_password(void)
 		return -1;
 
 	attrs = CREATE_SEC_ATTRIBUTES(kSecMatchLimit, kSecMatchLimitAll,
+				      kSecReturnRef, kCFBooleanTrue,
 				      NULL);
-	result = SecItemDelete(attrs);
+	result = SecItemCopyMatching(attrs, (CFTypeRef *)&refs);
 	CFRelease(attrs);
 
+	if (!result) {
+		for (CFIndex i = 0; !result && i < CFArrayGetCount(refs); i++)
+			result = delete_ref(CFArrayGetValueAtIndex(refs, i));
+
+		CFRelease(refs);
+	}
+
 	/* We consider not found to not be an error */
 	if (result == errSecItemNotFound)
 		result = errSecSuccess;
Signature · 1 line
gitgitgadget
#7 lore raw

PATCH 4/4 osxkeychain: store new attributes

In reply to Bo Anderson via GitGitGadget

From: Bo Anderson <[email protected]>

d208bfdfef (credential: new attribute password_expiry_utc, 2023-02-18) and a5c76569e7 (credential: new attribute oauth_refresh_token, 2023-04-21) introduced new credential attributes but support was missing from git-credential-osxkeychain.

Support these attributes by appending the data to the password in the keychain, separated by line breaks. Line breaks cannot appear in a git credential password so it is an appropriate separator.

Fixes the remaining test failures with osxkeychain:

18 - helper (osxkeychain) gets password_expiry_utc 19 - helper (osxkeychain) overwrites when password_expiry_utc changes
21 - helper (osxkeychain) gets oauth_refresh_token

---
 .../osxkeychain/git-credential-osxkeychain.c  | 68 +++++++++++++++++--
 1 file changed, 62 insertions(+), 6 deletions(-)
contrib/credential/osxkeychain/git-credential-osxkeychain.c +62 -6
index 9e742796336..6a40917b1ef 100644
@@ -6,10 +6,12 @@
 #define ENCODING kCFStringEncodingUTF8
 static CFStringRef protocol; /* Stores constant strings - not memory managed */
 static CFStringRef host;
+static CFNumberRef port;
 static CFStringRef path;
 static CFStringRef username;
 static CFDataRef password;
-static CFNumberRef port;
+static CFDataRef password_expiry_utc;
+static CFDataRef oauth_refresh_token;
 
 static void clear_credential(void)
 {
@@ -17,6 +19,10 @@ static void clear_credential(void)
 		CFRelease(host);
 		host = NULL;
 	}
+	if (port) {
+		CFRelease(port);
+		port = NULL;
+	}
 	if (path) {
 		CFRelease(path);
 		path = NULL;
@@ -29,12 +35,18 @@ static void clear_credential(void)
 		CFRelease(password);
 		password = NULL;
 	}
-	if (port) {
-		CFRelease(port);
-		port = NULL;
+	if (password_expiry_utc) {
+		CFRelease(password_expiry_utc);
+		password_expiry_utc = NULL;
+	}
+	if (oauth_refresh_token) {
+		CFRelease(oauth_refresh_token);
+		oauth_refresh_token = NULL;
 	}
 }
 
+#define STRING_WITH_LENGTH(s) s, sizeof(s) - 1
+
 __attribute__((format (printf, 1, 2), __noreturn__))
 static void die(const char *err, ...)
 {
@@ -197,9 +209,27 @@ static OSStatus delete_ref(const void *itemRef)
 		CFDictionarySetValue(query, kSecReturnData, kCFBooleanTrue);
 		result = SecItemCopyMatching(query, (CFTypeRef *)&data);
 		if (!result) {
-			if (CFEqual(data, password))
+			CFDataRef kc_password;
+			const UInt8 *raw_data;
+			const UInt8 *line;
+
+			/* Don't match appended metadata */
+			raw_data = CFDataGetBytePtr(data);
+			line = memchr(raw_data, '\n', CFDataGetLength(data));
+			if (line)
+				kc_password = CFDataCreateWithBytesNoCopy(
+						kCFAllocatorDefault,
+						raw_data,
+						line - raw_data,
+						kCFAllocatorNull);
+			else
+				kc_password = data;
+
+			if (CFEqual(kc_password, password))
 				result = SecItemDelete(delete_query);
 
+			if (line)
+				CFRelease(kc_password);
 			CFRelease(data);
 		}
 
@@ -250,6 +280,7 @@ static OSStatus delete_internet_password(void)
 
 static OSStatus add_internet_password(void)
 {
+	CFMutableDataRef data;
 	CFDictionaryRef attrs;
 	OSStatus result;
 
@@ -257,7 +288,23 @@ static OSStatus add_internet_password(void)
 	if (!protocol || !host || !username || !password)
 		return -1;
 
-	attrs = CREATE_SEC_ATTRIBUTES(kSecValueData, password,
+	data = CFDataCreateMutableCopy(kCFAllocatorDefault, 0, password);
+	if (password_expiry_utc) {
+		CFDataAppendBytes(data,
+		    (const UInt8 *)STRING_WITH_LENGTH("\npassword_expiry_utc="));
+		CFDataAppendBytes(data,
+				  CFDataGetBytePtr(password_expiry_utc),
+				  CFDataGetLength(password_expiry_utc));
+	}
+	if (oauth_refresh_token) {
+		CFDataAppendBytes(data,
+		    (const UInt8 *)STRING_WITH_LENGTH("\noauth_refresh_token="));
+		CFDataAppendBytes(data,
+				  CFDataGetBytePtr(oauth_refresh_token),
+				  CFDataGetLength(oauth_refresh_token));
+	}
+
+	attrs = CREATE_SEC_ATTRIBUTES(kSecValueData, data,
 				      NULL);
 
 	result = SecItemAdd(attrs, NULL);
@@ -268,6 +315,7 @@ static OSStatus add_internet_password(void)
 		CFRelease(query);
 	}
 
+	CFRelease(data);
 	CFRelease(attrs);
 
 	return result;
@@ -339,6 +387,14 @@ static void read_credential(void)
 			password = CFDataCreate(kCFAllocatorDefault,
 						(UInt8 *)v,
 						strlen(v));
+		else if (!strcmp(buf, "password_expiry_utc"))
+			password_expiry_utc = CFDataCreate(kCFAllocatorDefault,
+							   (UInt8 *)v,
+							   strlen(v));
+		else if (!strcmp(buf, "oauth_refresh_token"))
+			oauth_refresh_token = CFDataCreate(kCFAllocatorDefault,
+							   (UInt8 *)v,
+							   strlen(v));
 		/*
 		 * Ignore other lines; we don't know what they mean, but
 		 * this future-proofs us when later versions of git do
Signature · 1 line
gitgitgadget
#2 lore raw

Re: [PATCH 1/4] osxkeychain: replace deprecated SecKeychain API

In reply to Bo Anderson via GitGitGadget
Quoted 13 lines · Bo Anderson via GitGitGadget

The SecKeychain API was deprecated in macOS 10.10, nearly 10 years ago. The replacement SecItem API however is available as far back as macOS 10.6.

While supporting older macOS was perhaps prevously a concern, git-credential-osxkeychain already requires a minimum of macOS 10.7 since 5747c8072b (contrib/credential: avoid fixed-size buffer in osxkeychain, 2023-05-01) so using the newer API should not regress the range of macOS versions supported.

Adapting to use the newer SecItem API also happens to fix two test failures in osxkeychain:

Quoted 6 lines

8 - helper (osxkeychain) overwrites on store
9 - helper (osxkeychain) can forget host

The new API is compatible with credentials saved with the older API.

I haven't studied the SecItem API, so I can't comment on the meat of the patch, but I can make a few generic observations...

Quoted 8 lines
contrib/credential/osxkeychain/git-credential-osxkeychain.c +4 -1
@@ -3,14 +3,39 @@
-__attribute__((format (printf, 1, 2)))
+#define ENCODING kCFStringEncodingUTF8
+static CFStringRef protocol; /* Stores constant strings - not memory managed */
+static CFStringRef host;
[...]
+

contrib/credential/osxkeychain/git-credential-osxkeychain.c line 38

Quoted 10 lines
contrib/credential/osxkeychain/git-credential-osxkeychain.c +10 -0
+static void clear_credential(void)
+{
+       if (host) {
+               CFRelease(host);
+               host = NULL;
+       }
+       [...]
+}
+
+__attribute__((format (printf, 1, 2), __noreturn__))

The addition of __noreturn__ to the __attribute__ seems unrelated to the stated purpose of this patch. As such, it typically would be placed in its own patch. If it really is too minor for a separate patch, mentioning it in the commit message as a "While at it..." would be helpful.

contrib/credential/osxkeychain/git-credential-osxkeychain.c static void die(const char *err, ...) · line 55

Quoted 13 lines
contrib/credential/osxkeychain/git-credential-osxkeychain.c +12 -0
@@ -19,70 +44,135 @@ static void die(const char *err, ...)
+static CFDictionaryRef create_dictionary(CFAllocatorRef allocator, ...)
+{
+       va_list args;
+       const void *key;
+       CFMutableDictionaryRef result;
+
+       result = CFDictionaryCreateMutable(allocator,
+                                          0,
+                                          &kCFTypeDictionaryKeyCallBacks,
+                                          &kCFTypeDictionaryValueCallBacks);
+
+

Style: one blank line is preferred over two

contrib/credential/osxkeychain/git-credential-osxkeychain.c static void die(const char *err, ...) · line 78

Quoted 8 lines
contrib/credential/osxkeychain/git-credential-osxkeychain.c +8 -0
+       va_start(args, allocator);
+       while ((key = va_arg(args, const void *)) != NULL) {
+               const void *value;
+               value = va_arg(args, const void *);
+               if (value)
+                       CFDictionarySetValue(result, key, value);
+       }
+       va_end(args);

A couple related comments...

If va_arg() ever returns NULL for value, the next iteration of the loop will call va_arg() again, but calling va_arg() again after it has already returned NULL is likely undefined behavior. At minimum, I would have expected this to be written as:

    while (...) {
        ...
        if (!value)
            break;
        CFDictionarySetValue(...);
    }

However, isn't it a programmer error if va_arg() returns NULL for value? If so, I'd think we'd want to scream loudly about that rather than silently ignoring it. So, perhaps something like this:

    while (...) {
        ...
        if (!value) {
            fprintf(stderr, "BUG: ...");
            abort();
        }
        CFDictionarySetValue(...);

}

Or, perhaps just call the existing die() function in this file with a suitable "BUG ..." message.

contrib/credential/osxkeychain/git-credential-osxkeychain.c static void die(const char *err, ...) · line 113

Quoted 12 lines
contrib/credential/osxkeychain/git-credential-osxkeychain.c +10 -0
+static void find_username_in_item(CFDictionaryRef item)
 {
+       CFStringRef account_ref;
+       char *username_buf;
+       CFIndex buffer_len;
 
+       account_ref = CFDictionaryGetValue(item, kSecAttrAccount);
+       if (!account_ref)
+       {
+               write_item("username", "", 0);
+               return;
+       }

Style: opening brace sticks to the if line:

    if !(account_ref) {
        ...
    }

Same comment applies to the if below.

contrib/credential/osxkeychain/git-credential-osxkeychain.c static void die(const char *err, ...) · line 120

Quoted 6 lines
contrib/credential/osxkeychain/git-credential-osxkeychain.c +5 -0
+       username_buf = (char *)CFStringGetCStringPtr(account_ref, ENCODING);
+       if (username_buf)
+       {
+               write_item("username", username_buf, strlen(username_buf));
                return;
+       }

According to the documentation for CFStringGetCStringPtr(), the returned C-string is not newly-allocated, so the caller does not have to free it. Therefore, can username_buf be declared const char * rather than char * to make it clear to readers that nothing is being leaked here? Same comment about the (char *) cast.

contrib/credential/osxkeychain/git-credential-osxkeychain.c static void die(const char *err, ...) · line 123

Quoted 2 lines
contrib/credential/osxkeychain/git-credential-osxkeychain.c +2 -0
+       /* If we can't get a CString pointer then
+        * we need to allocate our own buffer */

Style:

/*
* Multi-line comments
* are formatted like this.
*/

contrib/credential/osxkeychain/git-credential-osxkeychain.c static void die(const char *err, ...) · line 133

Quoted 10 lines
contrib/credential/osxkeychain/git-credential-osxkeychain.c +10 -0
+       buffer_len = CFStringGetMaximumSizeForEncoding(
+                       CFStringGetLength(account_ref), ENCODING) + 1;
+       username_buf = xmalloc(buffer_len);
+       if (CFStringGetCString(account_ref,
+                               username_buf,
+                               buffer_len,
+                               ENCODING)) {
+               write_item("username", username_buf, buffer_len - 1);
+       }
+       free(username_buf);

Okay, this explains why username_buf is declared char * rather than const char *. Typically, when we have a situation in which a value may or may not need freeing, we use a to_free variable like this:

    const char *username_buf;
    char *to_free = NULL;
    ...
    username_buf = (const char *)CFStringGetCStringPtr(...);
    if (username_buf) {
        ...
        return;
    }
    ...
    username_buf = to_free = xmalloc(buffer_len);
    if (CFStringGetCString(...))
        ...
    free(to_free);

But that may be overkill for this simple case, and what you have here may be "good enough" for anyone already familiar with the API and who knows that the return after CFStringGetCStringPtr() isn't leaking.

contrib/credential/osxkeychain/git-credential-osxkeychain.c static void die(const char *err, ...) · line 150

Quoted 13 lines
contrib/credential/osxkeychain/git-credential-osxkeychain.c +11 -0
+static OSStatus find_internet_password(void)
 {
+       CFDictionaryRef attrs;
+       [...]
 
+       attrs = CREATE_SEC_ATTRIBUTES(kSecMatchLimit, kSecMatchLimitOne,
+                                     kSecReturnAttributes, kCFBooleanTrue,
+                                     kSecReturnData, kCFBooleanTrue,
+                                     NULL);
+       result = SecItemCopyMatching(attrs, (CFTypeRef *)&item);
+       if (result) {
+               goto out;
+       }

We omit braces when the body is a single statement:

    if (result)
        goto out;

(Same comment applies to other code in this patch.)

contrib/credential/osxkeychain/git-credential-osxkeychain.c static void die(const char *err, ...) · line 163

Quoted 5 lines
contrib/credential/osxkeychain/git-credential-osxkeychain.c +5 -0
+       data = CFDictionaryGetValue(item, kSecValueData);
+       [...]
+
+out:
+       CFRelease(attrs);

Good, attrs is released in all cases.

Quoted 5 lines
contrib/credential/osxkeychain/git-credential-osxkeychain.c +4 -0
+static OSStatus add_internet_password(void)
 {
+       [...]
+       attrs = CREATE_SEC_ATTRIBUTES(kSecValueData, password,
+                                     NULL);

contrib/credential/osxkeychain/git-credential-osxkeychain.c static void delete_internet_password(void) · line 219

Quoted 10 lines
contrib/credential/osxkeychain/git-credential-osxkeychain.c +9 -0
+       result = SecItemAdd(attrs, NULL);
+       if (result == errSecDuplicateItem) {
+               CFDictionaryRef query;
+               query = CREATE_SEC_ATTRIBUTES(NULL);
+               result = SecItemUpdate(query, attrs);
+               CFRelease(query);
+       }
+       CFRelease(attrs);
+       return result;
 }

Good, attrs and query are released in all cases.

#8 lore raw

Re: [PATCH 4/4] osxkeychain: store new attributes

In reply to Bo Anderson via GitGitGadget
Quoted 19 lines · Bo Anderson via GitGitGadget

d208bfdfef (credential: new attribute password_expiry_utc, 2023-02-18) and a5c76569e7 (credential: new attribute oauth_refresh_token, 2023-04-21) introduced new credential attributes but support was missing from git-credential-osxkeychain.

---

contrib/credential/osxkeychain/git-credential-osxkeychain.c +5 -1
@@ -6,10 +6,12 @@
 static CFStringRef host;
+static CFNumberRef port;
 static CFStringRef path;
-static CFNumberRef port;
@@ -17,6 +19,10 @@ static void clear_credential(void)
+       if (port) {
+               CFRelease(port);
+               port = NULL;
+       }

contrib/credential/osxkeychain/git-credential-osxkeychain.c static void clear_credential(void) · line 38

Quoted 7 lines
contrib/credential/osxkeychain/git-credential-osxkeychain.c +4 -3
@@ -29,12 +35,18 @@ static void clear_credential(void)
-       if (port) {
-               CFRelease(port);
-               port = NULL;
+       if (password_expiry_utc) {
+               CFRelease(password_expiry_utc);
+               password_expiry_utc = NULL;
+       }

The relocation of port is unrelated to the stated purpose of this patch. We would normally avoid this sort of "noise" change since it obscures the "real" changes made by the patch, and would instead place it in its own patch. That said, it's such a minor issue, I doubt that it's worth a reroll.

#9 lore raw
In reply to Bo Anderson via GitGitGadget
Quoted 8 lines · Bo Anderson via GitGitGadget

git-credential-osxkeychain has largely fallen behind other external credential helpers in the features it supports, and hasn't received any functional changes since 2013. [...]

osxkeychain also made use of macOS APIs that had been deprecated since 2014. Replacement API was able to be used without regressing the minimum supported macOS established in 5747c8072b (contrib/credential: avoid fixed-size buffer in osxkeychain, 2023-05-01).

Although I'm not familiar with the SecItem API nor with the Git keychain API, I gave this series a readthrough and left a few minor comments. Aside from a few very minor style nits, perhaps the only substantive comment was that patch [1/4] could do a slightly better job of protecting against future programmer error, but even that is minor in that it doesn't impact the functionality actually implemented by the patch, thus may not be worth a reroll.

Overall, despite not being familiar with the APIs in question, everything I read in the patches made sense and was cleanly implemented. Nicely done.

#3 lore raw

Re: [PATCH 1/4] osxkeychain: replace deprecated SecKeychain API

In reply to Eric Sunshine
Quoted 4 lines · Eric Sunshine

I haven't studied the SecItem API, so I can't comment on the meat of the patch, but I can make a few generic observations...

Thanks for taking a look!

Quoted 16 lines
Quoted 16 lines
contrib/credential/osxkeychain/git-credential-osxkeychain.c +12 -1
@@ -3,14 +3,39 @@
-__attribute__((format (printf, 1, 2)))
+#define ENCODING kCFStringEncodingUTF8
+static CFStringRef protocol; /* Stores constant strings - not memory managed */
+static CFStringRef host;
[...]
+
+static void clear_credential(void)
+{
+       if (host) {
+               CFRelease(host);
+               host = NULL;
+       }
+       [...]
+}
Quoted 8 lines

contrib/credential/osxkeychain/git-credential-osxkeychain.c

Quoted 2 lines
contrib/credential/osxkeychain/git-credential-osxkeychain.c +2 -0
+
+__attribute__((format (printf, 1, 2), __noreturn__))

The addition of __noreturn__ to the __attribute__ seems unrelated to the stated purpose of this patch. As such, it typically would be placed in its own patch. If it really is too minor for a separate patch, mentioning it in the commit message as a "While at it..." would be helpful.

Acknowledged. It is indeed a bit of a nothing change that doesn’t really do much on its own, but when paired with the port variable reorder could potentially make a “minor code cleanup” commit.

Quoted 36 lines

contrib/credential/osxkeychain/git-credential-osxkeychain.c static void die(const char *err, ...) · line 78

Quoted 8 lines
contrib/credential/osxkeychain/git-credential-osxkeychain.c +8 -0
+       va_start(args, allocator);
+       while ((key = va_arg(args, const void *)) != NULL) {
+               const void *value;
+               value = va_arg(args, const void *);
+               if (value)
+                       CFDictionarySetValue(result, key, value);
+       }
+       va_end(args);

A couple related comments...

If va_arg() ever returns NULL for value, the next iteration of the loop will call va_arg() again, but calling va_arg() again after it has already returned NULL is likely undefined behavior. At minimum, I would have expected this to be written as:

while (...) {
    ...
    if (!value)
        break;
    CFDictionarySetValue(...);
}

However, isn't it a programmer error if va_arg() returns NULL for value? If so, I'd think we'd want to scream loudly about that rather than silently ignoring it. So, perhaps something like this:

while (...) {
    ...
    if (!value) {
        fprintf(stderr, "BUG: ...");
        abort();
    }
    CFDictionarySetValue(...);
}
Quoted 3 lines

Or, perhaps just call the existing die() function in this file with a suitable "BUG ..." message.

In this case it’s by design to accept and check for NULL values as it greatly simplifies the code. Inputs to the credential helpers have various optional fields, such as port and path. It is programmer error to pass NULL to the SecItem API (runtime crash) so in order to simplify having to check each individual field in all of the callers (and probably ditch varargs since you can’t really do dynamic varargs), I check the value here instead. That means you can do something like:

create_dictionary(kCFAllocatorDefault,

     kSecAttrServer, host,
     kSecAttrPath, path, \
     kSecAttrPort, port,
     NULL)

And it will only include the key-value pairs that have non-NULL values.

It would indeed be programmer error to not pass key-value pairs, though it is equally programmer error to not have a terminating NULL.

Quoted 51 lines

contrib/credential/osxkeychain/git-credential-osxkeychain.c static void die(const char *err, ...) · line 120

Quoted 6 lines
contrib/credential/osxkeychain/git-credential-osxkeychain.c +5 -0
+       username_buf = (char *)CFStringGetCStringPtr(account_ref, ENCODING);
+       if (username_buf)
+       {
+               write_item("username", username_buf, strlen(username_buf));
            return;
+       }

According to the documentation for CFStringGetCStringPtr(), the returned C-string is not newly-allocated, so the caller does not have to free it. Therefore, can username_buf be declared const char * rather than char * to make it clear to readers that nothing is being leaked here? Same comment about the (char *) cast.

contrib/credential/osxkeychain/git-credential-osxkeychain.c static void die(const char *err, ...) · line 123

Quoted 2 lines
contrib/credential/osxkeychain/git-credential-osxkeychain.c +2 -0
+       /* If we can't get a CString pointer then
+        * we need to allocate our own buffer */

Style:

/*
* Multi-line comments
* are formatted like this.
*/

contrib/credential/osxkeychain/git-credential-osxkeychain.c static void die(const char *err, ...) · line 133

Quoted 10 lines
contrib/credential/osxkeychain/git-credential-osxkeychain.c +10 -0
+       buffer_len = CFStringGetMaximumSizeForEncoding(
+                       CFStringGetLength(account_ref), ENCODING) + 1;
+       username_buf = xmalloc(buffer_len);
+       if (CFStringGetCString(account_ref,
+                               username_buf,
+                               buffer_len,
+                               ENCODING)) {
+               write_item("username", username_buf, buffer_len - 1);
+       }
+       free(username_buf);

Okay, this explains why username_buf is declared char * rather than const char *. Typically, when we have a situation in which a value may or may not need freeing, we use a to_free variable like this:

const char *username_buf;
char *to_free = NULL;
...
username_buf = (const char *)CFStringGetCStringPtr(...);

if (username_buf) {
    ...
    return;
}
...
username_buf = to_free = xmalloc(buffer_len);
if (CFStringGetCString(...))
    ...
Quoted 5 lines

free(to_free);

But that may be overkill for this simple case, and what you have here may be "good enough" for anyone already familiar with the API and who knows that the return after CFStringGetCStringPtr() isn't leaking.

Would it make sense to just have a comment paired with the CFStringGetCStringPtr return explaining why it doesn’t need to be freed there? I’m OK with the to_free variable however if that’s clearer. Idea in my mind was pairing it based on xmalloc but I can see why pairing based on variable is clearer.

#4 lore raw

Re: [PATCH 1/4] osxkeychain: replace deprecated SecKeychain API

Quoted 29 lines · Bo Anderson
Quoted 10 lines · Eric Sunshine

contrib/credential/osxkeychain/git-credential-osxkeychain.c static void die(const char *err, ...) · line 78

Quoted 8 lines
contrib/credential/osxkeychain/git-credential-osxkeychain.c +8 -0
+    va_start(args, allocator);
+    while ((key = va_arg(args, const void *)) != NULL) {
+        const void *value;
+        value = va_arg(args, const void *);
+        if (value)
+            CFDictionarySetValue(result, key, value);
+    }
+    va_end(args);
Quoted 3 lines

However, isn't it a programmer error if va_arg() returns NULL for value? If so, I'd think we'd want to scream loudly about that rather than silently ignoring it. So, perhaps something like this: [...]

In this case it’s by design to accept and check for NULL values as it greatly simplifies the code. Inputs to the credential helpers have various optional fields, such as port and path. It is programmer error to pass NULL to the SecItem API (runtime crash) so in order to simplify having to check each individual field in all of the callers (and probably ditch varargs since you can’t really do dynamic varargs), I check the value here instead. That means you can do something like:

create_dictionary(kCFAllocatorDefault,
kSecAttrServer, host,
kSecAttrPath, path, \
kSecAttrPort, port,
NULL)

Quoted 6 lines

And it will only include the key-value pairs that have non-NULL values.

It would indeed be programmer error to not pass key-value pairs, though it is equally programmer error to not have a terminating NULL.

Okay. I had thought that this check was merely protecting against programmer error, but the described use-case to avoid passing NULL to SecItem API makes perfect sense. It might be helpful to future readers to explain this either as a function-level comment (explaining how to call the function) or as an in-code comment.

Quoted 33 lines
Quoted 24 lines

contrib/credential/osxkeychain/git-credential-osxkeychain.c static void die(const char *err, ...) · line 120

Quoted 6 lines
contrib/credential/osxkeychain/git-credential-osxkeychain.c +5 -0
+    username_buf = (char *)CFStringGetCStringPtr(account_ref, ENCODING);
+    if (username_buf)
+    {
+        write_item("username", username_buf, strlen(username_buf));
      return;
+    }

According to the documentation for CFStringGetCStringPtr(), the returned C-string is not newly-allocated, so the caller does not have to free it. Therefore, can username_buf be declared const char * rather than char * to make it clear to readers that nothing is being leaked here? Same comment about the (char *) cast.

contrib/credential/osxkeychain/git-credential-osxkeychain.c static void die(const char *err, ...) · line 133

Quoted 10 lines
contrib/credential/osxkeychain/git-credential-osxkeychain.c +10 -0
+    buffer_len = CFStringGetMaximumSizeForEncoding(
+            CFStringGetLength(account_ref), ENCODING) + 1;
+    username_buf = xmalloc(buffer_len);
+    if (CFStringGetCString(account_ref,
+                username_buf,
+                buffer_len,
+                ENCODING)) {
+        write_item("username", username_buf, buffer_len - 1);
+    }
+    free(username_buf);
Quoted 8 lines

Okay, this explains why username_buf is declared char * rather than const char *. Typically, when we have a situation in which a value may or may not need freeing, we use a to_free variable like
this: [...]

But that may be overkill for this simple case, and what you have here may be "good enough" for anyone already familiar with the API and who knows that the return after CFStringGetCStringPtr() isn't leaking.

Quoted 5 lines

Would it make sense to just have a comment paired with the CFStringGetCStringPtr return explaining why it doesn’t need to be freed there? I’m OK with the to_free variable however if that’s clearer. Idea in my mind was pairing it based on xmalloc but I can see why pairing based on variable is clearer.

Most likely, anyone working on this code is already familiar with the CoreFoundation API, thus would understand implicitly that this isn't leaking. But, yes, a simple comment should be plenty sufficient for everyone else if you are re-rolling anyhow.

#10 lore raw
In reply to Bo Anderson via GitGitGadget
Quoted 13 lines

git-credential-osxkeychain has largely fallen behind other external credential helpers in the features it supports, and hasn't received any functional changes since 2013. As it stood, osxkeychain failed seven tests in the external credential helper test suite:

not ok 8 - helper (osxkeychain) overwrites on store
not ok 9 - helper (osxkeychain) can forget host
not ok 11 - helper (osxkeychain) does not erase a password distinct from input not ok 15 - helper (osxkeychain) erases all matching credentials not ok 18 - helper (osxkeychain) gets password_expiry_utc
not ok 19 - helper (osxkeychain) overwrites when password_expiry_utc changes not ok 21 - helper (osxkeychain) gets oauth_refresh_token

Quoted 1 line

After this set of patches, osxkeychain passes all tests in the external

credential helper test suite.

Great work!

Could these tests run as part of macOS CI?

#11 lore raw
Quoted 15 lines · M Hickford
Quoted 14 lines

git-credential-osxkeychain has largely fallen behind other external credential helpers in the features it supports, and hasn't received any functional changes since 2013. As it stood, osxkeychain failed seven tests in the external credential helper test suite:

not ok 8 - helper (osxkeychain) overwrites on store
not ok 9 - helper (osxkeychain) can forget host
not ok 11 - helper (osxkeychain) does not erase a password distinct from input not ok 15 - helper (osxkeychain) erases all matching credentials not ok 18 - helper (osxkeychain) gets password_expiry_utc
not ok 19 - helper (osxkeychain) overwrites when password_expiry_utc changes not ok 21 - helper (osxkeychain) gets oauth_refresh_token

Quoted 6 lines
Quoted 1 line

After this set of patches, osxkeychain passes all tests in the external

credential helper test suite.

Great work!

Could these tests run as part of macOS CI?

Do we do so for any of the other external credential helpers?

It definitely makes sense in principle. Though the concern perhaps will be that any new features added to the credential helpers and thus its test suite would need adding to each credential helper simultaneously to avoid failing CI. Ideally we would do exactly that, though that requires knowledge on each of the keystore APIs used in each of the credential helpers.

#12 lore raw
Quoted 8 lines · Bo Anderson
Quoted 5 lines · M Hickford

Could these tests run as part of macOS CI?

Do we do so for any of the other external credential helpers?

We don't.

Quoted 1 line

It definitely makes sense in principle. Though the concern perhaps will be that any new features added to the credential helpers and thus its test suite would need adding to each credential helper simultaneously to avoid failing CI. Ideally we would do exactly that, though that requires knowledge on each of the keystore APIs used in each of the credential helpers.

Good point.

#13 lore raw
Quoted 8 lines · M Hickford
Quoted 6 lines

It definitely makes sense in principle. Though the concern perhaps will be that any new features added to the credential helpers and thus its test suite would need adding to each credential helper simultaneously to avoid failing CI. Ideally we would do exactly that, though that requires knowledge on each of the keystore APIs used in each of the credential helpers.

Good point.

I think we suffer from that somewhat already. You cannot run t0303 successfully against credential-store anymore, as of 0ce02e2fec (credential/libsecret: store new attributes, 2023-06-16).

There is some prior art in the GIT_TEST_CREDENTIAL_HELPER_TIMEOUT variable, as time is not a concept to every helper (like store, for example). Other new tests like the password-expiry and oauth features could be gated on similar variables. That would help non-CI users testing helpers manually, and then CI jobs could set the appropriate switches for each helper that they cover.

All that said, I'd be surprised if testing osxkeychain in the CI environment worked. Back when I worked on it in 2011, I found that I had to actually run the tests in a local terminal; even a remote ssh login could not access the keychain. It's possible that things have changed since then, though, or perhaps I was imply ignorant of how to configure things correctly.

-Peff

#17 lore raw
In reply to Bo Anderson via GitGitGadget
Quoted 40 lines

From: "Bo Anderson via GitGitGadget" <[email protected]>
To: [email protected]
Cc: Bo Anderson <[email protected]>
Subject: [PATCH 0/4] osxkeychain: bring in line with other credential helpers
Date: Sat, 17 Feb 2024 23:34:52 +0000 [thread overview]
Message-ID: <[email protected]> (raw)

git-credential-osxkeychain has largely fallen behind other external credential helpers in the features it supports, and hasn't received any functional changes since 2013. As it stood, osxkeychain failed seven tests in the external credential helper test suite:

not ok 8 - helper (osxkeychain) overwrites on store
not ok 9 - helper (osxkeychain) can forget host
not ok 11 - helper (osxkeychain) does not erase a password distinct from input not ok 15 - helper (osxkeychain) erases all matching credentials not ok 18 - helper (osxkeychain) gets password_expiry_utc
not ok 19 - helper (osxkeychain) overwrites when password_expiry_utc changes not ok 21 - helper (osxkeychain) gets oauth_refresh_token

osxkeychain also made use of macOS APIs that had been deprecated since 2014. Replacement API was able to be used without regressing the minimum supported macOS established in 5747c8072b (contrib/credential: avoid fixed-size buffer in osxkeychain, 2023-05-01).

After this set of patches, osxkeychain passes all tests in the external credential helper test suite.

Bo Anderson (4):
osxkeychain: replace deprecated SecKeychain API
osxkeychain: erase all matching credentials
osxkeychain: erase matching passwords only
osxkeychain: store new attributes

 contrib/credential/osxkeychain/Makefile       |   3 +-
 .../osxkeychain/git-credential-osxkeychain.c  | 376 ++++++++++++++----
 2 files changed, 310 insertions(+), 69 deletions(-)
Quoted 6 lines

base-commit: 3e0d3cd5c7def4808247caf168e17f2bbf47892b
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1667%2FBo98%2Fosxkeychain-update-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1667/Bo98/osxkeychain-update-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/1667

Signature · 1 line
gitgitgadget

Hi. Is this patch ready to cook in seen?

#18 lore raw
Quoted 9 lines · M Hickford
Quoted 8 lines

From: "Bo Anderson via GitGitGadget" <[email protected]>
To: [email protected]
Cc: Bo Anderson <[email protected]>
Subject: [PATCH 0/4] osxkeychain: bring in line with other credential helpers
Date: Sat, 17 Feb 2024 23:34:52 +0000 [thread overview]
Message-ID: <[email protected]> (raw)
...
git-credential-osxkeychain has largely fallen behind other external

Hi. Is this patch ready to cook in seen?

A better nudge would be for somebody who uses macOS to resend the patches with their "Tested-by:" trailers added ;-).

Thanks.

#14 lore raw
In reply to Jeff King

Hi all,

Quoted 6 lines

All that said, I'd be surprised if testing osxkeychain in the CI environment worked. Back when I worked on it in 2011, I found that I had to actually run the tests in a local terminal; even a remote ssh login could not access the keychain. It's possible that things have changed since then, though, or perhaps I was imply ignorant of how to configure things correctly.

I have gotten keychain working in Github Actions before: there's some helpers for it, but you can also basically do it manually via the steps from [1]. Basically anyone who needs to do Apple code-signing in CI has to make it work.

@Bo, how are you actually testing this manually? Following these steps:

$ make
$ (cd contrib/credential/osxkeychain && make)
$ ln -s contrib/credential/osxkeychain/git-credential-osxkeychain .
$ cd t
$ make GIT_TEST_CREDENTIAL_HELPER=osxkeychain t0303-credential-external.sh

I get 'A keychain cannot be found to store "store-user".' in a popup dialog when #2 runs; then similar for other tests in 0303. For #14 I get a slight alternative with "A keychain cannot be found". There's a "Reset To Defaults" button, but that wipes everything. AFAIK I have a relatively normal setup, with a login keychain as default. macOS 14.3.1; arm64.

$ security list-keychains
    "/Users/rc/Library/Keychains/login.keychain-db"
    "/Library/Keychains/System.keychain"
$ security default-keychain
    "/Users/rc/Library/Keychains/login.keychain-db"
$ security unlock-keychain
password to unlock default: ...

I don't see any settings or code for setting which keychain the credential helper uses, so I guess it's the default one?

Cheers,

Rob :)

[1] https://docs.github.com/en/actions/deployment/deploying-xcode-applications/installing-an-apple-certificate-on-macos-runners-for-xcode-development

#15 lore raw

The test script does not interact well with the env filtering. This was the case before this change too.

To interact with your default keychain, you will need:

GIT_TEST_CREDENTIAL_HELPER_SETUP="export HOME=$HOME”

This is because the default macOS user keychain is local to your home directory - that’s why it’s giving errors about not finding any.

Bo

Quoted 47 lines · Robert Coup

Hi all,

Quoted 6 lines

All that said, I'd be surprised if testing osxkeychain in the CI environment worked. Back when I worked on it in 2011, I found that I had to actually run the tests in a local terminal; even a remote ssh login could not access the keychain. It's possible that things have changed since then, though, or perhaps I was imply ignorant of how to configure things correctly.

I have gotten keychain working in Github Actions before: there's some helpers for it, but you can also basically do it manually via the steps from [1]. Basically anyone who needs to do Apple code-signing in CI has to make it work.

@Bo, how are you actually testing this manually? Following these steps:

$ make
$ (cd contrib/credential/osxkeychain && make)
$ ln -s contrib/credential/osxkeychain/git-credential-osxkeychain .
$ cd t
$ make GIT_TEST_CREDENTIAL_HELPER=osxkeychain t0303-credential-external.sh

I get 'A keychain cannot be found to store "store-user".' in a popup dialog when #2 runs; then similar for other tests in 0303. For #14 I get a slight alternative with "A keychain cannot be found". There's a "Reset To Defaults" button, but that wipes everything. AFAIK I have a relatively normal setup, with a login keychain as default. macOS 14.3.1; arm64.

$ security list-keychains
   "/Users/rc/Library/Keychains/login.keychain-db"
   "/Library/Keychains/System.keychain"
$ security default-keychain
   "/Users/rc/Library/Keychains/login.keychain-db"
$ security unlock-keychain
password to unlock default: ...

I don't see any settings or code for setting which keychain the credential helper uses, so I guess it's the default one?

Cheers,

Rob :)

[1] https://docs.github.com/en/actions/deployment/deploying-xcode-applications/installing-an-apple-certificate-on-macos-runners-for-xcode-development

Tested-by
#16 lore raw

Hi Bo,

Quoted 2 lines · Bo Anderson

The test script does not interact well with the env filtering. This was the case before this change too.

I guess without writing a helper-specific test or having some per-helper-setup thing it's a bit tricky.

Quoted 5 lines

To interact with your default keychain, you will need:

GIT_TEST_CREDENTIAL_HELPER_SETUP="export HOME=$HOME”

This is because the default macOS user keychain is local to your home directory - that’s why it’s giving errors about not finding any.

And with that, the tests all pass :-) Comparing with master where 7/21 failed.

Could we document that setup step somewhere? I guess the simplest is probably just to put it in the header of t/t0303-credential-external.sh; maybe along the lines of the patch below.

Rob :)

t/t0303-credential-external.sh +7 -0
index 095574bfc6..e4e693b233 100755
@@ -27,6 +27,13 @@ timeout, you can test that feature with:
 If your helper requires additional setup before the tests are started,
 you can set GIT_TEST_CREDENTIAL_HELPER_SETUP to a sequence of shell
 commands.
+
+- osxkeychain:
+
+  Because the default macOS user keychain is local to your home
+  directory, you will need:
+
+    GIT_TEST_CREDENTIAL_HELPER_SETUP="export HOME=$HOME”
 '
 
 . ./test-lib.sh