Page MenuHomePhorge

D5880.1778245727.diff
No OneTemporary

Authored By
Unknown
Size
27 KB
Referenced Files
None
Subscribers
None

D5880.1778245727.diff

diff --git a/src/app/Backends/PGP.php b/src/app/Backends/PGP.php
--- a/src/app/Backends/PGP.php
+++ b/src/app/Backends/PGP.php
@@ -2,11 +2,7 @@
namespace App\Backends;
-use App\PowerDNS\Domain;
-use App\PowerDNS\Record;
use App\User;
-use App\Utils;
-use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
class PGP
@@ -74,11 +70,8 @@
// Store the keypair in Roundcube Enigma storage
self::dbSave(true);
- // Get the ASCII armored data of the public key
- $armor = self::$gpg->exportPublicKey((string) $key, true);
-
- // Register the public key in DNS
- self::keyRegister($email, $armor);
+ // Register the public key in the system
+ self::keyRegister($user, $email, $key);
// FIXME: Should we remove the files from the worker filesystem?
// They are still in database and Roundcube hosts' filesystem
@@ -95,13 +88,14 @@
public static function keyDelete(User $user, string $email): void
{
// Start with the DNS, it's more important
- self::keyUnregister($email);
+ self::keyUnregister($user, $email);
- // Remove the whole Enigma keyring (if it's a delete user account)
+ // Remove the whole Enigma keyring if deleting a user
if ($user->email === $email) {
self::homedirCleanup($user);
- $user->aliases()->pluck('alias')->each(static fn ($alias) => self::keyUnregister($alias));
+ $user->aliases()->pluck('alias')->each(static fn ($alias) => self::keyUnregister($user, $alias));
}
+
// TODO: remove only the alias key from Enigma keyring
}
@@ -130,47 +124,51 @@
}
/**
- * Register the key in the WOAT DNS system
- *
- * @param string $email Email address
- * @param string $key The ASCII-armored key content
+ * Check if a specific PGP service is enabled
*/
- private static function keyRegister(string $email, string $key): void
+ public static function isEnabled(string $service): bool
{
- [$local, $domain] = Utils::normalizeAddress($email, true);
-
- DB::beginTransaction();
-
- $domain = Domain::firstOrCreate([
- 'name' => '_woat.' . $domain,
- ]);
+ return in_array(strtolower($service), \config('pgp.services'));
+ }
- Record::create([
- 'domain_id' => $domain->id,
- 'name' => sha1($local) . '.' . $domain->name,
- 'type' => 'TXT',
- 'content' => 'v=woat1,public_key=' . $key,
- ]);
+ /**
+ * Get the ASCII armored data of a public key
+ *
+ * @param User $user User object
+ * @param \Crypt_GPG_Key $key Key
+ */
+ public static function exportPublicKey(User $user, \Crypt_GPG_Key $key): string
+ {
+ self::initGPG($user, true);
- DB::commit();
+ return self::$gpg->exportPublicKey((string) $key, true);
}
/**
- * Remove the key from the WOAT DNS system
- *
- * @param string $email Email address
+ * Register a new key in the system (WOAT, HKP)
*/
- private static function keyUnregister(string $email): void
+ private static function keyRegister(User $user, string $email, \Crypt_GPG_Key $key): void
{
- [$local, $domain] = Utils::normalizeAddress($email, true);
+ if (self::isEnabled('woat')) {
+ PGP\WOAT::addKey($user, $email, $key);
+ }
- $domain = Domain::where('name', '_woat.' . $domain)->first();
+ if (self::isEnabled('hkp')) {
+ PGP\Keyserver::addKey($user, $key);
+ }
+ }
- if ($domain) {
- $fqdn = sha1($local) . '.' . $domain->name;
+ /**
+ * Remove the key from the system (WOAT, HKP)
+ */
+ private static function keyUnregister(User $user, string $email): void
+ {
+ if (self::isEnabled('woat')) {
+ PGP\WOAT::deleteKey($user, $email);
+ }
- // For now we support only one WOAT key record
- $domain->records()->where('name', $fqdn)->delete();
+ if (self::isEnabled('hkp')) {
+ PGP\Keyserver::deleteKey($user, $email);
}
}
diff --git a/src/app/Backends/PGP/Key.php b/src/app/Backends/PGP/Key.php
new file mode 100644
--- /dev/null
+++ b/src/app/Backends/PGP/Key.php
@@ -0,0 +1,108 @@
+<?php
+
+namespace App\Backends\PGP;
+
+use App\User;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+use Illuminate\Database\Eloquent\Relations\HasMany;
+
+/**
+ * Definition of PGP (public) key
+ *
+ * @property string $content Key ascii-armored content
+ * @property string $id Key identifier
+ * @property int $user_id Key owner
+ */
+class Key extends Model
+{
+ /** @var array<string, string> The attributes that should be cast */
+ protected $casts = [
+ 'created_at' => 'datetime:Y-m-d H:i:s',
+ 'updated_at' => 'datetime:Y-m-d H:i:s',
+ ];
+
+ /** @var list<string> The attributes that are mass assignable */
+ protected $fillable = [
+ 'id',
+ 'content',
+ 'user_id',
+ ];
+
+ /** @var string Database table name */
+ protected $table = 'pgp_keys';
+
+ /** @var bool Enable primary key autoincrement (required here for Pivots) */
+ public $incrementing = false;
+
+ /**
+ * Extract key ID (integer) from a hex fingerprint
+ */
+ public static function fingerprintId(string $fingerprint): string
+ {
+ // Note: We can't be using integer type as PHP does not support unsigned int.
+ // So, we operate on strings
+ return substr($fingerprint, -16);
+ }
+
+ /**
+ * Find a key by an email address
+ */
+ public static function findByEmail(string $email): ?self
+ {
+ $keys = KeyIdentity::select('key_id')->distinct()
+ ->where('email', $email)
+ ->limit(2)
+ ->get();
+
+ return $keys->count() === 1 ? self::find($keys[0]->key_id) : null;
+ }
+
+ /**
+ * Find a key by a key identifier (ID or fingerprint)
+ */
+ public static function findById(string $id): ?self
+ {
+ $id = self::fingerprintId($id);
+
+ // Lookup the main key
+ if ($key = self::find($id)) {
+ return $key;
+ }
+
+ // Lookup via a subkey
+ $subkey = KeySubkey::find($id);
+
+ return $subkey ? self::find($subkey->key_id) : null;
+ }
+
+ /**
+ * The key identities (users)
+ *
+ * @return HasMany<KeyIdentity, $this>
+ */
+ public function identities()
+ {
+ return $this->hasMany(KeyIdentity::class, 'key_id');
+ }
+
+ /**
+ * The key subkeys
+ *
+ * @return HasMany<KeySubkey, $this>
+ */
+ public function subkeys()
+ {
+ return $this->hasMany(KeySubkey::class, 'key_id');
+ }
+
+ /**
+ * The key owner
+ *
+ * @return BelongsTo<User, $this>
+ */
+ public function user()
+ {
+ return $this->belongsTo(User::class, 'user_id');
+ }
+}
diff --git a/src/app/Backends/PGP/KeyIdentity.php b/src/app/Backends/PGP/KeyIdentity.php
new file mode 100644
--- /dev/null
+++ b/src/app/Backends/PGP/KeyIdentity.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace App\Backends\PGP;
+
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+
+/**
+ * Definition of PGP key user identity (for user ID lookups)
+ *
+ * @property string $email Email address
+ * @property int $id User identifier
+ * @property string $key_id Key identifier
+ */
+class KeyIdentity extends Model
+{
+ /** @var array<string, string> The attributes that should be cast */
+ protected $casts = [
+ 'created_at' => 'datetime:Y-m-d H:i:s',
+ 'updated_at' => 'datetime:Y-m-d H:i:s',
+ ];
+
+ /** @var list<string> The attributes that are mass assignable */
+ protected $fillable = [
+ 'email',
+ 'key_id',
+ ];
+
+ /** @var string Database table name */
+ protected $table = 'pgp_identities';
+
+ /**
+ * The public key
+ *
+ * @return BelongsTo<Key, $this>
+ */
+ public function key()
+ {
+ return $this->belongsTo(Key::class, 'key_id');
+ }
+}
diff --git a/src/app/Backends/PGP/KeySubkey.php b/src/app/Backends/PGP/KeySubkey.php
new file mode 100644
--- /dev/null
+++ b/src/app/Backends/PGP/KeySubkey.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace App\Backends\PGP;
+
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+
+/**
+ * Definition of PGP subkey (for subkey ID lookups)
+ *
+ * @property string $id Subkey identifier
+ * @property string $key_id Key identifier
+ */
+class KeySubkey extends Model
+{
+ /** @var array<string, string> The attributes that should be cast */
+ protected $casts = [
+ 'created_at' => 'datetime:Y-m-d H:i:s',
+ 'updated_at' => 'datetime:Y-m-d H:i:s',
+ ];
+
+ /** @var list<string> The attributes that are mass assignable */
+ protected $fillable = [
+ 'id',
+ 'key_id',
+ ];
+
+ /** @var string Database table name */
+ protected $table = 'pgp_subkeys';
+
+ /** @var bool Enable primary key autoincrement (required here for Pivots) */
+ public $incrementing = false;
+
+ /**
+ * The public key
+ *
+ * @return BelongsTo<Key, $this>
+ */
+ public function key()
+ {
+ return $this->belongsTo(Key::class, 'key_id');
+ }
+}
diff --git a/src/app/Backends/PGP/Keyserver.php b/src/app/Backends/PGP/Keyserver.php
new file mode 100644
--- /dev/null
+++ b/src/app/Backends/PGP/Keyserver.php
@@ -0,0 +1,69 @@
+<?php
+
+namespace App\Backends\PGP;
+
+use App\Backends\PGP;
+use App\User;
+use Illuminate\Support\Facades\DB;
+
+/**
+ * PGP key server backend
+ */
+class Keyserver
+{
+ /**
+ * Add a new key to the server
+ */
+ public static function addKey(User $user, \Crypt_GPG_Key $key): void
+ {
+ // Get the ASCII armored data of the public key
+ $armor = PGP::exportPublicKey($user, $key);
+
+ $key_id = Key::fingerprintId((string) $key);
+
+ DB::beginTransaction();
+
+ $record = Key::create([
+ 'id' => $key_id,
+ 'content' => $armor,
+ 'user_id' => $user->id,
+ ]);
+
+ $emails = [];
+ foreach ($key->getUserIds() as $userid) {
+ if ($userid->isValid() && !$userid->isRevoked()) {
+ if (($email = $userid->getEmail()) && !in_array($email, $emails)) {
+ $record->identities()->create(['email' => $email]);
+ $emails[] = $email;
+ }
+ }
+ }
+
+ foreach ($key->getSubKeys() as $subkey) {
+ if (!$subkey->isRevoked()) {
+ $subkey_id = Key::fingerprintId($subkey->getFingerprint());
+ if ($subkey_id != $key_id) {
+ $record->subkeys()->create(['id' => $subkey_id]);
+ }
+ }
+ }
+
+ DB::commit();
+ }
+
+ /**
+ * Remove a key from the server
+ */
+ public static function deleteKey(User $user, string $email): void
+ {
+ // TODO
+ }
+
+ /**
+ * Remove a key from the server
+ */
+ public static function updateKey(\Crypt_GPG_Key $key): void
+ {
+ // TODO
+ }
+}
diff --git a/src/app/Backends/PGP/WOAT.php b/src/app/Backends/PGP/WOAT.php
new file mode 100644
--- /dev/null
+++ b/src/app/Backends/PGP/WOAT.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace App\Backends\PGP;
+
+use App\Backends\PGP;
+use App\PowerDNS\Domain;
+use App\User;
+use App\Utils;
+use Illuminate\Support\Facades\DB;
+
+class WOAT
+{
+ /**
+ * Register a new key in the system (WOAT, HKP)
+ */
+ public static function addKey(User $user, string $email, \Crypt_GPG_Key $key): void
+ {
+ // Get the ASCII armored data of the public key
+ $armor = PGP::exportPublicKey($user, $key);
+
+ [$local, $domain] = Utils::normalizeAddress($email, true);
+
+ DB::beginTransaction();
+
+ $domain = Domain::firstOrCreate([
+ 'name' => '_woat.' . $domain,
+ ]);
+
+ $domain->records()->create([
+ 'name' => sha1($local) . '.' . $domain->name,
+ 'type' => 'TXT',
+ 'content' => 'v=woat1,public_key=' . $armor,
+ ]);
+
+ DB::commit();
+ }
+
+ /**
+ * Remove the key from the system (WOAT, HKP)
+ */
+ public static function deleteKey(User $user, string $email): void
+ {
+ [$local, $domain] = Utils::normalizeAddress($email, true);
+
+ $domain = Domain::where('name', '_woat.' . $domain)->first();
+
+ if ($domain) {
+ $fqdn = sha1($local) . '.' . $domain->name;
+
+ // For now we support only one WOAT key record
+ $domain->records()->where('name', $fqdn)->delete();
+ }
+ }
+}
diff --git a/src/app/Http/Controllers/KeyserverController.php b/src/app/Http/Controllers/KeyserverController.php
new file mode 100644
--- /dev/null
+++ b/src/app/Http/Controllers/KeyserverController.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Backends\PGP\Key;
+use Illuminate\Http\Request;
+use Illuminate\Http\Response;
+
+class KeyserverController extends Controller
+{
+ /**
+ * HKP lookup
+ *
+ * Partial implementation of HKP v1 lookups
+ * https://www.ietf.org/archive/id/draft-gallagher-openpgp-hkp-10.html#legacy-lookups
+ */
+ public function lookupHKPv1(Request $request): Response
+ {
+ // Note: We use the same limitations as Hagrid, see https://keys.openpgp.org/about/api
+ // - Only exact matches by email address, fingerprint or long key id are returned.
+ // - All requests return either one or no keys.
+ // - The expiration date field in op=index is left blank (?).
+ // - All parameters and options other than op and search are ignored.
+ // - Output is always machine readable (i.e. options=mr is always assumed).
+ // - We return public key as is (no filtering of extra packets).
+
+ // TODO: Support op=index and op=vindex, Roundcube manual key search uses that
+
+ if ($request->op != 'get') {
+ return response("Not supported 'op' parameter value", 501)
+ ->header('Access-Control-Allow-Origin', '*');
+ }
+
+ if (!is_string($request->search) || !strlen($request->search)) {
+ return response("Empty or invalid 'search' parameter value", 422)
+ ->header('Access-Control-Allow-Origin', '*');
+ }
+
+ // Find the key
+ if (str_contains($request->search, '@')) {
+ // by email address
+ $key = Key::findByEmail($request->search);
+ } else {
+ // by a key ID or fingerprint:
+ // - A hexadecimal representation of a long KeyID (e.g., 069C0C348DD82C19, optionally prefixed by 0x).
+ // This may be a KeyID of either a primary key or a subkey.
+ // - A hexadecimal representation of a Fingerprint (e.g., 8E8C33FA4626337976D97978069C0C348DD82C19, optionally prefixed by 0x).
+ // This may be a Fingerprint of either a primary key or a subkey.
+
+ $search = str_replace('0x', '', $request->search);
+ $key = Key::findById($search);
+ }
+
+ if (empty($key)) {
+ return response('Not found', 404)->header('Access-Control-Allow-Origin', '*');
+ }
+
+ // Key output is ascii-armored
+ return response($key->content, 200)
+ ->header('Access-Control-Allow-Origin', '*')
+ ->header('Last-Modified', $key->updated_at->format(\DateTime::RFC7231))
+ ->header('Content-Type', 'application/pgp-keys');
+ }
+}
diff --git a/src/config/pgp.php b/src/config/pgp.php
--- a/src/config/pgp.php
+++ b/src/config/pgp.php
@@ -15,4 +15,9 @@
// Default size of the new RSA key
'length' => (int) env('PGP_LENGTH', 3072),
+
+ // Enables services:
+ // - WOAT: Keys stored in built-in DNS database
+ // - HKP: Built-in HTTP key server(s)
+ 'services' => \explode(',', \strtolower(env('PGP_SERVICES', ''))),
];
diff --git a/src/database/migrations/2026_05_01_100000_create_pgp_keys_tables.php b/src/database/migrations/2026_05_01_100000_create_pgp_keys_tables.php
new file mode 100644
--- /dev/null
+++ b/src/database/migrations/2026_05_01_100000_create_pgp_keys_tables.php
@@ -0,0 +1,73 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration {
+ /**
+ * Run the migrations.
+ */
+ public function up(): void
+ {
+ Schema::create(
+ 'pgp_keys',
+ static function (Blueprint $table) {
+ // key ID is the lowest 64 bits of a fingerprint (last 16 characters in a hex fingerprint)
+ // Note: Can't use integer because in PHP int is signed
+ $table->string('id', 16)->primary();
+ // v4 key fingerprint is 40-char, but v6 is 64-char
+ // $table->string('fingerprint', 64)->index();
+ $table->bigInteger('user_id')->index();
+ $table->mediumText('content');
+ $table->timestamp('created_at')->useCurrent();
+ $table->timestamp('updated_at')->useCurrent();
+
+ $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade')->onUpdate('cascade');
+ }
+ );
+
+ Schema::create(
+ 'pgp_subkeys',
+ static function (Blueprint $table) {
+ // subkey ID is the lowest 64 bits of a fingerprint (last 16 characters in a hex fingerprint)
+ // Note: Can't use integer because in PHP int is signed
+ $table->string('id', 16)->primary();
+ // v4 key fingerprint is 40-char, but v6 is 64-char
+ // $table->string('fingerprint', 64)->index();
+ $table->string('key_id', 16);
+ $table->timestamp('created_at')->useCurrent();
+ $table->timestamp('updated_at')->useCurrent();
+
+ $table->unique(['key_id', 'id']);
+
+ $table->foreign('key_id')->references('id')->on('pgp_keys')->onDelete('cascade')->onUpdate('cascade');
+ }
+ );
+
+ Schema::create(
+ 'pgp_identities',
+ static function (Blueprint $table) {
+ $table->bigIncrements('id');
+ $table->string('key_id', 16);
+ $table->string('email')->index();
+ $table->timestamp('created_at')->useCurrent();
+ $table->timestamp('updated_at')->useCurrent();
+
+ $table->unique(['key_id', 'email']);
+
+ $table->foreign('key_id')->references('id')->on('pgp_keys')->onDelete('cascade')->onUpdate('cascade');
+ }
+ );
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('pgp_identities');
+ Schema::dropIfExists('pgp_subkeys');
+ Schema::dropIfExists('pgp_keys');
+ }
+};
diff --git a/src/phpunit.xml b/src/phpunit.xml
--- a/src/phpunit.xml
+++ b/src/phpunit.xml
@@ -43,6 +43,7 @@
<server name="SESSION_DRIVER" value="array"/>
<server name="SWOOLE_HTTP_ACCESS_LOG" value="false"/>
<server name="PGP_LENGTH" value="1024"/>
+ <server name="PGP_SERVICES" value="hkp,woat"/>
<server name="DAV_WITH_DEFAULT_FOLDERS" value="false"/>
<server name="IMAP_CREATE_EXCEPTION_DISABLE" value="false"/>
</php>
diff --git a/src/routes/web.php b/src/routes/web.php
--- a/src/routes/web.php
+++ b/src/routes/web.php
@@ -71,3 +71,8 @@
}
Controllers\DiscoveryController::registerRoutes();
+
+if (in_array('hkp', \config('pgp.services'))) {
+ Route::get('/pks/lookup', [Controllers\KeyserverController::class, 'lookupHKPv1']);
+ // Route::get('/pks/v2/certs/{mode}/{search}', [Controllers\KeyserverController::class, 'lookupHKPv2']);
+}
diff --git a/src/tests/Feature/Backends/PGPTest.php b/src/tests/Feature/Backends/PGPTest.php
--- a/src/tests/Feature/Backends/PGPTest.php
+++ b/src/tests/Feature/Backends/PGPTest.php
@@ -3,6 +3,7 @@
namespace Tests\Feature\Backends;
use App\Backends\PGP;
+use App\Backends\PGP\Key;
use App\Backends\Roundcube;
use App\PowerDNS\Domain;
use Illuminate\Support\Facades\Queue;
@@ -18,6 +19,7 @@
$user->aliases()->where('alias', 'test-alias@kolab.org')->delete();
PGP::homedirCleanup($user);
Domain::where('name', '_woat.kolab.org')->delete();
+ Key::query()->delete();
}
protected function tearDown(): void
@@ -26,18 +28,20 @@
$user->aliases()->where('alias', 'test-alias@kolab.org')->delete();
PGP::homedirCleanup($user);
Domain::where('name', '_woat.kolab.org')->delete();
+ Key::query()->delete();
parent::tearDown();
}
/**
- * Test key pair, listing and deletion creation.
+ * Test key pair creation, listing and deletion.
*
* @group pgp
* @group roundcube
*/
public function testKeyCreateListDelete(): void
{
+ \config(['pgp.services' => ['woat', 'hkp']]);
Queue::fake();
$user = $this->getTestUser('john@kolab.org');
@@ -56,6 +60,7 @@
$keys = PGP::listKeys($user);
$this->assertCount(1, $keys);
+ $keyId = Key::fingerprintId((string) $keys[0]);
$userIds = $keys[0]->getUserIds();
$this->assertCount(1, $userIds);
$this->assertSame($user->email, $userIds[0]->getEmail());
@@ -81,7 +86,7 @@
$this->assertTrue($key->canEncrypt());
$this->assertFalse($key->isRevoked());
- // Assert the public key in DNS
+ // WOAT: Assert the public key in DNS
$dns_domain = Domain::where('name', '_woat.kolab.org')->first();
$this->assertNotNull($dns_domain);
$dns_record = $dns_domain->records()->where('type', 'TXT')->first();
@@ -97,6 +102,19 @@
$dns_record->content
);
+ // HKP: Keyserver
+ $key = Key::find($keyId);
+ $this->assertInstanceOf(Key::class, $key);
+ $this->assertMatchesRegularExpression(
+ '/-----BEGIN PGP PUBLIC KEY BLOCK-----[a-zA-Z0-9\n\/+=]+-----END PGP PUBLIC KEY BLOCK-----$/',
+ $key->content
+ );
+ $this->assertCount(1, $key_users = $key->identities()->get());
+ $this->assertSame($user->email, $key_users[0]->email);
+ $subkeys = $keys[0]->getSubKeys();
+ $this->assertCount(1, $key_subkeys = $key->subkeys()->get());
+ $this->assertSame(Key::fingerprintId($keys[0]->getSubKeys()[1]->getFingerprint()), $key_subkeys[0]->id);
+
// Test an alias
$alias = $user->aliases()->create(['alias' => 'test-alias@kolab.org']);
PGP::keypairCreate($user, $alias->alias);
@@ -130,12 +148,19 @@
$this->assertTrue($key->canEncrypt());
$this->assertFalse($key->isRevoked());
+ // WOAT
$this->assertSame(2, $dns_domain->records()->where('type', 'TXT')->count());
+ // TODO: HKP
+
// Delete the key
PGP::keyDelete($user, $user->email);
- $this->assertSame(0, $dns_domain->records()->where('type', 'TXT')->count());
$this->assertCount(0, PGP::listKeys($user));
+
+ // WOAT
+ $this->assertSame(0, $dns_domain->records()->where('type', 'TXT')->count());
+
+ // TODO: HKP
}
}
diff --git a/src/tests/Feature/Controller/KeyserverTest.php b/src/tests/Feature/Controller/KeyserverTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Controller/KeyserverTest.php
@@ -0,0 +1,111 @@
+<?php
+
+namespace Tests\Feature\Controller;
+
+use App\Backends\PGP\Key;
+use Tests\TestCase;
+
+/**
+ * @group pgp
+ */
+class KeyserverTest extends TestCase
+{
+ protected function setUp(): void
+ {
+ parent::setUp();
+
+ $this->deleteTestUser('keyserver@kolab.org');
+ }
+
+ protected function tearDown(): void
+ {
+ $this->deleteTestUser('keyserver@kolab.org');
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test HKPv1 lookups
+ */
+ public function testLookupHKPv1(): void
+ {
+ $user = $this->getTestUser('keyserver@kolab.org');
+
+ $this->get('pks/lookup')
+ ->assertStatus(501)
+ ->assertHeader('Access-Control-Allow-Origin', '*');
+
+ $this->get('pks/lookup?op=stats')
+ ->assertStatus(501)
+ ->assertHeader('Access-Control-Allow-Origin', '*');
+
+ $this->get('pks/lookup?op=get')
+ ->assertStatus(422)
+ ->assertHeader('Access-Control-Allow-Origin', '*');
+
+ $this->get('pks/lookup?op=get&search=')
+ ->assertStatus(422)
+ ->assertHeader('Access-Control-Allow-Origin', '*');
+
+ $this->get('pks/lookup?op=get&search=0x1234')
+ ->assertStatus(404)
+ ->assertHeader('Access-Control-Allow-Origin', '*');
+
+ $this->get("pks/lookup?op=get&search={$user->email}")
+ ->assertStatus(404)
+ ->assertHeader('Access-Control-Allow-Origin', '*');
+
+ // Test success cases
+ $key = Key::create([
+ 'user_id' => $user->id,
+ 'id' => '12345678abcdef12',
+ 'content' => "-----BEGIN PGP PUBLIC KEY BLOCK-----\n123456789\n-----END PGP PUBLIC KEY BLOCK-----",
+ ]);
+ $subkey1 = $key->subkeys()->create(['id' => 'AABBCCDDAABBCCDD']);
+ $subkey2 = $key->subkeys()->create(['id' => '1122334455667788']);
+ $key->identities()->create(['email' => 'keyserver@kolab.org']);
+ $key->identities()->create(['email' => 'alias@kolab.org']);
+
+ // Search by email address
+ $this->get("pks/lookup?op=get&search={$user->email}")
+ ->assertStatus(200)
+ ->assertHeader('Access-Control-Allow-Origin', '*')
+ ->assertHeader('Last-Modified', $key->created_at->format(\DateTime::RFC7231))
+ ->assertHeader('Content-Type', 'application/pgp-keys')
+ ->assertContent($key->content);
+
+ // Search by the primary key ID
+ $this->get("pks/lookup?op=get&search={$key->id}")
+ ->assertStatus(200)
+ ->assertHeader('Access-Control-Allow-Origin', '*')
+ ->assertHeader('Last-Modified', $key->created_at->format(\DateTime::RFC7231))
+ ->assertHeader('Content-Type', 'application/pgp-keys')
+ ->assertContent($key->content);
+
+ // Search by the primary key ID (uppercase)
+ $this->get("pks/lookup?op=get&search=" . strtoupper($key->id))
+ ->assertStatus(200)
+ ->assertHeader('Access-Control-Allow-Origin', '*')
+ ->assertHeader('Last-Modified', $key->created_at->format(\DateTime::RFC7231))
+ ->assertHeader('Content-Type', 'application/pgp-keys')
+ ->assertContent($key->content);
+
+ // Search by the primary key fingerprint
+ $this->get("pks/lookup?op=get&search=AABBCCDDAABBCCDD{$key->id}")
+ ->assertStatus(200)
+ ->assertHeader('Access-Control-Allow-Origin', '*')
+ ->assertHeader('Last-Modified', $key->created_at->format(\DateTime::RFC7231))
+ ->assertHeader('Content-Type', 'application/pgp-keys')
+ ->assertContent($key->content);
+
+ // Search by the subkey key fingerprint
+ $this->get("pks/lookup?op=get&search=AABBCCDDAABBCCDD{$subkey2->id}")
+ ->assertStatus(200)
+ ->assertHeader('Access-Control-Allow-Origin', '*')
+ ->assertHeader('Last-Modified', $key->created_at->format(\DateTime::RFC7231))
+ ->assertHeader('Content-Type', 'application/pgp-keys')
+ ->assertContent($key->content);
+
+ // TODO: Test what happens when there are two keys for the same email address?
+ }
+}

File Metadata

Mime Type
text/plain
Expires
Fri, May 8, 1:08 PM (1 w, 6 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18922724
Default Alt Text
D5880.1778245727.diff (27 KB)

Event Timeline