diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml
index d539874de0a982e3d650d4d74c8d88251a07105c..ba100da3888e4d506badeca4dffa0501b07d96c7 100644
--- a/.github/workflows/checks.yaml
+++ b/.github/workflows/checks.yaml
@@ -31,3 +31,15 @@ jobs:
         java-version: '17'
     - name: Run Gradle tests
       run: ./gradlew check --info --stacktrace
+  build-macos:
+    runs-on: macos-latest
+    steps:
+    - name: Checkout repository
+      uses: actions/checkout@v3
+    - name: Setup Java
+      uses: actions/setup-java@v3
+      with:
+        distribution: 'temurin'
+        java-version: '17'
+    - name: Run Gradle tests
+      run: ./gradlew check --info --stacktrace
diff --git a/onionwrapper-core/src/main/java/org/briarproject/onionwrapper/AbstractTorWrapper.java b/onionwrapper-core/src/main/java/org/briarproject/onionwrapper/AbstractTorWrapper.java
index b2dbb22f555fa8efd1a887de6c873d59fc7d3d07..99733387dcbe00c9509fa0396df2cc30f53bdc69 100644
--- a/onionwrapper-core/src/main/java/org/briarproject/onionwrapper/AbstractTorWrapper.java
+++ b/onionwrapper-core/src/main/java/org/briarproject/onionwrapper/AbstractTorWrapper.java
@@ -50,6 +50,7 @@ import static org.briarproject.onionwrapper.TorWrapper.TorState.STARTED;
 import static org.briarproject.onionwrapper.TorWrapper.TorState.STARTING;
 import static org.briarproject.onionwrapper.TorWrapper.TorState.STOPPED;
 import static org.briarproject.onionwrapper.TorWrapper.TorState.STOPPING;
+import static org.briarproject.onionwrapper.util.OsUtils.isMac;
 
 @InterfaceNotNullByDefault
 abstract class AbstractTorWrapper implements EventHandler, TorWrapper {
@@ -110,6 +111,10 @@ abstract class AbstractTorWrapper implements EventHandler, TorWrapper {
 		return new File(torDirectory, "tor");
 	}
 
+	protected File getLibEventFile() {
+		return new File(torDirectory, "libevent-2.1.7.dylib");
+	}
+
 	@Override
 	public File getObfs4ExecutableFile() {
 		return new File(torDirectory, "obfs4proxy");
@@ -202,6 +207,9 @@ abstract class AbstractTorWrapper implements EventHandler, TorWrapper {
 		//noinspection ResultOfMethodCallIgnored
 		doneFile.delete();
 		installTorExecutable();
+		if (isMac()) {
+			installLibEvent();
+		}
 		installObfs4Executable();
 		installSnowflakeExecutable();
 		extract(getConfigInputStream(), configFile);
@@ -225,6 +233,14 @@ abstract class AbstractTorWrapper implements EventHandler, TorWrapper {
 		if (!torFile.setExecutable(true, true)) throw new IOException();
 	}
 
+	protected void installLibEvent() throws IOException {
+		if (LOG.isLoggable(INFO)) {
+			LOG.info("Installing libevent binary for " + architecture);
+		}
+		File libEventFile = getLibEventFile();
+		extract(getExecutableInputStream("libevent-2.1.7.dylib"), libEventFile);
+	}
+
 	protected void installObfs4Executable() throws IOException {
 		if (LOG.isLoggable(INFO)) {
 			LOG.info("Installing obfs4proxy binary for " + architecture);
diff --git a/onionwrapper-core/src/main/java/org/briarproject/onionwrapper/util/OsUtils.java b/onionwrapper-core/src/main/java/org/briarproject/onionwrapper/util/OsUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..67373dd98d64c998410f3a10ff1ca3aa8b27bfb6
--- /dev/null
+++ b/onionwrapper-core/src/main/java/org/briarproject/onionwrapper/util/OsUtils.java
@@ -0,0 +1,32 @@
+package org.briarproject.onionwrapper.util;
+
+import org.briarproject.nullsafety.NotNullByDefault;
+
+import javax.annotation.Nullable;
+
+import static org.briarproject.onionwrapper.util.StringUtils.startsWithIgnoreCase;
+
+@NotNullByDefault
+public class OsUtils {
+
+	@Nullable
+	private static final String os = System.getProperty("os.name");
+	@Nullable
+	private static final String vendor = System.getProperty("java.vendor");
+
+	public static boolean isWindows() {
+		return os != null && startsWithIgnoreCase(os, "Win");
+	}
+
+	public static boolean isMac() {
+		return os != null && os.equalsIgnoreCase("Mac OS X");
+	}
+
+	public static boolean isLinux() {
+		return os != null && startsWithIgnoreCase(os, "Linux") && !isAndroid();
+	}
+
+	public static boolean isAndroid() {
+		return vendor != null && vendor.contains("Android");
+	}
+}
diff --git a/onionwrapper-core/src/main/java/org/briarproject/onionwrapper/util/StringUtils.java b/onionwrapper-core/src/main/java/org/briarproject/onionwrapper/util/StringUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..8964d41ac5651579eeb80de976ca5d7cd4a0b99d
--- /dev/null
+++ b/onionwrapper-core/src/main/java/org/briarproject/onionwrapper/util/StringUtils.java
@@ -0,0 +1,12 @@
+package org.briarproject.onionwrapper.util;
+
+import org.briarproject.nullsafety.NotNullByDefault;
+
+@NotNullByDefault
+public class StringUtils {
+
+	// see https://stackoverflow.com/a/38947571
+	static boolean startsWithIgnoreCase(String s, String prefix) {
+		return s.regionMatches(true, 0, prefix, 0, prefix.length());
+	}
+}
diff --git a/onionwrapper-core/src/test/java/org/briarproject/onionwrapper/TestUtils.java b/onionwrapper-core/src/test/java/org/briarproject/onionwrapper/TestUtils.java
index 2c6b65e1e62a466edef7e5223ce8691d7983f1a5..82ad9bd63cb4b0ac45b8635aa52e0ddbf1f38611 100644
--- a/onionwrapper-core/src/test/java/org/briarproject/onionwrapper/TestUtils.java
+++ b/onionwrapper-core/src/test/java/org/briarproject/onionwrapper/TestUtils.java
@@ -12,7 +12,9 @@ import javax.annotation.concurrent.ThreadSafe;
 import static java.util.Arrays.asList;
 import static java.util.logging.Level.WARNING;
 import static java.util.logging.Logger.getLogger;
-import static org.briarproject.onionwrapper.StringUtils.startsWithIgnoreCase;
+import static org.briarproject.onionwrapper.util.OsUtils.isLinux;
+import static org.briarproject.onionwrapper.util.OsUtils.isMac;
+import static org.briarproject.onionwrapper.util.OsUtils.isWindows;
 
 @ThreadSafe
 @NotNullByDefault
@@ -54,29 +56,18 @@ public class TestUtils {
 		}
 	}
 
-	public static boolean isLinux() {
-		String os = System.getProperty("os.name");
-		return os != null && os.contains("Linux");
-	}
-
-	public static boolean isWindows() {
-		String os = System.getProperty("os.name");
-		return os != null && startsWithIgnoreCase(os, "Win");
-	}
-
-	public static boolean isMac() {
-		String os = System.getProperty("os.name");
-		return os != null && os.equalsIgnoreCase("Mac OS X");
-	}
-
 	@Nullable
 	public static String getArchitectureForTorBinary() {
 		String arch = System.getProperty("os.arch");
 		if (arch == null) return null;
-		//noinspection IfCanBeSwitch
-		if (arch.equals("amd64")) return "x86_64";
-		else if (arch.equals("aarch64")) return "aarch64";
-		else if (arch.equals("arm")) return "armhf";
+		if (isLinux() || isWindows()) {
+			//noinspection IfCanBeSwitch
+			if (arch.equals("amd64")) return "x86_64";
+			else if (arch.equals("aarch64")) return "aarch64";
+			else if (arch.equals("arm")) return "armhf";
+		} else if (isMac()) {
+			return "any";
+		}
 		return null;
 	}
 
diff --git a/onionwrapper-java/build.gradle b/onionwrapper-java/build.gradle
index 913f000b25e6e8ecdefdbca470d2a10acf790042..0226df4eb2ec4129cd48c5e5427bb898a87d5498 100644
--- a/onionwrapper-java/build.gradle
+++ b/onionwrapper-java/build.gradle
@@ -1,5 +1,4 @@
-import static org.briarproject.onionwrapper.OS.Linux
-import static org.briarproject.onionwrapper.OS.Windows
+import static org.briarproject.onionwrapper.OS.*
 import static org.briarproject.onionwrapper.OsUtils.currentOS
 
 plugins {
@@ -19,7 +18,7 @@ checkstyle {
 
 dependencies {
     api project(':onionwrapper-core')
-    def jna_version = '4.5.2'
+    def jna_version = '5.10.0'
     implementation "net.java.dev.jna:jna:$jna_version"
     implementation "net.java.dev.jna:jna-platform:$jna_version"
 
@@ -29,6 +28,10 @@ dependencies {
         testImplementation "org.briarproject:tor-$currentOS.id:0.4.7.13-2"
         testImplementation "org.briarproject:obfs4proxy-$currentOS.id:0.0.14-tor2"
         testImplementation "org.briarproject:snowflake-$currentOS.id:2.5.1"
+    } else if (currentOS == MacOS) {
+        testImplementation "org.briarproject:tor-macos-torbrowser:0.4.7.13"
+        testImplementation "org.briarproject:obfs4proxy-macos-torbrowser:0.0.14"
+        testImplementation "org.briarproject:snowflake-macos-torbrowser:2.5.1"
     }
 }
 
diff --git a/onionwrapper-java/src/main/java/org/briarproject/onionwrapper/UnixTorWrapper.java b/onionwrapper-java/src/main/java/org/briarproject/onionwrapper/UnixTorWrapper.java
index 3d196f98b70d07ae31bb81628d6644121fceea8d..eaea8158d4a93892141cf84186f37ae788d2a190 100644
--- a/onionwrapper-java/src/main/java/org/briarproject/onionwrapper/UnixTorWrapper.java
+++ b/onionwrapper-java/src/main/java/org/briarproject/onionwrapper/UnixTorWrapper.java
@@ -46,7 +46,7 @@ public class UnixTorWrapper extends JavaTorWrapper {
 
 	private interface CLibrary extends Library {
 
-		CLibrary INSTANCE = Native.loadLibrary("c", CLibrary.class);
+		CLibrary INSTANCE = Native.load("c", CLibrary.class);
 
 		int getpid();
 	}
diff --git a/onionwrapper-java/src/test/java/org/briarproject/onionwrapper/BootstrapTest.java b/onionwrapper-java/src/test/java/org/briarproject/onionwrapper/BootstrapTest.java
index 63d56d87dd83fa4d5a5c0f2a7490d6986ce97625..e8d9cc6ee1334841e7349396ce91f5b677a7b34a 100644
--- a/onionwrapper-java/src/test/java/org/briarproject/onionwrapper/BootstrapTest.java
+++ b/onionwrapper-java/src/test/java/org/briarproject/onionwrapper/BootstrapTest.java
@@ -15,11 +15,12 @@ import static org.briarproject.nullsafety.NullSafety.requireNonNull;
 import static org.briarproject.onionwrapper.TestUtils.deleteTestDirectory;
 import static org.briarproject.onionwrapper.TestUtils.getArchitectureForTorBinary;
 import static org.briarproject.onionwrapper.TestUtils.getTestDirectory;
-import static org.briarproject.onionwrapper.TestUtils.isLinux;
-import static org.briarproject.onionwrapper.TestUtils.isWindows;
 import static org.briarproject.onionwrapper.TorWrapper.TorState.CONNECTED;
 import static org.briarproject.onionwrapper.TorWrapper.TorState.STARTED;
 import static org.briarproject.onionwrapper.TorWrapper.TorState.STOPPED;
+import static org.briarproject.onionwrapper.util.OsUtils.isLinux;
+import static org.briarproject.onionwrapper.util.OsUtils.isMac;
+import static org.briarproject.onionwrapper.util.OsUtils.isWindows;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeNotNull;
@@ -38,7 +39,7 @@ public class BootstrapTest extends BaseTest {
 
 	@Before
 	public void setUp() {
-		assumeTrue(isLinux() || isWindows());
+		assumeTrue(isLinux() || isWindows() || isMac());
 		assumeNotNull(getArchitectureForTorBinary());
 	}
 
@@ -52,7 +53,7 @@ public class BootstrapTest extends BaseTest {
 	public void testBootstrapping() throws Exception {
 		String architecture = requireNonNull(getArchitectureForTorBinary());
 		TorWrapper tor;
-		if (isLinux()) {
+		if (isLinux() || isMac()) {
 			tor = new UnixTorWrapper(executor, executor, architecture, torDir,
 					CONTROL_PORT, SOCKS_PORT);
 		} else if (isWindows()) {
diff --git a/onionwrapper-java/src/test/java/org/briarproject/onionwrapper/BridgeTest.java b/onionwrapper-java/src/test/java/org/briarproject/onionwrapper/BridgeTest.java
index 1c822f53e62ae7853ae2860b682deaaa03c99000..5401107e5e8f46c82cc2e9562e0db4bba43bf8b8 100644
--- a/onionwrapper-java/src/test/java/org/briarproject/onionwrapper/BridgeTest.java
+++ b/onionwrapper-java/src/test/java/org/briarproject/onionwrapper/BridgeTest.java
@@ -32,9 +32,9 @@ import static org.briarproject.onionwrapper.CircumventionProvider.BridgeType.VAN
 import static org.briarproject.onionwrapper.TestUtils.deleteTestDirectory;
 import static org.briarproject.onionwrapper.TestUtils.getArchitectureForTorBinary;
 import static org.briarproject.onionwrapper.TestUtils.getTestDirectory;
-import static org.briarproject.onionwrapper.TestUtils.isLinux;
 import static org.briarproject.onionwrapper.TestUtils.isOptionalTestEnabled;
 import static org.briarproject.onionwrapper.TorWrapper.TorState.CONNECTED;
+import static org.briarproject.onionwrapper.util.OsUtils.isLinux;
 import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeNotNull;
 import static org.junit.Assume.assumeTrue;
diff --git a/onionwrapper-java/src/test/java/org/briarproject/onionwrapper/ResourcesLinuxTest.java b/onionwrapper-java/src/test/java/org/briarproject/onionwrapper/ResourcesLinuxTest.java
index 7ebbf50a754d7c948e64041bb33dc630f005f6fb..666f627bd3da974793117229a0feb98de208d195 100644
--- a/onionwrapper-java/src/test/java/org/briarproject/onionwrapper/ResourcesLinuxTest.java
+++ b/onionwrapper-java/src/test/java/org/briarproject/onionwrapper/ResourcesLinuxTest.java
@@ -3,7 +3,7 @@ package org.briarproject.onionwrapper;
 import org.junit.Before;
 import org.junit.Test;
 
-import static org.briarproject.onionwrapper.TestUtils.isLinux;
+import static org.briarproject.onionwrapper.util.OsUtils.isLinux;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assume.assumeTrue;
 
diff --git a/onionwrapper-java/src/test/java/org/briarproject/onionwrapper/ResourcesMacTest.java b/onionwrapper-java/src/test/java/org/briarproject/onionwrapper/ResourcesMacTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..884225b9394cd202862053405bc0da5dd5e035f8
--- /dev/null
+++ b/onionwrapper-java/src/test/java/org/briarproject/onionwrapper/ResourcesMacTest.java
@@ -0,0 +1,42 @@
+package org.briarproject.onionwrapper;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.briarproject.onionwrapper.util.OsUtils.isMac;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assume.assumeTrue;
+
+public class ResourcesMacTest {
+
+	@Before
+	public void setUp() {
+		assumeTrue(isMac());
+	}
+
+	@Test
+	public void testCanLoadTor() {
+		testCanLoadResource("any/tor");
+	}
+
+	@Test
+	public void testCanLoadLibEvent() {
+		testCanLoadResource("any/libevent-2.1.7.dylib");
+	}
+
+	@Test
+	public void testCanLoadObfs4() {
+		testCanLoadResource("any/obfs4proxy");
+	}
+
+	@Test
+	public void testCanLoadSnowflake() {
+		testCanLoadResource("any/snowflake");
+	}
+
+	private void testCanLoadResource(String name) {
+		ClassLoader classLoader =
+				Thread.currentThread().getContextClassLoader();
+		assertNotNull(classLoader.getResourceAsStream(name));
+	}
+}
diff --git a/onionwrapper-java/src/test/java/org/briarproject/onionwrapper/ResourcesWindowsTest.java b/onionwrapper-java/src/test/java/org/briarproject/onionwrapper/ResourcesWindowsTest.java
index 01a013786170089f6af09618edeb3e3c63c38fa6..51c8ddfac2c66035f88679196fec79b9d5439641 100644
--- a/onionwrapper-java/src/test/java/org/briarproject/onionwrapper/ResourcesWindowsTest.java
+++ b/onionwrapper-java/src/test/java/org/briarproject/onionwrapper/ResourcesWindowsTest.java
@@ -3,7 +3,7 @@ package org.briarproject.onionwrapper;
 import org.junit.Before;
 import org.junit.Test;
 
-import static org.briarproject.onionwrapper.TestUtils.isWindows;
+import static org.briarproject.onionwrapper.util.OsUtils.isWindows;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assume.assumeTrue;
 
diff --git a/settings.gradle b/settings.gradle
index 2bb9f2af6e7be08bf5b012a9037f63195f94ac9f..7412b9f092671aad1fc05449707410ebb051f8aa 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -10,6 +10,7 @@ dependencyResolutionManagement {
     repositories {
         google()
         mavenCentral()
+        maven { url "https://mvntmp.mobanisto.de" }
     }
 }
 rootProject.name = "Onion Wrapper"