Passed
Push — master ( 50ed2d...29d863 )
by Russell
05:06
created

VerifiableAdminController::getProofViz()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 8
dl 0
loc 14
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 2
1
<?php
2
3
/**
4
 * @author  Russell Michell 2018 <[email protected]>
5
 * @package silverstripe-verifiable
6
 */
7
8
namespace PhpTek\Verifiable\Controller;
9
10
use SilverStripe\Control\Controller;
11
use SilverStripe\Control\HTTPRequest;
12
use SilverStripe\Versioned\Versioned;
13
use SilverStripe\ORM\ValidationException;
14
use SilverStripe\Security\Security;
15
use SilverStripe\Security\Permission;
16
use PhpTek\Verifiable\ORM\FieldType\ChainpointProof;
17
use PhpTek\Verifiable\Model\VerifiableExtension;
18
use PhpTek\Verifiable\Exception\VerifiableBackendException;
19
use Dcentrica\Viz\ChainpointViz;
20
21
/**
22
 * Accepts incoming requests for data verification e.g. from within the CMS
23
 * or framework's admin area, proxies them through {@link VerifiableService} and
24
 * sends them on their way.
25
 *
26
 * Will proxy validation requests to the currently configured backend for both
27
 * {@link SiteTree} and {@link DataObject} subclasses.
28
 */
29
class VerifiableAdminController extends Controller
30
{
31
    /**
32
     * No local proof was found for this version. If this is the first version,
33
     * you can safely ignore this message. Otherwise, this is evidence that this
34
     * version has been tampered-with.
35
     *
36
     * @var string
37
     */
38
    const STATUS_LOCAL_PROOF_NONE = 'Local Proof Not Found';
39
40
    /**
41
     * One or more key components of the local proof, were found to be invalid.
42
     * Evidence that the record has been tampered-with.
43
     *
44
     * @var string
45
     */
46
    const STATUS_LOCAL_COMPONENT_INVALID = 'Local Components Invalid';
47
48
    /**
49
     * A mismatch exists between the stored hash for this version, and the data the
50
     * hash was generated from. Evidence that the record has been tampered-with.
51
     *
52
     * @var string
53
     */
54
    const STATUS_LOCAL_HASH_INVALID = 'Local Hash Invalid';
55
56
    /**
57
     * All verification checks passed. This version's hash and proof are intact and verified.
58
     *
59
     * @var string
60
     */
61
    const STATUS_VERIFIED_OK = 'Verified';
62
63
    /**
64
     * Some or all verification checks failed. This version's hash and proof are not intact.
65
     * Evidence that the record has been tampered-with.
66
     *
67
     * @var string
68
     */
69
    const STATUS_VERIFIED_FAIL = 'Verification failure';
70
71
    /**
72
     * This version is unverified. If this state persists, something is not working
73
     * correctly. Please consult your developer.
74
     *
75
     * @var string
76
     */
77
    const STATUS_UNVERIFIED = 'Unverified';
78
79
    /**
80
     * This version's hash confirmation is currently pending. If it's been more than
81
     * two hours since submission, try again.
82
     *
83
     * @var string
84
     */
85
    const STATUS_PENDING = 'Pending';
86
87
    /**
88
     * This version's hash confirmation is currently awaiting processing. If it's
89
     * been more than two hours since submission, please check the automated update job.
90
     * Consult your developer..
91
     *
92
     * @var string
93
     */
94
    const STATUS_INITIAL = 'Initial';
95
96
    /**
97
     * The verification process encountered a network error communicating with the
98
     * backend. Try again in a moment.
99
     *
100
     * @var string
101
     */
102
    const STATUS_UPSTREAM_ERROR = 'Upstream Error';
103
104
    /**
105
     * @var array
106
     */
107
    private static $allowed_actions = [
0 ignored issues
show
introduced by
The private property $allowed_actions is not used, and could be removed.
Loading history...
108
        'verifyhash',
109
    ];
110
111
    /**
112
     * Verify the integrity of arbitrary data by means of a single hash.
113
     *
114
     * Responds to URIs of the following prototype: /verifiable/verify/<model>/<ID>/<VID>
115
     * by echoing a JSON response for consumption by client-side logic.
116
     *
117
     * @param  HTTPRequest $request
118
     * @return void
119
     */
120
    public function verifyhash(HTTPRequest $request)
121
    {
122
        if (!Permission::checkMember(Security::getCurrentUser(), 'ADMIN')) {
123
            return $this->httpError(401, 'Unauthorised');
124
        }
125
126
        $class = $request->param('ClassName');
127
        $id = $request->param('ModelID');
128
        $version = $request->param('VersionID');
129
        $verificationData = [];
130
131
        if (empty($id) || !is_numeric($id) ||
132
                empty($version) || !is_numeric($version) ||
133
                empty($class)) {
134
            return $this->httpError(400, 'Bad request');
135
        }
136
137
        if (!$record = Versioned::get_version($class, $id, $version)) {
0 ignored issues
show
Bug introduced by
$version of type string is incompatible with the type integer expected by parameter $version of SilverStripe\Versioned\Versioned::get_version(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

137
        if (!$record = Versioned::get_version($class, $id, /** @scrutinizer ignore-type */ $version)) {
Loading history...
Bug introduced by
$id of type string is incompatible with the type integer expected by parameter $id of SilverStripe\Versioned\Versioned::get_version(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

137
        if (!$record = Versioned::get_version($class, /** @scrutinizer ignore-type */ $id, $version)) {
Loading history...
138
            return $this->httpError(400, 'Bad request');
139
        }
140
141
        try {
142
            $status = $this->getStatus($record, $record->getExtraByIndex(), $verificationData);
143
        } catch (ValidationException $ex) {
144
            $status = self::STATUS_UPSTREAM_ERROR;
145
        }
146
147
        $response = json_encode([
148
            'Record' => [
149
                'RecordID' => "$record->RecordID",
150
                'CreatedDate' => self::display_date($record->Created),
151
                'Version' => "$record->Version",
152
                'Class' => get_class($record),
153
                'VerifiableFields' => $record->sourceMode() === VerifiableExtension::SOURCE_MODE_FIELD ?
154
                    json_decode($record->VerifiableFields) :
155
                    [],
156
            ],
157
            'Status' => [
158
                'Nice' => $status,
159
                'Code' => $this->getCodeMeta($status, 'code'),
160
                'Def' => $this->getCodeMeta($status, 'defn'),
161
            ],
162
            'Proof' => [
163
                'SubmittedDate' => self::display_date($verificationData['SubmittedAt'] ?? ''),
164
                'SubmittedTo' => $record->dbObject('Extra')->getStoreAsArray(),
165
                'ChainpointProof' => $verificationData['ChainpointProof'] ?? '',
166
                'ChainpointViz' => $verificationData['ChainpointViz'] ?? '',
167
                'MerkleRoot' => $verificationData['MerkleRoot'] ?? '',
168
                'BlockHeight' => $verificationData['BlockHeight'] ?? '',
169
                'Hashes' => $verificationData['Hashes'] ?? '',
170
                'UUID' => $verificationData['UUID'] ?? '',
171
                'GV' => $verificationData['Dotfile'] ?? '',
172
                'TXID' => $verificationData['TXID'] ?? '',
173
                'OPRET' => $verificationData['OPRET'] ?? '',
174
            ]
175
        ], JSON_UNESCAPED_SLASHES);
176
177
        $this->renderJSON($response);
178
    }
179
180
    /**
181
     * Return data used for verifiable statuses.
182
     *
183
     * @param  string $status
184
     * @param  string $key
185
     * @return mixed
186
     */
187
    private function getCodeMeta($status, $key)
188
    {
189
        $refl = new \ReflectionClass(__CLASS__);
190
        $const = array_search($status, $refl->getConstants());
191
        $keyJson = file_get_contents(realpath(__DIR__) . '/../../statuses.json');
192
        $keyMap = json_decode($keyJson, true);
193
        $defn = '';
194
195
        foreach ($keyMap as $map) {
196
            if (isset($map[$const])) {
197
                $defn = $map[$const];
198
            }
199
        }
200
201
        $data = [
202
            'code' => $const,
203
            'defn' => $defn,
204
        ];
205
206
        return $data[$key] ?? $data;
207
    }
208
209
    /**
210
     * Gives us the current verification status of the given record. Takes into
211
     * account the state of the saved proof as well as by making a backend
212
     * verification call.
213
     *
214
     * @param  DataObject $record           The versioned record we're checking.
0 ignored issues
show
Bug introduced by
The type PhpTek\Verifiable\Controller\DataObject was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
215
     * @param  array      $nodes            Array of cached chainpoint node IPs.
216
     * @param  array      $verificationData Array of data for manual verification.
217
     * @return string
218
     */
219
    public function getStatus($record, $nodes, &$verificationData) : string
220
    {
221
        // Set some extra data on the service. In this case, the actual chainpoint
222
        // node addresses, used to submit hashes for the given $record
223
        $this->service->setExtra($nodes);
0 ignored issues
show
Bug introduced by
The method setExtra() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

223
        $this->service->/** @scrutinizer ignore-call */ 
224
                        setExtra($nodes);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug Best Practice introduced by
The property service does not exist on PhpTek\Verifiable\Contro...rifiableAdminController. Since you implemented __get, consider adding a @property annotation.
Loading history...
224
        $proof = $record->dbObject('Proof');
225
226
        // Basic existence of proof
227
        if (!$proof->getValue()) {
228
            return self::STATUS_LOCAL_PROOF_NONE;
229
        }
230
231
        if ($proof->isInitial()) {
232
            return self::STATUS_INITIAL;
233
        }
234
235
        if ($proof->isPending()) {
236
            return self::STATUS_PENDING;
237
        }
238
239
        // So the saved proof claims to be full. Perform some rudimentary checks
240
        // before we send a full-blown verification request to the backend
241
        if ($proof->isFull()) {
242
            // Tests 3 of the key components of the local proof. Sending a verification
243
            // request will do this and much more for us, but rudimentary local checks
244
            // can prevent a network request
245
            if (!$proof->getHashIdNode() || !$proof->getProof() || !count($proof->getAnchorsComplete())) {
246
                return self::STATUS_LOCAL_COMPONENT_INVALID;
247
            }
248
249
            // We've got this far. The local proof seems to be good. Let's verify
250
            // it against the backend
251
            $response = $this->service->call('verify', $proof->getProof());
252
            $responseModel = ChainpointProof::create()->setValue($response);
253
            $isVerified = $responseModel->isVerified();
254
255
            if (!$isVerified) {
256
                return self::STATUS_VERIFIED_FAIL;
257
            }
258
259
            // OK, so we have an intact local full proof, let's ensure it still
260
            // matches a hash of the data it purports to represent
261
            $remoteHash = $responseModel->getHash();
262
            $reCalculated = $this->service->hash($record->source());
263
264
            if ($reCalculated !== $remoteHash) {
265
                return self::STATUS_LOCAL_HASH_INVALID;
266
            }
267
268
            $chainpointViz = new ChainpointViz($proof->getProofJson(), 'btc');
269
270
            // Setup data for display & manual re-verification
271
            $v3proof = ChainpointProof::create()->setValue($proof->getProofJson());
272
            $verificationData['ChainpointProof'] = $proof->getProofJson();
273
            $verificationData['MerkleRoot'] = $v3proof->getMerkleRoot('btc');
274
            $verificationData['BlockHeight'] = $v3proof->getBitcoinBlockHeight();
275
            $verificationData['UUID'] = $v3proof->getHashIdNode();
276
            $verificationData['TXID'] = $chainpointViz->getBtcTXID();
277
            $verificationData['OPRET'] = $chainpointViz->getBtcOpReturn();
278
            $verificationData['SubmittedAt'] = $v3proof->getSubmittedAt();
279
            $verificationData['Hashes'] = [
280
                'local' => $reCalculated,
281
                'remote' => $v3proof->getHash(),
282
            ];
283
            $verificationData['ChainpointViz'] = $this->getProofViz($chainpointViz);
284
285
            // All is well. As you were...
286
            return self::STATUS_VERIFIED_OK;
287
        }
288
289
        // Default status
290
        return self::STATUS_UNVERIFIED;
291
    }
292
293
    /**
294
     * Properly return JSON, allowing consumers to render returned JSON correctly.
295
     *
296
     * @param  string $json
297
     * @return void
298
     */
299
    private function renderJSON(string $json)
300
    {
301
        header('Content-Type: application/json');
302
        echo $json;
303
        exit(0);
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
304
    }
305
306
    /**
307
     * Common date format in ISO 8601.
308
     *
309
     * @return string
310
     */
311
    private static function display_date(string $date) : string
312
    {
313
        return date('Y-m-d H:i:s', strtotime($date));
314
    }
315
316
    /**
317
     * Generate a visualisation of a chainpoint proof.
318
     *
319
     * @param  ChainpointViz $viz         A passed ChainpointViz object to work with.
320
     * @param  string $format             Any format accepted by Graphviz.
321
     * @return string                     A URI path to the location of the generated graphic.
322
     */
323
    private function getProofViz(ChainpointViz $viz, $format = 'svg')
324
    {
325
        $fileName = sprintf('chainpoint.%s', $format);
326
        $filePath = sprintf('%s/%s', ASSETS_PATH, $fileName);
327
        $fileHref = sprintf('/%s/%s', ASSETS_DIR, $fileName);
328
329
        $viz->setFilename($filePath);
330
        $viz->visualize();
331
332
        if (!file_exists($filePath)) {
333
            return '';
334
        }
335
336
        return $fileHref;
337
    }
338
339
}
340