From fc2898083cae65c49f97ccff4aa0f3282219b264 Mon Sep 17 00:00:00 2001
From: jessikitty
Date: Mon, 7 Sep 2026 09:43:36 +1000
Subject: [PATCH] SSL Cert Fixes
---
public/js/admin.js | 3 ++-
push-to-gitea.ps1 | 24 ++++++++++++++++++++++++
push-to-gitea.sh | 20 ++++++++++++++++++++
src/routes/admin.js | 18 ++++++++++++++++++
src/server.js | 19 +++++++++++++++++++
src/tls.js | 13 +++++++++++++
6 files changed, 96 insertions(+), 1 deletion(-)
diff --git a/public/js/admin.js b/public/js/admin.js
index bf5b541..986af67 100644
--- a/public/js/admin.js
+++ b/public/js/admin.js
@@ -1354,7 +1354,8 @@ function renderTls(tls) {
itself before it lapses, and devices that trust the authority keep working without being
touched again.
`;
diff --git a/push-to-gitea.ps1 b/push-to-gitea.ps1
index d11b74f..c11d9a7 100644
--- a/push-to-gitea.ps1
+++ b/push-to-gitea.ps1
@@ -41,6 +41,30 @@ if ((git remote) -match '^origin$') {
git remote add origin $Remote
}
+# ------------------------------------------------- finish what was started
+
+# A rebase or merge left half-done blocks everything that follows, and the error
+# git gives is easy to mistake for a push problem. Catch it here and say plainly
+# what to do.
+$gitDir = (git rev-parse --git-dir 2>$null)
+if ($gitDir) {
+ $stuck = @('rebase-merge', 'rebase-apply', 'MERGE_HEAD', 'CHERRY_PICK_HEAD') |
+ Where-Object { Test-Path (Join-Path $gitDir $_) }
+ if ($stuck) {
+ Write-Host ''
+ Write-Host 'There is an unfinished rebase or merge in this folder.' -ForegroundColor Red
+ Write-Host 'Nothing else can happen until it is settled. Your options:' -ForegroundColor Yellow
+ Write-Host ''
+ Write-Host ' git rebase --abort throw the attempt away and go back to how things were'
+ Write-Host ' git status see which files still need attention'
+ Write-Host ' git rebase --continue after fixing the files git listed'
+ Write-Host ''
+ Write-Host 'If you are unsure, "git rebase --abort" is the safe one. It puts the' -ForegroundColor Yellow
+ Write-Host 'folder back exactly as it was before the rebase started.' -ForegroundColor Yellow
+ exit 1
+ }
+}
+
# ---------------------------------------------------------------- commit
git add -A
diff --git a/push-to-gitea.sh b/push-to-gitea.sh
index 5aa3ed1..2ef8e64 100644
--- a/push-to-gitea.sh
+++ b/push-to-gitea.sh
@@ -19,6 +19,26 @@ else
git remote add origin "$REMOTE"
fi
+# An unfinished rebase or merge blocks everything below, and git's own error is
+# easy to mistake for a push problem.
+GIT_DIR_PATH=$(git rev-parse --git-dir 2>/dev/null || echo .git)
+for marker in rebase-merge rebase-apply MERGE_HEAD CHERRY_PICK_HEAD; do
+ if [ -e "$GIT_DIR_PATH/$marker" ]; then
+ cat >&2 <<'MSG'
+
+There is an unfinished rebase or merge in this folder.
+Nothing else can happen until it is settled:
+
+ git rebase --abort throw the attempt away, back to how things were
+ git status see which files still need attention
+ git rebase --continue after fixing the files git listed
+
+If unsure, "git rebase --abort" is the safe one.
+MSG
+ exit 1
+ fi
+done
+
git add -A
if [ -n "$(git status --porcelain)" ]; then
read -r -p "Describe this change (enter for a dated default): " MSG
diff --git a/src/routes/admin.js b/src/routes/admin.js
index e6d9fca..cfdfe53 100644
--- a/src/routes/admin.js
+++ b/src/routes/admin.js
@@ -1224,6 +1224,24 @@ router.get('/tls/ca.crt', (req, res) => {
res.send(ca);
});
+/**
+ * The same authority in DER form, for Jamf Pro and anything else built on Apple's
+ * tooling. Offered as .cer and .der because different consoles insist on
+ * different extensions for the identical bytes.
+ */
+router.get(['/tls/ca.cer', '/tls/ca.der'], (req, res) => {
+ try {
+ const der = tls.caCertificateDer();
+ if (!der) return res.status(404).send('No certificate authority has been generated yet.');
+ const ext = req.path.endsWith('.der') ? 'der' : 'cer';
+ res.setHeader('Content-Type', 'application/pkix-cert');
+ res.setHeader('Content-Disposition', `attachment; filename="visitor-signin-ca.${ext}"`);
+ res.send(der);
+ } catch (err) {
+ res.status(500).send(`Could not convert the certificate: ${err.message}`);
+ }
+});
+
router.post('/tls/renew', requireOwner, (req, res) => {
try {
// A brand new CA means every kiosk device has to trust it again, so it is
diff --git a/src/server.js b/src/server.js
index ffb5740..3ea6366 100644
--- a/src/server.js
+++ b/src/server.js
@@ -121,6 +121,25 @@ function startRedirectServer() {
http
.createServer((req, res) => {
+ // DER for Apple tooling, PEM for everything else.
+ if (req.url === '/ca.cer' || req.url === '/ca.der') {
+ try {
+ const der = tls.caCertificateDer();
+ if (!der) {
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
+ return res.end('No certificate authority has been generated yet.');
+ }
+ res.writeHead(200, {
+ 'Content-Type': 'application/pkix-cert',
+ 'Content-Disposition': 'attachment; filename="visitor-signin-ca.cer"',
+ });
+ return res.end(der);
+ } catch (err) {
+ res.writeHead(500, { 'Content-Type': 'text/plain' });
+ return res.end(`Could not convert the certificate: ${err.message}`);
+ }
+ }
+
if (req.url === '/ca.crt' || req.url === '/ca.pem') {
const ca = tls.caCertificate();
if (!ca) {
diff --git a/src/tls.js b/src/tls.js
index 4d93e4c..1145c1d 100644
--- a/src/tls.js
+++ b/src/tls.js
@@ -225,6 +225,19 @@ export function describe() {
};
}
+/**
+ * The CA in DER form.
+ *
+ * The .crt on disk is PEM: base64 text between BEGIN/END lines. Apple's tooling,
+ * and therefore Jamf Pro's certificate payload, wants the raw binary DER instead
+ * and rejects the file on its extension. Same certificate, different wrapper.
+ */
+export function caCertificateDer() {
+ const p = paths();
+ if (!fs.existsSync(p.caCert)) return null;
+ return openssl(['x509', '-in', p.caCert, '-outform', 'der']);
+}
+
export function caCertificate() {
const p = paths();
return fs.existsSync(p.caCert) ? fs.readFileSync(p.caCert) : null;