Stop saying Signal requires a phone number to register

Signal now documents registration without one, as "Phone Numberless
Registration for Android". The row said "No" with the gloss "A number is
required to register", which is the same kind of stale-in-our-own-favour
scoring the page already admits to in the post-quantum paragraph.

The row is now "Partly", naming what the registration actually is: new
accounts on Android, paid, and no way for an account that already has a
number to drop it. The lede no longer claims we are alone in not asking
for a number, because we are not; what is still only ours is that it
costs nothing and needs no app store account.

What could not be verified from here: this machine cannot resolve
support.signal.org and the in-app browser is blocked from it, so the
price and the accepted payment methods are left off the page rather than
taken from a news write-up. Two secondary sources agree it is a paid
registration, which is the part the row states.

The test renders both languages. Nothing rendered the Spanish page
before: PublicPagesTests reads those templates as text, which catches a
broken link and not a template that fails to render, and the SEO suite
renders the English page alone. Falsified by putting the old Spanish
sentence back, which fails on /es/comparison by name.

752 tests, no failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Paul Hitt
2026-09-25 08:35:45 -04:00
co-authored by Claude Opus 5
parent 3f0e24bc0a
commit b50a2dbe92
3 changed files with 116 additions and 14 deletions
+8 -8
View File
@@ -5,11 +5,11 @@
<h1>How this compares</h1>
<p class="lede lede-wide">
Four messengers, side by side. A few things on this table only Flamenet
Messenger does: it runs on a server you control, it never asks for your
phone number, and your address is yours rather than the server's, so a
relay shutting down costs you a mailbox, not your name. The rest is laid
out plainly and sourced from each project's own documentation, so you can
check every line of it.
Messenger does: it runs on a server you control, it asks for no phone
number and charges nothing to leave it out, and your address is yours
rather than the server's, so a relay shutting down costs you a mailbox,
not your name. The rest is laid out plainly and sourced from each
project's own documentation, so you can check every line of it.
</p>
<div class="compare-scroll">
@@ -34,7 +34,7 @@
<tr>
<th scope="row">Works without a phone number</th>
<td class="mine"><strong class="yes">Yes</strong><br><small>A username and an email address.</small></td>
<td><strong class="no">No</strong><br><small>A number is required to register. Usernames hide it from other people, but not from Signal.</small></td>
<td><strong class="part">Partly</strong><br><small>New accounts on Android, through the paid registration Signal documents as "Phone Numberless Registration". An account that already has a number cannot drop it, and usernames hide a number from other people but not from Signal.</small></td>
<td><strong class="no">No</strong></td>
<td><strong class="part">Partly</strong><br><small>An Apple ID, reachable by phone number or email.</small></td>
</tr>
@@ -179,12 +179,12 @@
<h2>Where these facts come from</h2>
<p>
Everything above is drawn from each project's own published documentation
and from public security research, checked in August 2026. These are given
and from public security research, checked in September 2026. These are given
as references rather than links, because this site deliberately doesn't
send you anywhere else:
</p>
<ul>
<li><strong>Signal</strong>, Signal Support, "Phone Number Privacy and Usernames"; Signal blog, "Keep your phone number private with Signal usernames"; the PQXDH specification.</li>
<li><strong>Signal</strong>, Signal Support, "Phone Number Privacy and Usernames" and "Phone Numberless Registration for Android"; Signal blog, "Keep your phone number private with Signal usernames"; the PQXDH specification.</li>
<li><strong>Telegram</strong>, Telegram Core API, "End-to-End Encryption, Secret Chats", which documents that cloud chats are client–server encrypted and that Secret Chats are opt-in and one-to-one.</li>
<li><strong>Apple</strong>, Apple Security Research, "iMessage with PQ3"; Apple Support, "iCloud data security overview"; the iOS 26.5 release notes covering encrypted RCS; Stebila, "Security analysis of the iMessage PQ3 protocol", and the USENIX Security 2025 formal analysis of PQ3.</li>
<li><strong>Flamenet Messenger</strong>, the <a href="/spec">specification</a>, the <a href="/metadata">metadata page</a> and the <a href="/roadmap">roadmap</a> on this site.</li>
+7 -6
View File
@@ -5,9 +5,10 @@
<h1>Cómo se compara</h1>
<p class="lede lede-wide">
Cuatro aplicaciones de mensajería, una al lado de otra. Algunas cosas de esta tabla
solo las hace Flamenet Messenger: funciona en un servidor que tú controlas, nunca
pide tu número de teléfono, y tu dirección es tuya y no del servidor, así que un
servidor que cierra te cuesta un buzón, no tu nombre. El resto está puesto tal cual y
solo las hace Flamenet Messenger: funciona en un servidor que tú controlas, no
pide ningún número de teléfono y no te cobra nada por prescindir de él, y tu
dirección es tuya y no del servidor, así que un servidor que cierra te cuesta
un buzón, no tu nombre. El resto está puesto tal cual y
sale de la documentación de cada proyecto, para que puedas comprobar cada línea.
</p>
@@ -33,7 +34,7 @@
<tr>
<th scope="row">Funciona sin número de teléfono</th>
<td class="mine"><strong class="yes">Sí</strong><br><small>Un nombre de usuario y una dirección de correo.</small></td>
<td><strong class="no">No</strong><br><small>Hace falta un número para registrarse. Los nombres de usuario lo ocultan a otras personas, pero no a Signal.</small></td>
<td><strong class="part">En parte</strong><br><small>Solo en cuentas nuevas de Android, mediante el registro de pago que Signal documenta como "Phone Numberless Registration". Una cuenta que ya tiene número no puede quitárselo, y los nombres de usuario ocultan el número a otras personas, pero no a Signal.</small></td>
<td><strong class="no">No</strong></td>
<td><strong class="part">En parte</strong><br><small>Un Apple ID, localizable por número de teléfono o por correo.</small></td>
</tr>
@@ -173,12 +174,12 @@
<h2>De dónde salen estos datos</h2>
<p>
Todo lo de arriba sale de la documentación publicada por cada proyecto y de la
investigación pública en seguridad, comprobado en agosto de 2026. Se dan como
investigación pública en seguridad, comprobado en septiembre de 2026. Se dan como
referencias y no como enlaces, porque este sitio, a propósito, no te manda a ningún
otro:
</p>
<ul>
<li><strong>Signal</strong>: Signal Support, "Phone Number Privacy and Usernames"; el blog de Signal, "Keep your phone number private with Signal usernames"; la especificación PQXDH.</li>
<li><strong>Signal</strong>: Signal Support, "Phone Number Privacy and Usernames" y "Phone Numberless Registration for Android"; el blog de Signal, "Keep your phone number private with Signal usernames"; la especificación PQXDH.</li>
<li><strong>Telegram</strong>: Telegram Core API, "End-to-End Encryption, Secret Chats", que documenta que los chats en la nube van cifrados cliente-servidor y que los chats secretos son opcionales y de uno a uno.</li>
<li><strong>Apple</strong>: Apple Security Research, "iMessage with PQ3"; Apple Support, "iCloud data security overview"; las notas de la versión iOS 26.5 sobre RCS cifrado; Stebila, "Security analysis of the iMessage PQ3 protocol", y el análisis formal de PQ3 de USENIX Security 2025.</li>
<li><strong>Flamenet Messenger</strong>: la <a href="/spec">especificación</a>, la <a href="/es/metadata">página de metadatos</a> y la <a href="/es/roadmap">hoja de ruta</a> de este sitio.</li>
@@ -0,0 +1,101 @@
import Crypto
import Foundation
import Testing
import VaporTesting
@testable import FlamenetRelay
/// The one row on the comparison table that names a competitor's limit.
///
/// Signal shipped registration without a phone number, documented as "Phone
/// Numberless Registration for Android", and until this was written the table
/// still said a number was required to register. A comparison page that is
/// stale in its own favour is the failure mode this page already admits to
/// once, in the paragraph about the post-quantum row.
///
/// Both languages, because the Spanish page carries the same claim in its own
/// words and nothing rendered it: `PublicPagesTests` reads the Spanish
/// templates as TEXT, which catches a broken link and not a template that
/// fails to render, and the SEO suite renders `/comparison` alone.
@Suite("the comparison table does not overstate a competitor", .serialized)
struct ComparisonClaimsTests {
private func withRelayApp(_ body: (Application) async throws -> Void) async throws {
// Serialised against the other suites that boot a relay this way.
// `setenv` is process-global; see ProcessEnvironmentLock.
try await ProcessEnvironmentLock.withExclusiveEnvironment {
let issuer = Curve25519.Signing.PrivateKey()
let dbPath = NSTemporaryDirectory() + "fn-cmp-\(UUID().uuidString).sqlite"
let attachments = NSTemporaryDirectory() + "fn-cmp-att-\(UUID().uuidString)"
let env: [String: String] = [
"FN_ISSUER_PUBLIC_KEY": issuer.publicKey.rawRepresentation.base64EncodedString(),
"FN_ISSUER_PRIVATE_KEY": issuer.rawRepresentation.base64EncodedString(),
"FN_RELAY_AUDIENCE": "test.relay",
"FN_DB": dbPath,
"FN_ATTACHMENTS_DIR": attachments,
"FN_LOG_LEVEL": "critical",
]
let previous = env.keys.reduce(into: [String: String?]()) {
$0[$1] = ProcessInfo.processInfo.environment[$1]
}
for (k, v) in env { setenv(k, v, 1) }
defer {
for (k, v) in previous { if let v { setenv(k, v, 1) } else { unsetenv(k) } }
try? FileManager.default.removeItem(atPath: dbPath)
try? FileManager.default.removeItem(atPath: attachments)
}
let app = try await Application.make(.testing)
do {
try await configure(app)
try await body(app)
} catch {
try? await app.asyncShutdown()
throw error
}
try? await app.asyncShutdown()
}
}
@Test("neither language still says Signal requires a number to register")
func phoneNumberRowIsCurrent() async throws {
try await withRelayApp { app in
// path, the row heading that proves the page rendered, and the
// sentence that went stale.
let pages = [
(
"/comparison", "Works without a phone number",
"A number is required to register"
),
(
"/es/comparison", "Funciona sin número de teléfono",
"Hace falta un número para registrarse"
),
]
for (path, heading, stale) in pages {
try await app.testing().test(
.GET, path,
afterResponse: { res async in
#expect(res.status == .ok, "\(path) did not render")
let html = res.body.string
// Control. Everything below searches a string, and a
// page that failed to render is an empty one: the
// absence assertion would pass on nothing at all.
#expect(
html.contains(heading),
"\(path) rendered without the phone number row, so this proves nothing")
#expect(
!html.contains(stale),
"\(path) still says Signal requires a number to register")
// And it says what is true now, so the assertion above
// cannot be satisfied by deleting the row.
#expect(
html.contains("Phone Numberless Registration"),
"\(path) does not name the registration Signal now documents")
})
}
}
}
}