feat(portal): sign Apple profiles — Verified instead of 'unsigned' warning
ci / changes (push) Successful in 4s
ci / tc_booking (push) Has been skipped
ci / tc_operator (push) Has been skipped
ci / tc_website (push) Has been skipped
ci / tc_platform_api (push) Has been skipped
ci / test_platform_api (push) Has been skipped
ci / build_booking (push) Has been skipped
ci / build_operator (push) Has been skipped
ci / build_platform_api (push) Has been skipped
ci / tc_portal (push) Successful in 26s
ci / build_portal (push) Successful in 49s
ci / deploy (push) Successful in 42s
ci / changes (push) Successful in 4s
ci / tc_booking (push) Has been skipped
ci / tc_operator (push) Has been skipped
ci / tc_website (push) Has been skipped
ci / tc_platform_api (push) Has been skipped
ci / test_platform_api (push) Has been skipped
ci / build_booking (push) Has been skipped
ci / build_operator (push) Has been skipped
ci / build_platform_api (push) Has been skipped
ci / tc_portal (push) Successful in 26s
ci / build_portal (push) Successful in 49s
ci / deploy (push) Successful in 42s
Unsigned .mobileconfig installs trip macOS warnings ('unknown developer')
and an extra System Settings hunt. The route now wraps the profile in
PKCS#7 SignedData (node-forge, SHA-256, full chain embedded) using the
portal's own cert-manager LE certificate mounted read-only into the pod
(PROFILE_SIGN_CERT/KEY). Publicly-trusted chain → Apple shows Verified.
Dev (no env) and any signing failure fall back to unsigned — the
download must never break over the badge. Signature round-trip verified
with openssl smime.
This commit is contained in:
@@ -12,6 +12,8 @@
|
||||
|
||||
import { getUserSession } from 'nuxt-oidc-auth/runtime/server/utils/session.js'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import forge from 'node-forge'
|
||||
|
||||
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
|
||||
@@ -124,5 +126,45 @@ export default defineEventHandler(async (event) => {
|
||||
'Content-Disposition',
|
||||
`attachment; filename="dezky-mail-${localPart.replace(/[^a-z0-9.-]/gi, '_')}.mobileconfig"`,
|
||||
)
|
||||
return profile
|
||||
return signProfile(profile)
|
||||
})
|
||||
|
||||
// Sign the profile (PKCS#7 SignedData with embedded content) using the TLS
|
||||
// cert mounted from cert-manager (PROFILE_SIGN_CERT/KEY in production —
|
||||
// fleet/apps/portal.yaml). A publicly-trusted signature turns Apple's
|
||||
// "unsigned profile" warning into a green Verified badge. Unset env (dev)
|
||||
// → unsigned profile, still installable with the warning.
|
||||
function signProfile(profileXml: string): Buffer | string {
|
||||
const certPath = process.env.PROFILE_SIGN_CERT
|
||||
const keyPath = process.env.PROFILE_SIGN_KEY
|
||||
if (!certPath || !keyPath) return profileXml
|
||||
try {
|
||||
const certPem = readFileSync(certPath, 'utf8')
|
||||
const keyPem = readFileSync(keyPath, 'utf8')
|
||||
// cert-manager's tls.crt is the full chain — leaf first. Include every
|
||||
// cert so Apple can build the path to the trusted root.
|
||||
const certs = certPem
|
||||
.split(/(?=-----BEGIN CERTIFICATE-----)/)
|
||||
.filter((c) => c.includes('BEGIN CERTIFICATE'))
|
||||
.map((c) => forge.pki.certificateFromPem(c))
|
||||
const p7 = forge.pkcs7.createSignedData()
|
||||
p7.content = forge.util.createBuffer(profileXml, 'utf8')
|
||||
for (const c of certs) p7.addCertificate(c)
|
||||
p7.addSigner({
|
||||
key: forge.pki.privateKeyFromPem(keyPem) as forge.pki.rsa.PrivateKey,
|
||||
certificate: certs[0]!,
|
||||
digestAlgorithm: forge.pki.oids.sha256!,
|
||||
authenticatedAttributes: [
|
||||
{ type: forge.pki.oids.contentType!, value: forge.pki.oids.data! },
|
||||
{ type: forge.pki.oids.messageDigest! },
|
||||
{ type: forge.pki.oids.signingTime! },
|
||||
],
|
||||
})
|
||||
p7.sign()
|
||||
return Buffer.from(forge.asn1.toDer(p7.toAsn1()).getBytes(), 'binary')
|
||||
} catch (err) {
|
||||
// Never fail the download over signing — fall back to unsigned.
|
||||
console.error('apple-mailconfig: profile signing failed, serving unsigned:', err)
|
||||
return profileXml
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user