diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 066a4a5bc885e49e4dbc68e4bf6fd4e9d20313e8..e8caf0ac4035be56594cc6401595f9fa4a849f08 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -11,6 +11,7 @@ stages:
 variables:
   TEST_IMAGE: briar/tor-reproducer:${CI_BUILD_REF_NAME}
   RELEASE_IMAGE: briar/tor-reproducer:latest
+  UPSTREAM_IMAGE: briar/tor-upstream-builder
 
 before_script:
   - echo ${DOCKER_HUB_PASS} | docker login -u ${DOCKER_HUB_USER} --password-stdin
@@ -45,6 +46,14 @@ build:
     expire_in: 1 week
     when: always
 
+.base-mac:
+  stage: test
+  artifacts:
+    paths:
+      - output/macos
+    expire_in: 1 week
+    when: always
+
 test_build_android:
   extends: .base-android
   script:
@@ -69,6 +78,15 @@ test_build_windows:
   except:
     - tags
 
+test_build_mac:
+  extends: .base-mac
+  script:
+    - docker build -t ${UPSTREAM_IMAGE} -f upstream/Dockerfile .
+    - docker run --privileged -v `pwd`/output:/opt/tor-reproducer/output ${UPSTREAM_IMAGE} /bin/bash -c "touch /dev/console && su builduser -c \"./build_tor_macos.py && ./verify_tor_macos.py\""
+  allow_failure: true
+  except:
+    - tags
+
 test_tag_android:
   extends: .base-android
   script:
@@ -90,6 +108,13 @@ test_tag_windows:
   only:
     - tags
 
+test_tag_macos:
+  extends: .base-mac
+  script:
+    - docker run --privileged -v `pwd`/output:/opt/tor-reproducer/output ${UPSTREAM_IMAGE} ./verify_tor_macos.py ${CI_BUILD_REF_NAME}
+  only:
+    - tags
+
 release:
   stage: release
   script:
diff --git a/template-macos.pom b/template-macos.pom
new file mode 100644
index 0000000000000000000000000000000000000000..64c8a7e19e477228b88141ddb3893133cc1e5ee8
--- /dev/null
+++ b/template-macos.pom
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project>
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>org.briarproject</groupId>
+  <artifactId>tor-macos</artifactId>
+  <name>tor-macos</name>
+  <version>VERSION</version>
+  <url>https://torproject.org</url>
+  <description>Repo for building Tor for macOS.</description>
+  <licenses>
+   <license>
+     <name>BSD-3-clause</name>
+     <url>https://gitweb.torproject.org/tor.git/tree/LICENSE</url>
+   </license>
+  </licenses>
+  <developers>
+    <developer>
+      <id>dingledine</id>
+      <name>Roger Dingledine</name>
+      <email>arma@mit.edu</email>
+    </developer>
+    <developer>
+      <id>mathewson</id>
+      <name>Nick Mathewson</name>
+      <email>nickm@torproject.org</email>
+    </developer>
+    <developer>
+      <id>torproject</id>
+      <name>Tor Project</name>
+      <email>frontdesk@rt.torproject.org</email>
+    </developer>
+  </developers>
+  <scm>
+    <connection>scm:https://gitweb.torproject.org/tor.git/</connection>
+    <developerConnection>scm:git@gitweb.torproject.org/tor.git</developerConnection>
+    <url>scm:https://gitweb.torproject.org/tor.git</url>
+  </scm>
+</project>
diff --git a/tor-versions.json b/tor-versions.json
index 13804fbe3dfbf431aacee8c0be0a5a6a0b928afb..997338080a6d41594b59ae97bf65db800c97344a 100644
--- a/tor-versions.json
+++ b/tor-versions.json
@@ -29,6 +29,12 @@
       "revision": "21.4.7075529",
       "sha256": "ad7ce5467e18d40050dc51b8e7affc3e635c85bd8c59be62de32352328ed467e"
     },
+    "upstream": {
+      "url": "https://gitlab.torproject.org/tpo/applications/tor-browser-build.git",
+      "commit": "b1b4bf77",
+      "tor-browser": "12.0.6",
+      "libevent": "2.1.7"
+    },
     "timestamp": "201001010000.00"
   },
   "0.4.7.13-1": {
diff --git a/upstream/Dockerfile b/upstream/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..b2b99082979ba79d7ec46e22fd5072979a75505e
--- /dev/null
+++ b/upstream/Dockerfile
@@ -0,0 +1,15 @@
+FROM briar/tor-upstream-reproducer:latest
+
+ENV LANG=C.UTF-8
+ENV DEBIAN_FRONTEND=noninteractive
+
+WORKDIR /opt/tor-reproducer
+
+ADD upstream/build_tor_macos.py ./
+ADD tor-versions.json ./
+ADD utils.py ./
+ADD template-macos.pom ./
+ADD verify_tor_macos.py ./
+ADD verify_tor_utils.py ./
+
+CMD ./build_tor_macos.py
diff --git a/upstream/build_tor_macos.py b/upstream/build_tor_macos.py
new file mode 100755
index 0000000000000000000000000000000000000000..e3f7ceaff09342ba8d6fb9b31cfa9f121d2b5e2c
--- /dev/null
+++ b/upstream/build_tor_macos.py
@@ -0,0 +1,155 @@
+#!/usr/bin/env python3
+import os
+from shutil import copytree, rmtree
+from subprocess import check_call
+import hashlib
+
+import utils
+from utils import get_version, get_build_versions, reset_time, \
+    get_sources_file_name, get_output_dir, get_sha256, pack, create_pom_file
+from pathlib import Path
+import tarfile
+
+PLATFORM = "macos"
+BUILD_DIR = "tor-browser-build"
+
+
+def setup():
+    # get Tor version from command or show usage information
+    version = get_version()
+
+    # get versions of upstream repository and Tor browser for comparison
+    versions = get_build_versions(version)
+    print("Building Tor from upstream repository at commit %s" % versions['upstream']['commit'])
+
+    # remove output from previous build
+    output_dir = get_output_dir(PLATFORM)
+    print(output_dir)
+    os.makedirs(output_dir, exist_ok=True)
+
+    # clone tor-browser-build repo
+    check_call(['git', 'clone', versions['upstream']['url'], BUILD_DIR])
+    check_call(['git', 'checkout', versions['upstream']['commit']], cwd=Path(BUILD_DIR))
+    check_call(['git', 'submodule', 'init'], cwd=Path(BUILD_DIR))
+    check_call(['git', 'submodule', 'update'], cwd=Path(BUILD_DIR))
+
+    # create sources jar before building
+    jar_name = create_sources_jar(versions)
+
+    return versions, jar_name
+
+
+def build():
+    versions, jar_name = setup()
+
+    copytree('pre-out', os.path.join(BUILD_DIR, 'out'))
+    copytree('pre-clones', os.path.join(BUILD_DIR, 'git_clones'))
+
+    build_arch(versions, 'aarch64')
+    build_arch(versions, 'x86_64')
+
+    package_macos(versions, jar_name)
+    return versions
+
+
+def build_arch(versions, arch):
+    target = PLATFORM + '-' + arch
+    libevent_version = versions['upstream']['libevent']
+    # build using rbm
+    check_call(['./rbm/rbm', 'build', 'tor', '--target', 'release', '--target', 'torbrowser-' + target], cwd=Path(BUILD_DIR))
+    # extract tar.gz file
+    arch_dir = Path('output') / PLATFORM / arch
+    arch_dir.mkdir(parents=True, exist_ok=True)
+    tar_file = next(Path(BUILD_DIR).glob('out/tor/tor-*-' + target + '-*.tar.gz'))
+    tar = tarfile.open(tar_file, "r:gz")
+    tar.extractall(path=arch_dir)
+    tar.close()
+    # move contents out of tor/ directory
+    (arch_dir / 'tor').rename(arch_dir / 'tordir')
+    for file in (arch_dir / 'tordir').glob('*'):
+        file.rename(arch_dir / file.name)
+    (arch_dir / 'tordir').rmdir
+    rmtree(arch_dir / 'data')
+    # print hashsums
+    tor_file = arch_dir / 'tor'
+    libevent_file = arch_dir / ('libevent-' + libevent_version + '.dylib')
+    for file in [tor_file, libevent_file]:
+        sha256hash = get_sha256(file)
+        print("%s: %s" % (file, sha256hash))
+
+
+def package_macos(versions, jar_name):
+    libevent_version = versions['upstream']['libevent']
+    # zip binaries together
+    output_dir = get_output_dir(PLATFORM)
+    file_list = [
+        os.path.join(output_dir, 'aarch64', 'tor'),
+        os.path.join(output_dir, 'aarch64', 'libevent-' + libevent_version + '.dylib'),
+        os.path.join(output_dir, 'x86_64', 'tor'),
+        os.path.join(output_dir, 'x86_64', 'libevent-' + libevent_version + '.dylib'),
+    ]
+    zip_name = pack(versions, file_list, PLATFORM)
+    pom_name = create_pom_file(versions, PLATFORM)
+    print("%s:" % PLATFORM)
+    for file in file_list + [zip_name, jar_name, pom_name]:
+        sha256hash = get_sha256(file)
+        print("%s: %s" % (file, sha256hash))
+
+
+def create_sources_jar(versions):
+    output_dir = get_output_dir(PLATFORM)
+    jar_files = []
+    for root, dir_names, filenames in os.walk(BUILD_DIR):
+        for f in filenames:
+            if '/.git' in root:
+                continue
+            jar_files.append(os.path.join(root, f))
+    for file in jar_files:
+        reset_time(file, versions)
+    jar_name = get_sources_file_name(versions, PLATFORM)
+    jar_path = os.path.abspath(jar_name)
+    rel_paths = [os.path.relpath(f, BUILD_DIR) for f in sorted(jar_files)]
+    # create jar archive with first files
+    jar_step = 5000
+    check_call(['jar', 'cf', jar_path] + rel_paths[0:jar_step], cwd=BUILD_DIR)
+    # add subsequent files in steps, because the command line can't handle all at once
+    for i in range(jar_step, len(rel_paths), jar_step):
+        check_call(['jar', 'uf', jar_path] + rel_paths[i:i + jar_step], cwd=BUILD_DIR)
+    return jar_name
+
+
+def compare_output_with_upstream(versions):
+    compare_with_upstream(versions, "aarch64")
+    compare_with_upstream(versions, "x86_64")
+
+
+def compare_with_upstream(versions, arch):
+    print('comparing hashsums for {0}'.format(arch))
+    tor_browser_version = versions['upstream']['tor-browser']
+    libevent_version = versions['upstream']['libevent']
+    check_call(['wget', '-c', ('https://archive.torproject.org/tor-package-archive/torbrowser/{0}/'
+                + 'tor-expert-bundle-{0}-macos-{1}.tar.gz').format(tor_browser_version, arch)])
+    check_call(['tar', 'xvfz', 'tor-expert-bundle-{0}-macos-{1}.tar.gz'.format(tor_browser_version, arch),
+                '--one-top-level=upstream-' + arch, '--strip-components=1',
+                'tor/tor', 'tor/libevent-' + libevent_version + '.dylib'])
+    hash_tor_upstream = get_sha256(os.path.join('upstream-' + arch, 'tor'))
+    hash_libevent_upstream = get_sha256(os.path.join('upstream-' + arch, 'libevent-' + libevent_version + '.dylib'))
+    print('upstream tor: {0}'.format(hash_tor_upstream))
+    print('upstream libevent: {0}'.format(hash_libevent_upstream))
+
+    hash_tor_built = get_sha256(os.path.join('output', 'macos', arch, 'tor'))
+    hash_libevent_built = get_sha256(os.path.join('output', 'macos', arch, 'libevent-' + libevent_version + '.dylib'))
+    print('built tor: {0}'.format(hash_tor_built))
+    print('built libevent: {0}'.format(hash_libevent_built))
+
+    if hash_tor_upstream != hash_tor_built:
+        print("tor hash does not match")
+        exit(1)
+
+    if hash_libevent_upstream != hash_libevent_built:
+        print("libevent hash does not match")
+        exit(1)
+
+if __name__ == "__main__":
+    versions = build()
+    compare_output_with_upstream(versions)
diff --git a/verify_tor_macos.py b/verify_tor_macos.py
new file mode 100755
index 0000000000000000000000000000000000000000..464a8a2ac200e0f0da70322c3383864c2ce07b7d
--- /dev/null
+++ b/verify_tor_macos.py
@@ -0,0 +1,5 @@
+#!/usr/bin/env python3
+from verify_tor_utils import main
+
+if __name__ == "__main__":
+    main("macos")