Completed
Push — master ( da1a6c...6bb072 )
by Marco
02:08
created

GitSignatureCheck::asHumanReadableString()   B

Complexity

Conditions 8
Paths 1

Size

Total Lines 17
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 17
rs 7.7777
c 0
b 0
f 0
cc 8
eloc 12
nc 1
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Roave\ComposerGpgVerify\Package\Git;
6
7
use Composer\Package\PackageInterface;
8
9
/**
10
 * @internal do not use: I will cut you.
11
 *
12
 * This class abstracts the parsing of the output of `git verify-commit` and `git tag -v`, which are broken/useless.
13
 * Ideally, we'd massively simplify this class once GIT 2.12 is mainstream
14
 *
15
 * @link https://stackoverflow.com/questions/17371955/verifying-signed-git-commits/32038784#32038784
16
 */
17
class GitSignatureCheck
18
{
19
    /**
20
     * @var string
21
     */
22
    private $packageName;
23
24
    /**
25
     * @var string
26
     */
27
    private $command;
28
29
    /**
30
     * @var string|null
31
     */
32
    private $commitHash;
33
34
    /**
35
     * @var string|null
36
     */
37
    private $tagName;
38
39
    /**
40
     * @var int
41
     */
42
    private $exitCode;
43
44
    /**
45
     * @var string
46
     */
47
    private $output;
48
49
    /**
50
     * @var bool
51
     */
52
    private $isSigned;
53
54
    /**
55
     * @var bool
56
     */
57
    private $isVerified;
58
59
    /**
60
     * @var string|null
61
     */
62
    private $signatureAuthor;
63
64
    /**
65
     * @var string|null
66
     */
67
    private $signatureKey;
68
69
    private function __construct(
70
        string $packageName,
71
        ?string $commitHash,
72
        ?string $tagName,
73
        string $command,
74
        int $exitCode,
75
        string $output,
76
        bool $isSigned,
77
        bool $isVerified,
78
        ?string $signatureAuthor,
79
        ?string $signatureKey
80
    ) {
81
        $this->packageName     = $packageName; // @TODO get rid of this, or add it to the error messages
82
        $this->commitHash      = $commitHash;
83
        $this->tagName         = $tagName;
84
        $this->command         = $command;
85
        $this->exitCode        = $exitCode;
86
        $this->output          = $output;
87
        $this->isSigned        = $isSigned;
88
        $this->isVerified      = $isVerified;
89
        $this->signatureAuthor = $signatureAuthor;
90
        $this->signatureKey    = $signatureKey;
91
    }
92
93
    public static function fromGitCommitCheck(
94
        PackageInterface $package,
95
        string $command,
96
        int $exitCode,
97
        string $output
98
    ) : self {
99
        $signatureKey = self::extractKeyIdentifier($output);
100
        $signed       = $signatureKey && ! $exitCode;
101
102
        return new self(
103
            $package->getName(),
104
            self::extractCommitHash($output),
105
            null,
106
            $command,
107
            $exitCode,
108
            $output,
109
            $signed,
110
            $signed && self::signatureValidationHasNoWarnings($output),
111
            self::extractSignatureAuthor($output),
112
            $signatureKey
113
        );
114
    }
115
116
    public static function fromGitTagCheck(
117
        PackageInterface $package,
118
        string $command,
119
        int $exitCode,
120
        string $output
121
    ) : self {
122
        $signatureKey = self::extractKeyIdentifier($output);
123
        $signed       = $signatureKey && ! $exitCode;
124
125
        return new self(
126
            $package->getName(),
127
            self::extractCommitHash($output),
128
            self::extractTagName($output),
129
            $command,
130
            $exitCode,
131
            $output,
132
            $signed,
133
            $signed && self::signatureValidationHasNoWarnings($output),
134
            self::extractSignatureAuthor($output),
135
            self::extractKeyIdentifier($output)
136
        );
137
    }
138
139
    public function asHumanReadableString() : string
140
    {
141
        return implode(
142
            "\n",
143
            [
144
                (($this->isSigned || $this->signatureKey) ? '[SIGNED]' : '[NOT SIGNED]')
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->signatureKey of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
145
                . ' ' . ($this->isVerified ? '[VERIFIED]' : '[NOT VERIFIED]')
146
                . ' ' . ($this->commitHash ? 'Commit #' . $this->commitHash : '')
147
                . ' ' . ($this->tagName ? 'Tag ' . $this->tagName : '')
148
                . ' ' . ($this->signatureAuthor ? 'By "' . $this->signatureAuthor . '"' : '')
149
                . ' ' . ($this->signatureKey ? '(Key ' . $this->signatureKey . ')' : ''),
150
                'Command: ' . $this->command,
151
                'Exit code: ' . $this->exitCode,
152
                'Output: ' . $this->output,
153
            ]
154
        );
155
    }
156
157
    public function canBeTrusted() : bool
158
    {
159
        return $this->isVerified;
160
    }
161
162
    private static function extractCommitHash(string $output) : ?string
163
    {
164
        $keys = array_filter(array_map(
165
            function (string $outputRow) {
166
                preg_match('/^(tree|object) ([a-fA-F0-9]{40})$/i', $outputRow, $matches);
167
168
                return $matches[2] ?? false;
169
            },
170
            explode("\n", $output)
171
        ));
172
173
        return reset($keys) ?: null;
174
    }
175
176
    private static function extractTagName(string $output) : ?string
177
    {
178
        $keys = array_filter(array_map(
179
            function (string $outputRow) {
180
                preg_match('/^tag (.+)$/i', $outputRow, $matches);
181
182
                return $matches[1] ?? false;
183
            },
184
            explode("\n", $output)
185
        ));
186
187
        return reset($keys) ?: null;
188
    }
189
190
    private static function extractKeyIdentifier(string $output) : ?string
191
    {
192
        $keys = array_filter(array_map(
193
            function (string $outputRow) {
194
                preg_match('/gpg:.*using .* key ([a-fA-F0-9]+)/i', $outputRow, $matches);
195
196
                return $matches[1] ?? false;
197
            },
198
            explode("\n", $output)
199
        ));
200
201
        return reset($keys) ?: null;
202
    }
203
204
    private static function extractSignatureAuthor(string $output) : ?string
205
    {
206
        $keys = array_filter(array_map(
207
            function (string $outputRow) {
208
                preg_match('/gpg: Good signature from "(.+)" \\[.*\\]/i', $outputRow, $matches);
209
210
                return $matches[1] ?? false;
211
            },
212
            explode("\n", $output)
213
        ));
214
215
        return reset($keys) ?: null;
216
    }
217
218
    private static function signatureValidationHasNoWarnings(string $output) : bool
219
    {
220
        return ! array_filter(
221
            explode("\n", $output),
222
            function (string $outputRow) {
223
                return false !== strpos($outputRow, 'gpg: WARNING: ');
224
            }
225
        );
226
    }
227
}
228