Skip to content

Commit 5db5062

Browse files
committed
Refactor gpg import to use machine readable colon format
Signed-off-by: Aleksei Khudiakov <aleksey@xerkus.pro>
1 parent 3f7565f commit 5db5062

File tree

7 files changed

+401
-7
lines changed

7 files changed

+401
-7
lines changed

src/Gpg/ImportGpgKeyFromStringViaTemporaryFile.php

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,76 @@
44

55
namespace Laminas\AutomaticReleases\Gpg;
66

7+
use Laminas\AutomaticReleases\Gpg\Value\ColonFormattedKeyRecord;
78
use Psl;
89
use Psl\Env;
910
use Psl\Filesystem;
10-
use Psl\Regex;
1111
use Psl\Shell;
12+
use Psl\Str;
13+
use Psl\Vec;
1214

15+
use function array_shift;
16+
use function count;
1317
use function Psl\File\write;
1418

1519
final class ImportGpgKeyFromStringViaTemporaryFile implements ImportGpgKeyFromString
1620
{
1721
public function __invoke(string $keyContents): SecretKeyId
1822
{
1923
$keyFileName = Filesystem\create_temporary_file(Env\temp_dir(), 'imported-key');
20-
write($keyFileName, $keyContents);
24+
try {
25+
write($keyFileName, $keyContents);
2126

22-
$output = Shell\execute('gpg', ['--import', $keyFileName], null, [], Shell\ErrorOutputBehavior::Append);
27+
$output = Shell\execute(
28+
'gpg',
29+
['--import', '--import-options', 'import-show', '--with-colons', $keyFileName],
30+
null,
31+
[],
32+
Shell\ErrorOutputBehavior::Discard,
33+
);
2334

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

26-
Psl\invariant($matches !== null, 'unexpected output.');
42+
// Primary key secret is exported as unusable gnu-stub secret with --export-secret-subkeys.
43+
// Consequently primary key always has secret even when signing is done by subkey with actual secret.
44+
$primaryKeyRecords = Vec\filter(
45+
$keyRecords,
46+
static fn (ColonFormattedKeyRecord $record): bool => $record->isPrimaryKey() && $record->hasSecretKey(),
47+
);
2748

28-
Filesystem\delete_file($keyFileName);
49+
Psl\invariant(count($primaryKeyRecords) > 0, 'Imported GPG key material does not contain secret key');
50+
// import can contain multiple keys. Sanity check to ensure no unexpected key usage.
51+
Psl\invariant(
52+
count($primaryKeyRecords) === 1,
53+
'Imported GPG key material contains more than one primary key',
54+
);
2955

30-
return SecretKeyId::fromBase16String($matches[1]);
56+
$primaryKeyRecord = array_shift($primaryKeyRecords);
57+
58+
if ($primaryKeyRecord->hasSignCapability()) {
59+
return $primaryKeyRecord->keyId();
60+
}
61+
62+
$subkeyRecords = Vec\filter(
63+
$keyRecords,
64+
static function (ColonFormattedKeyRecord $record): bool {
65+
return $record->isSubkey() && $record->hasSecretKey() && $record->hasSignCapability();
66+
},
67+
);
68+
69+
Psl\invariant(
70+
count($subkeyRecords) > 0,
71+
'Imported GPG key material does not contain primary key or subkey with sign capabilities',
72+
);
73+
74+
return $primaryKeyRecord->keyId();
75+
} finally {
76+
Filesystem\delete_file($keyFileName);
77+
}
3178
}
3279
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Laminas\AutomaticReleases\Gpg\Value;
6+
7+
use Laminas\AutomaticReleases\Gpg\SecretKeyId;
8+
use Psl\Str;
9+
10+
use function in_array;
11+
use function str_contains;
12+
13+
final readonly class ColonFormattedKeyRecord
14+
{
15+
private const FIELD_TYPE = 0;
16+
private const FIELD_KEYID = 4;
17+
private const FIELD_CAPABILITIES = 11;
18+
19+
private function __construct(
20+
private bool $isSubkey,
21+
private bool $hasSecretKey,
22+
private SecretKeyId $keyId,
23+
private string $capabilities,
24+
) {
25+
}
26+
27+
public static function fromRecordLine(string $recordLine): self|null
28+
{
29+
$record = Str\split($recordLine, ':');
30+
$type = $record[self::FIELD_TYPE] ?? '';
31+
if (! in_array($type, ['pub', 'sec', 'sub', 'ssb'])) {
32+
return null;
33+
}
34+
35+
$isSubkey = in_array($type, ['sub', 'ssb']);
36+
$hasSecretKey = in_array($type, ['sec', 'ssb']);
37+
$keyId = SecretKeyId::fromBase16String($record[self::FIELD_KEYID] ?? '');
38+
$capabilities = $record[self::FIELD_CAPABILITIES] ?? '';
39+
40+
return new self($isSubkey, $hasSecretKey, $keyId, $capabilities);
41+
}
42+
43+
public function isPrimaryKey(): bool
44+
{
45+
return ! $this->isSubkey;
46+
}
47+
48+
public function isSubkey(): bool
49+
{
50+
return $this->isSubkey;
51+
}
52+
53+
public function hasSecretKey(): bool
54+
{
55+
return $this->hasSecretKey;
56+
}
57+
58+
public function keyId(): SecretKeyId
59+
{
60+
return $this->keyId;
61+
}
62+
63+
public function hasSignCapability(): bool
64+
{
65+
return str_contains($this->capabilities, 's');
66+
}
67+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
-----BEGIN PGP PUBLIC KEY BLOCK-----
2+
3+
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
35+
=qlLW
36+
-----END PGP PUBLIC KEY BLOCK-----

test/asset/dummy-gpg-no-sign.asc

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
-----BEGIN PGP PRIVATE KEY BLOCK-----
2+
3+
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
62+
=cI7Z
63+
-----END PGP PRIVATE KEY BLOCK-----
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
-----BEGIN PGP PRIVATE KEY BLOCK-----
2+
3+
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
49+
=VmJU
50+
-----END PGP PRIVATE KEY BLOCK-----

test/unit/Gpg/ImportGpgKeyFromStringViaTemporaryFileTest.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
use Laminas\AutomaticReleases\Gpg\ImportGpgKeyFromStringViaTemporaryFile;
88
use Laminas\AutomaticReleases\Gpg\SecretKeyId;
99
use PHPUnit\Framework\TestCase;
10+
use Psl\Exception\InvariantViolationException;
11+
use Psl\Shell\Exception\FailedExecutionException;
1012

1113
use function Psl\File\read;
1214

@@ -21,4 +23,35 @@ public function testWillImportValidGpgKey(): void
2123
->__invoke(read(__DIR__ . '/../../asset/dummy-gpg-key.asc')),
2224
);
2325
}
26+
27+
public function testWillImportGpgKeyWithValidSubkey(): void
28+
{
29+
self::assertEquals(
30+
SecretKeyId::fromBase16String('8CA5C026AE941316'),
31+
(new ImportGpgKeyFromStringViaTemporaryFile())
32+
->__invoke(read(__DIR__ . '/../../asset/dummy-gpg-only-subkey.asc')),
33+
);
34+
}
35+
36+
public function testWillFailOnNoSecretKey(): void
37+
{
38+
$this->expectException(InvariantViolationException::class);
39+
$this->expectExceptionMessage('Imported GPG key material does not contain secret key');
40+
(new ImportGpgKeyFromStringViaTemporaryFile())
41+
->__invoke(read(__DIR__ . '/../../asset/dummy-gpg-key-no-secret.asc'));
42+
}
43+
44+
public function testWillFailOnMissingSignCapabilities(): void
45+
{
46+
$this->expectException(InvariantViolationException::class);
47+
$this->expectExceptionMessage('Imported GPG key material does not contain primary key or subkey with sign capabilities');
48+
(new ImportGpgKeyFromStringViaTemporaryFile())
49+
->__invoke(read(__DIR__ . '/../../asset/dummy-gpg-no-sign.asc'));
50+
}
51+
52+
public function testWillFailOnInvalidGpgKey(): void
53+
{
54+
$this->expectException(FailedExecutionException::class);
55+
(new ImportGpgKeyFromStringViaTemporaryFile())->__invoke('-----BEGIN PGP PRIVATE KEY BLOCK-----');
56+
}
2457
}

0 commit comments

Comments
 (0)