GitSignatureCheck   A
last analyzed

Complexity

Total Complexity 25

Size/Duplication

Total Lines 211
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 1

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 25
lcom 2
cbo 1
dl 0
loc 211
ccs 88
cts 88
cp 1
rs 10
c 0
b 0
f 0

10 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 23 1
A fromGitCommitCheck() 0 22 3
A fromGitTagCheck() 0 22 3
B asHumanReadableString() 0 17 8
A canBeTrusted() 0 4 1
A extractCommitHash() 0 13 2
A extractTagName() 0 13 2
A extractKeyIdentifier() 0 13 2
A extractSignatureAuthor() 0 13 2
A signatureValidationHasNoWarnings() 0 9 1
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 10
    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 10
        $this->packageName     = $packageName; // @TODO get rid of this, or add it to the error messages
82 10
        $this->commitHash      = $commitHash;
83 10
        $this->tagName         = $tagName;
84 10
        $this->command         = $command;
85 10
        $this->exitCode        = $exitCode;
86 10
        $this->output          = $output;
87 10
        $this->isSigned        = $isSigned;
88 10
        $this->isVerified      = $isVerified;
89 10
        $this->signatureAuthor = $signatureAuthor;
90 10
        $this->signatureKey    = $signatureKey;
91 10
    }
92
93 5
    public static function fromGitCommitCheck(
94
        PackageInterface $package,
95
        string $command,
96
        int $exitCode,
97
        string $output
98
    ) : self {
99 5
        $signatureKey = self::extractKeyIdentifier($output);
100 5
        $signed       = $signatureKey && ! $exitCode;
101
102 5
        return new self(
103 5
            $package->getName(),
104 5
            self::extractCommitHash($output),
105 5
            null,
106 5
            $command,
107 5
            $exitCode,
108 5
            $output,
109 5
            $signed,
110 5
            $signed && self::signatureValidationHasNoWarnings($output),
111 5
            self::extractSignatureAuthor($output),
112 5
            $signatureKey
113
        );
114
    }
115
116 5
    public static function fromGitTagCheck(
117
        PackageInterface $package,
118
        string $command,
119
        int $exitCode,
120
        string $output
121
    ) : self {
122 5
        $signatureKey = self::extractKeyIdentifier($output);
123 5
        $signed       = $signatureKey && ! $exitCode;
124
125 5
        return new self(
126 5
            $package->getName(),
127 5
            self::extractCommitHash($output),
128 5
            self::extractTagName($output),
129 5
            $command,
130 5
            $exitCode,
131 5
            $output,
132 5
            $signed,
133 5
            $signed && self::signatureValidationHasNoWarnings($output),
134 5
            self::extractSignatureAuthor($output),
135 5
            self::extractKeyIdentifier($output)
136
        );
137
    }
138
139 10
    public function asHumanReadableString() : string
140
    {
141 10
        return implode(
142 10
            "\n",
143
            [
144 10
                (($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 10
                . ' ' . ($this->isVerified ? '[VERIFIED]' : '[NOT VERIFIED]')
146 10
                . ' ' . ($this->commitHash ? 'Commit #' . $this->commitHash : '')
147 10
                . ' ' . ($this->tagName ? 'Tag ' . $this->tagName : '')
148 10
                . ' ' . ($this->signatureAuthor ? 'By "' . $this->signatureAuthor . '"' : '')
149 10
                . ' ' . ($this->signatureKey ? '(Key ' . $this->signatureKey . ')' : ''),
150 10
                'Command: ' . $this->command,
151 10
                'Exit code: ' . $this->exitCode,
152 10
                'Output: ' . $this->output,
153
            ]
154
        );
155
    }
156
157 10
    public function canBeTrusted() : bool
158
    {
159 10
        return $this->isVerified;
160
    }
161
162 10
    private static function extractCommitHash(string $output) : ?string
163
    {
164 10
        $keys = array_filter(array_map(
165
            function (string $outputRow) {
166 10
                preg_match('/^(tree|object) ([a-fA-F0-9]{40})$/i', $outputRow, $matches);
167
168 10
                return $matches[2] ?? false;
169 10
            },
170 10
            explode("\n", $output)
171
        ));
172
173 10
        return reset($keys) ?: null;
174
    }
175
176 5
    private static function extractTagName(string $output) : ?string
177
    {
178 5
        $keys = array_filter(array_map(
179
            function (string $outputRow) {
180 5
                preg_match('/^tag (.+)$/i', $outputRow, $matches);
181
182 5
                return $matches[1] ?? false;
183 5
            },
184 5
            explode("\n", $output)
185
        ));
186
187 5
        return reset($keys) ?: null;
188
    }
189
190 10
    private static function extractKeyIdentifier(string $output) : ?string
191
    {
192 10
        $keys = array_filter(array_map(
193
            function (string $outputRow) {
194 10
                preg_match('/gpg:.*using .* key ([a-fA-F0-9]+)/i', $outputRow, $matches);
195
196 10
                return $matches[1] ?? false;
197 10
            },
198 10
            explode("\n", $output)
199
        ));
200
201 10
        return reset($keys) ?: null;
202
    }
203
204 10
    private static function extractSignatureAuthor(string $output) : ?string
205
    {
206 10
        $keys = array_filter(array_map(
207
            function (string $outputRow) {
208 10
                preg_match('/gpg: Good signature from "(.+)" \\[.*\\]/i', $outputRow, $matches);
209
210 10
                return $matches[1] ?? false;
211 10
            },
212 10
            explode("\n", $output)
213
        ));
214
215 10
        return reset($keys) ?: null;
216
    }
217
218 4
    private static function signatureValidationHasNoWarnings(string $output) : bool
219
    {
220 4
        return ! array_filter(
221 4
            explode("\n", $output),
222 4
            function (string $outputRow) {
223 4
                return false !== strpos($outputRow, 'gpg: WARNING: ');
224 4
            }
225
        );
226
    }
227
}
228