Skip to content

The security & trust model

Registrars move real domains with the operator's credentials, so Core enforces trust rather than assuming it — the signing scheme, hardened install, and the three-gate load.

10 min readUpdated Aug 15, 2026
On this page

A registrar extension is not data — it is executable PHP that talks to a registrar with your operator's credentials and moves real domains: it registers, transfers, renews, and can release an EPP code. That is a lot of trust to hand a third party, so Core does not extend it on faith. Every time a driver is about to run, one class decides whether it may: App\Services\Registrars\RegistrarRegistry. It refuses unless three independent conditions hold, and it re-checks them on every resolve — not once at install. This article explains those gates, the signing scheme behind them, why a folder someone copies onto the box is inert, and the handful of author habits that follow directly from how the model works.

Why a registrar is trusted code, not trusted data#

A theme is verified once and then treated as inert data. A registrar is different: it is code that executes, and the operations it executes are irreversible on someone else's asset. So Core keeps the trust material around after install — the signed artifact itself, the signature headers, and a per-file hash manifest — and re-verifies against them every time the driver loads. The gate is not a one-time install check you can slip past afterward; it runs on the path that turns a database row into a live object, so there is no other way to get a driver instance.

If any gate fails, driverFor() returns null, and the caller falls back to NullRegistrar — a register, renew, or transfer fails safely instead of executing an unverified or unlicensed registrar, and Core hides every domain control rather than presenting one it cannot back. Core ships no built-in registrar drivers at all, so a slug with no installed, enabled extension simply resolves to nothing. (Domain availability search is the one path that never touches your driver — Core probes RDAP/WHOIS/DNS itself; see Building a registrar extension.)

The signing scheme#

On approval the marketplace signs the artifact with an Ed25519 key, producing two signatures that answer two different questions. The artifact signature is over "salieno.marketplace.artifact/1\0" || sha256(zip) — "are these the exact bytes Salieno approved". The domain tag is not decoration: without it the signature could be replayed into another Salieno protocol that signs a raw digest, and the Core updater is one. The descriptor signature is over the canonical descriptor JSON (schema salieno.marketplace.version/1) — "and is this what it claims to be": which product, which version, which Core window. Core verifies both against a pinned public key (rk-b4425f5d) baked into config/licensing.php. The key is never read from the response — a key the server hands over is one an attacker who can answer as the server has just minted — so a build signed by any other key is rejected. This is why there is no registrar upload: an operator cannot produce the signature the marketplace applies.

The three gates#

Every resolve runs all three, in order. Each is independent: passing one tells you nothing about the others.

Gate 1 — REGISTERED#

There must be an enabled `registrar_extensions` row for the slug:

php
$ext = RegistrarExtension::where('slug', $slug)->enabled()->first();
return $ext !== null ? $this->loadExtensionDriver($ext) : null;

That row is written only by the installer, at the end of a signed marketplace install. A directory someone drops onto the server has no row, so there is no record to resolve and nothing to load. A disabled row is treated as absent — it will not resolve, and it will not even leak its connection or credential profile to the Domain Registrars form.

Gate 2 — GENUINE#

The stored artifact must re-verify against the pinned marketplace key, and each on-disk class file's SHA-256 must match the hash recorded from that verified artifact at install:

php
$verified = app(MarketplaceSignature::class)->verify($ext->signatureHeaders(), $ext->artifactPath());
if (! ($verified['ok'] ?? false)) {
    return $this->deny($ext, 'signature_'.($verified['error'] ?? 'invalid'));
}

Because the key is pinned, you cannot forge a signature for it — which closes off hand-written drivers. The second half closes off tampering after install. When the installer unpacks a verified artifact it records relative-path => sha256 for every file into the extension's manifest, and the scoped autoloader consults that map before it requires anything:

php
// Bind loaded bytes to the verified artifact: refuse a class file edited after install.
$expected = $map['files'][$relative] ?? null;
if (is_string($expected) && $expected !== '' && ! hash_equals($expected, hash_file('sha256', $file))) {
    Log::warning('Registrar extension class file failed its hash check.', ['slug' => $slug, 'file' => $relative]);
    return;
}

require $file;

Edit a class file on disk and its hash no longer matches — the file will not load. The code that runs is exactly the code the marketplace signed, byte for byte. That verification rests on hardened extraction at install time: the installer rejects symlinks, refuses any entry that escapes its directory (../ or an absolute path), and caps both the entry count and the uncompressed size, so a malicious archive cannot plant files outside the extension or exhaust the disk before it is ever recorded.

Gate 3 — ENTITLED#

The activated licence must still own this registrar, and the marketplace decides that — not anything on the box:

php
$owns = app(MarketplaceClient::class)->ownsRegistrar($ext->slug);

if ($owns === true) {
    $ext->forceFill(['entitlement_checked_at' => now()])->saveQuietly();
    return true;
}

if ($owns === false) {
    return false; // marketplace answered: not owned
}

// $owns === null → unreachable. Trust a recent confirmation, then fail closed.
$graceHours = (int) config('licensing.entitlement_grace_hours', 72);
return $ext->entitlement_checked_at !== null
    && $ext->entitlement_checked_at->gt(now()->subHours($graceHours));

A definite yes refreshes the confirmation timestamp; a definite no denies immediately. If the marketplace is unreachable, Core trusts the last confirmation for a grace window (72 hours by default) so a transient outage does not tear down live domain management — and once that window lapses without a fresh confirmation, it fails closed. Install itself is proof of entitlement at that moment, because the single-use grant the installer consumes is only ever issued to an owner — an active entitlement plus an active licence for the same customer; the load-time gate simply keeps re-confirming.

This is the gate that makes ownership real rather than cosmetic. The signature proves the code is genuine, but genuineness is not permission — the entitlement check is what ties a specific genuine artifact to a specific paying licence.

Why a copied folder cannot run#

Put the three gates together and the answer falls out. Take a perfectly genuine, signed Namecheap folder off one server and drop it onto another install:

  • No row. Nothing wrote a registrar_extensions record, so Gate 1 finds nothing to resolve.
  • No entitlement. Even if you manufactured a row, the second install's licence never bought that registrar, so the marketplace answers not owned and Gate 3 denies it.

You cannot fake your way past Gate 2 either — you cannot forge the signature, and you cannot edit the extracted code without breaking its hash. There is no combination of file operations that turns a copied folder into a running driver.

Where installed code lives#

Installed extensions unpack to storage/app/registrars/{slug}/ — a runtime artifact that is gitignored and not on Composer's normal autoload path. Dropping classes there loads nothing on its own: no standard autoloader is looking in that directory.

The only thing that loads a registrar class is RegistrarRegistry's scoped autoloader, registered by the registrar service provider. It refuses to load anything until the full gate has run and unlocked the slug, and then it loads only classes that belong to that unlocked extension's namespace and whose file hash matches the recorded value. A slug that has not passed the gate is not in the unlock list, so its namespace is never even considered. Loading is gated at both ends: nothing generic can trigger it, and the bytes it loads are pinned to the verified artifact.

What this means for you as an author#

The model is strict on purpose, and a few author habits follow directly from it.

Hold no per-domain state — the driver is stateless#

The registry instantiates your driver once, with no constructor, and passes the Domain into every method. That is deliberate isolation: because the driver carries nothing between calls, one domain's operation cannot leak into another's. Read your credentials and context per call from the domain's connection profile — $this->registrarOf($domain), $this->getParam($registrar, 'api_key', ''), $this->parseDomain($domain) — and never stash anything on $this. Writing into your own folder fights the system twice over: a modified file fails the hash check, and an update replaces the whole directory atomically, discarding whatever you wrote. Persist state on what Core hands you — the Domain model, which you may ->save().

Never leak credentials#

The operator's registrar API credentials live in the DomainRegister connection profile, encrypted at rest and hidden from serialization, logs, and Livewire snapshots — so a stack trace or a component payload never carries a live key. Keep it that way. Declare secret fields as type: "password" in your manifest's credentials block so Core masks them in the form (see Connection profiles and credentials). Never log a secret, and never echo one back in a message. Registrar APIs fail with raw text full of account identifiers and sometimes token fragments — surface the real registrar error so the operator can act on it, but strip the credentials out first.

Declare capabilities honestly#

capabilities() is a security boundary as much as a UI one. Core shows a control only if you declare its capability, so declaring something you cannot actually honor produces a button that fails when the operator clicks it — on a live domain. Declare only what your driver truly implements: a registrar with no EPP-code API omits epp_code and Core shows the client manual-retrieval instructions instead; one with no child-nameserver support omits child_nameservers and that card never appears. A partial registrar is first-class; a padded declaration is a broken one. The manifest's capabilities and the driver's capabilities() must match — see The driver contract.

Target exactly one domain, and fail safe#

Every method receives one specific Domain, and every registrar operation acts on a real, often irreversible asset — a transfer, a nameserver delegation, an unlock. Build the API call from the exact domain you were handed; if an identifier it needs is missing or malformed, return $this->error(...) with a clear message rather than issuing a call a registrar might misinterpret. And never throw out of a public method: an uncaught exception escaping the driver is a failure Core cannot shape into a safe result. Catch it, return the error envelope, and let the caller fall back cleanly.

In short#

Core will run your code because three things are simultaneously true: it is registered from a signed install, it is genuine and unmodified, and the licence still owns it. That safety is what lets a hosting operator install a third-party registrar without auditing its source — you cannot ship a backdoor that survives review, signing, and the hash gate, you cannot run unentitled, and Core hides every control your capabilities() does not declare. Your part of the bargain is to stay a clean, stateless artifact — hold no state, leak no credentials, declare only what you do, and target precisely. When you are ready to ship, Publishing and updates walks through the signed install and update path that these gates enforce.

Was this article helpful?
Still stuck?Contact support
The security & trust model · Salieno Docs