Skip to content

Commit

Permalink
Refactor gpg import to use machine readable colon format
Browse files Browse the repository at this point in the history
Signed-off-by: Aleksei Khudiakov <aleksey@xerkus.pro>
  • Loading branch information
Xerkus committed Feb 11, 2024
1 parent 3f7565f commit 592c7de
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 7 deletions.
58 changes: 51 additions & 7 deletions src/Gpg/ImportGpgKeyFromStringViaTemporaryFile.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,73 @@

namespace Laminas\AutomaticReleases\Gpg;

use Laminas\AutomaticReleases\Gpg\Value\ColonFormattedKeyRecord;
use Psl;
use Psl\Env;
use Psl\Filesystem;
use Psl\Regex;
use Psl\Shell;
use Psl\Str;
use Psl\Vec;

use function array_shift;
use function count;
use function Psl\File\write;

final class ImportGpgKeyFromStringViaTemporaryFile implements ImportGpgKeyFromString
{
public function __invoke(string $keyContents): SecretKeyId
{
$keyFileName = Filesystem\create_temporary_file(Env\temp_dir(), 'imported-key');
write($keyFileName, $keyContents);
try {
write($keyFileName, $keyContents);

$output = Shell\execute('gpg', ['--import', $keyFileName], null, [], Shell\ErrorOutputBehavior::Append);
$output = Shell\execute(
'gpg',
['--import', '--import-options', 'import-show', '--with-colons', $keyFileName],
null,
[],
Shell\ErrorOutputBehavior::Discard,
);

$matches = Regex\first_match($output, '/key\\s+([A-F0-9]+):\\s+secret\\s+key\\s+imported/im', Regex\capture_groups([1]));
$keyRecords = Vec\filter_nulls(Vec\map(
Str\split($output, "\n"),
static fn (string $record): ColonFormattedKeyRecord|null => ColonFormattedKeyRecord::fromRecordLine(
$record,
),
));

Psl\invariant($matches !== null, 'unexpected output.');
$primaryKeyRecords = Vec\filter(
$keyRecords,
static fn (ColonFormattedKeyRecord $record): bool => $record->isPrimaryKey(),
);

Filesystem\delete_file($keyFileName);
Psl\invariant(count($primaryKeyRecords) > 0, 'Imported GPG key material does not contain primary key');
Psl\invariant(
count($primaryKeyRecords) === 1,
'Imported GPG key material contains more than one primary key',
);

return SecretKeyId::fromBase16String($matches[1]);
$primaryKeyRecord = array_shift($primaryKeyRecords);

if ($primaryKeyRecord->hasSecretKey() && $primaryKeyRecord->hasSignCapability()) {
return $primaryKeyRecord->keyId();
}

$subkeyRecords = Vec\filter(
$keyRecords,
static function (ColonFormattedKeyRecord $record): bool {
return $record->isSubkey() && $record->hasSecretKey() && $record->hasSignCapability();
},
);

Psl\invariant(
count($subkeyRecords) > 0,
'Imported GPG key material does not contain secret key or subkey with sign capabilities',
);

return $primaryKeyRecord->keyId();
} finally {
Filesystem\delete_file($keyFileName);
}
}
}
67 changes: 67 additions & 0 deletions src/Gpg/Value/ColonFormattedKeyRecord.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

declare(strict_types=1);

namespace Laminas\AutomaticReleases\Gpg\Value;

use Laminas\AutomaticReleases\Gpg\SecretKeyId;
use Psl\Str;

use function in_array;
use function str_contains;

final readonly class ColonFormattedKeyRecord
{
private const FIELD_TYPE = 0;
private const FIELD_KEYID = 4;
private const FIELD_CAPABILITIES = 11;

private function __construct(
private bool $isSubkey,
private bool $hasSecretKey,
private SecretKeyId $keyId,
private string $capabilities,
) {
}

public static function fromRecordLine(string $recordLine): self|null
{
$record = Str\split($recordLine, ':');
$type = $record[self::FIELD_TYPE] ?? '';
if (! in_array($type, ['pub', 'sec', 'sub', 'ssb'])) {
return null;
}

$isSubkey = in_array($type, ['sub', 'ssb']);
$hasSecretKey = in_array($type, ['sec', 'ssb']);
$keyId = SecretKeyId::fromBase16String($record[self::FIELD_KEYID] ?? '');
$capabilities = $record[self::FIELD_CAPABILITIES] ?? '';

return new self($isSubkey, $hasSecretKey, $keyId, $capabilities);
}

public function isPrimaryKey(): bool
{
return ! $this->isSubkey;
}

public function isSubkey(): bool
{
return $this->isSubkey;
}

public function hasSecretKey(): bool
{
return $this->hasSecretKey;
}

public function keyId(): SecretKeyId
{
return $this->keyId;
}

public function hasSignCapability(): bool
{
return str_contains($this->capabilities, 's');
}
}

0 comments on commit 592c7de

Please sign in to comment.