Skip to content

Instantly share code, notes, and snippets.

@ajorg
Last active February 6, 2026 19:41
Show Gist options
  • Select an option

  • Save ajorg/61ee5f8a0567b487f882a04065d86d56 to your computer and use it in GitHub Desktop.

Select an option

Save ajorg/61ee5f8a0567b487f882a04065d86d56 to your computer and use it in GitHub Desktop.
FIDO2 Security Key Provider for macOS SSH

FIDO2 Security Key Provider for macOS SSH

macOS ships OpenSSH with FIDO2 support but without the middleware library (sk-libfido2.dylib) that talks to hardware keys like YubiKeys. This script builds it from openssh-portable source, statically linked so it has no Homebrew runtime dependencies.

Usage

bash build-sk-libfido2.sh

Build dependencies are installed via Homebrew temporarily and removed afterward. The result is a single file: ~/.ssh/sk-libfido2.dylib

The script also adds SecurityKeyProvider ~/.ssh/sk-libfido2.dylib to your ~/.ssh/config. Re-run at any time to rebuild from the latest source.

Note

SecurityKeyProvider only affects the ssh client. Other tools need the provider specified separately:

ssh-keygen -K -w ~/.ssh/sk-libfido2.dylib
SSH_SK_PROVIDER=~/.ssh/sk-libfido2.dylib ssh-add ...

Acknowledgments

Inspired by BertanT's macOS OpenSSH patcher gist.

Written by Claude (Anthropic, claude-opus-4-6) with Andrew Jorgensen.

#!/bin/bash
# build-sk-libfido2.sh
#
# Builds the OpenSSH FIDO2 security key provider (sk-libfido2.dylib) from
# openssh-portable source for use with macOS's built-in SSH client.
#
# macOS ships OpenSSH with FIDO2 support compiled in, but without the
# middleware library that talks to hardware keys. This script builds it,
# statically linking libfido2/libcbor/libcrypto so there are no Homebrew
# runtime dependencies. Build tools are removed afterward.
#
# Re-run at any time to rebuild from the latest source.
#
# SPDX-License-Identifier: 0BSD
set -euo pipefail
DEST="$HOME/.ssh/sk-libfido2.dylib"
# Track which Homebrew packages are newly installed so we can remove only those.
echo "==> Recording existing Homebrew packages"
brew_before=$(brew list --formula -1 | sort)
echo "==> Installing build dependencies"
brew install libfido2 openssl@3 autoconf automake libtool pkgconf cmake
brew_after=$(brew list --formula -1 | sort)
new_packages=$(comm -13 <(echo "$brew_before") <(echo "$brew_after"))
BREW_PREFIX=$(brew --prefix)
OPENSSL=$(brew --prefix openssl@3)
LIBFIDO2=$(brew --prefix libfido2)
WORKDIR=$(mktemp -d)
trap 'rm -rf "$WORKDIR"' EXIT
# Homebrew's libcbor only has shared libraries. Build a static archive.
echo "==> Building static libcbor"
git clone --depth 1 --quiet https://github.com/pjk/libcbor.git "$WORKDIR/libcbor"
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF \
-S "$WORKDIR/libcbor" -B "$WORKDIR/libcbor/_build" > /dev/null 2>&1
cmake --build "$WORKDIR/libcbor/_build" > /dev/null 2>&1
LIBCBOR_A="$WORKDIR/libcbor/_build/src/libcbor.a"
# --with-security-key-standalone builds only the FIDO2 middleware, not all of openssh.
echo "==> Cloning and building openssh sk-libfido2.dylib"
git clone --depth 1 --quiet https://github.com/openssh/openssh-portable.git "$WORKDIR/openssh"
cd "$WORKDIR/openssh"
autoreconf -i > /dev/null 2>&1
if ! ./configure --with-security-key-standalone \
CFLAGS="-I$OPENSSL/include -I$LIBFIDO2/include" \
LDFLAGS="-L$OPENSSL/lib -L$LIBFIDO2/lib" \
> "$WORKDIR/configure.log" 2>&1; then
tail -20 "$WORKDIR/configure.log"
exit 1
fi
if ! make V=1 sk-libfido2.dylib > "$WORKDIR/build.log" 2>&1; then
tail -20 "$WORKDIR/build.log"
exit 1
fi
# --- Re-link with static archives ---
#
# make linked against Homebrew's shared libraries. We replay the link command
# with three modifications:
# - Replace -lfido2 with static libfido2.a + libcbor.a (libcbor is a
# transitive dep that static linking won't resolve on its own)
# - Replace -lcrypto with static libcrypto.a
# - Add CoreFoundation and IOKit frameworks (needed by libfido2's HID
# transport; the shared libfido2.dylib links them implicitly)
echo "==> Re-linking with static libraries"
# Join backslash-continuation lines, then extract the link command
link_cmd=$(perl -pe 's/\\\n\s*/ /g' "$WORKDIR/build.log" \
| grep -F -- '-o sk-libfido2' | tail -1)
if [ -z "$link_cmd" ]; then
echo "ERROR: Could not extract link command from build log."
exit 1
fi
rm -f sk-libfido2.dylib
# Swap -l flags for static .a paths, drop Homebrew -L paths, add frameworks
link_cmd=$(echo "$link_cmd" | sed \
-e "s|-lfido2|$LIBFIDO2/lib/libfido2.a $LIBCBOR_A|" \
-e "s|-lcrypto|$OPENSSL/lib/libcrypto.a|g" \
-e "s|-L$BREW_PREFIX[^ ]*||g")
link_cmd="$link_cmd -framework CoreFoundation -framework IOKit"
eval "$link_cmd"
# Verify only macOS system libraries remain
echo "==> Library dependencies:"
otool -L sk-libfido2.dylib
static_ok=true
if otool -L sk-libfido2.dylib | grep -q "$BREW_PREFIX"; then
echo "WARNING: Library still has Homebrew runtime dependencies."
static_ok=false
else
echo "OK: No Homebrew runtime dependencies."
fi
# Install
echo "==> Installing to $DEST"
mkdir -p ~/.ssh && chmod 700 ~/.ssh
cp sk-libfido2.dylib "$DEST"
chmod 644 "$DEST"
# Add SecurityKeyProvider to ~/.ssh/config (prepend for global scope)
[ -f ~/.ssh/config ] || touch ~/.ssh/config
chmod 600 ~/.ssh/config
if ! grep -q "^SecurityKeyProvider" ~/.ssh/config; then
echo "==> Adding SecurityKeyProvider to ~/.ssh/config"
{ echo "SecurityKeyProvider $DEST"; echo ""; cat ~/.ssh/config; } > ~/.ssh/config.tmp
mv ~/.ssh/config.tmp ~/.ssh/config
chmod 600 ~/.ssh/config
fi
# Remove build deps that weren't previously installed
if [ "$static_ok" = true ] && [ -n "${new_packages:-}" ]; then
echo "==> Removing build-only dependencies"
# shellcheck disable=SC2086
brew uninstall --ignore-dependencies $new_packages 2>/dev/null || true
fi
echo "==> Done. Installed $DEST"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment