VerifiableAdminController::getProofViz()   A
last analyzed

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

143
        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

143
        if (!$record = Versioned::get_version($class, /** @scrutinizer ignore-type */ $id, $version)) {
Loading history...
144
            return $this->httpError(400, 'Bad request');
145
        }
146
147
        try {
148
            $status = $this->getStatus($record, $record->getExtraByIndex(), $verificationData);
149
        } catch (ValidationException $ex) {
150
            $status = self::STATUS_UPSTREAM_ERROR;
151
        }
152
153
        $response = json_encode([
154
            'Record' => [
155
                'RecordID' => "$record->RecordID",
156
                'CreatedDate' => self::display_date($record->Created),
157
                'Version' => "$record->Version",
158
                'Class' => get_class($record),
159
                'VerifiableFields' => $record->verifiableFields(),
160
            ],
161
            'Status' => [
162
                'Nice' => $status,
163
                'Code' => $this->getCodeMeta($status, 'code'),
164
                'Def' => $this->getCodeMeta($status, 'defn'),
165
            ],
166
            'Proof' => [
167
                'SubmittedDate' => self::display_date($verificationData['SubmittedAt'] ?? ''),
168
                'SubmittedTo' => $record->dbObject('Extra')->getStoreAsArray(),
169
                'ChainpointProof' => $verificationData['ChainpointProof'] ?? '',
170
                'ChainpointViz' => $verificationData['ChainpointViz'] ?? '',
171
                'MerkleRoot' => $verificationData['MerkleRoot'] ?? '',
172
                'BlockHeight' => $verificationData['BlockHeight'] ?? '',
173
                'Hashes' => $verificationData['Hashes'] ?? '',
174
                'UUID' => $verificationData['UUID'] ?? '',
175
                'GV' => $verificationData['Dotfile'] ?? '',
176
                'TXID' => $verificationData['TXID'] ?? '',
177
                'OPRET' => $verificationData['OPRET'] ?? '',
178
            ]
179
        ], JSON_UNESCAPED_SLASHES);
180
181
        $this->renderJSON($response);
182
183
        return $response;
184
    }
185
186
    /**
187
     * Return data used for verifiable statuses.
188
     *
189
     * @param  string $status
190
     * @param  string $key
191
     * @return mixed
192
     */
193
    private function getCodeMeta($status, $key)
194
    {
195
        $refl = new \ReflectionClass(__CLASS__);
196
        $const = array_search($status, $refl->getConstants());
197
        $keyJson = file_get_contents(realpath(__DIR__) . '/../../statuses.json');
198
        $keyMap = json_decode($keyJson, true);
199
        $defn = '';
200
201
        foreach ($keyMap as $map) {
202
            if (isset($map[$const])) {
203
                $defn = $map[$const];
204
            }
205
        }
206
207
        $data = [
208
            'code' => $const,
209
            'defn' => $defn,
210
        ];
211
212
        return $data[$key] ?? $data;
213
    }
214
215
    /**
216
     * Centerpiece of verification controller requests. Gives us the current
217
     * verification status of the given record. Takes into account the state of
218
     * the saved proof as well as by making a backend verification call.
219
     *
220
     * @param  DataObject $record           The versioned record we're checking.
0 ignored issues
show
Bug introduced by
The type PhpTek\Verifiable\Control\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...
221
     * @param  array      $nodes            Array of cached chainpoint node IPs.
222
     * @param  array      $verificationData Array of data for manual verification.
223
     * @return string
224
     */
225
    public function getStatus($record, $nodes, &$verificationData) : string
226
    {
227
        // Set some extra data on the service. In this case, the actual chainpoint
228
        // node addresses, used to submit hashes for the given $record
229
        $this->service->setExtra($nodes);
0 ignored issues
show
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...
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

229
        $this->service->/** @scrutinizer ignore-call */ 
230
                        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...
230
        $proof = $record->dbObject('Proof');
231
232
        // Basic existence of proof
233
        if (!$proof->getValue()) {
234
            return self::STATUS_LOCAL_PROOF_NONE;
235
        }
236
237
        if ($proof->isInitial()) {
238
            return self::STATUS_INITIAL;
239
        }
240
241
        if ($proof->isPending()) {
242
            return self::STATUS_PENDING;
243
        }
244
245
        // So the saved proof claims to be full. Perform some rudimentary checks
246
        // before we send a full-blown verification request to the backend
247
        if ($proof->isFull()) {
248
            // Tests 3 of the key components of the local proof. Sending a verification
249
            // request will do this and much more for us, but rudimentary local checks
250
            // may prevent unnecessary network requests
251
            // TODO Integrate or port github.com/chainpoint/chainpoint-node-validator
252
            // and replace these local checks
253
            if (!$proof->getHashIdNode() || !$proof->getProof() || !count($proof->getAnchorsComplete())) {
254
                return self::STATUS_LOCAL_COMPONENT_INVALID;
255
            }
256
257
            // We've got this far. The local proof seems to be good. Let's verify
258
            // it against the backend
259
            $response = $this->service->call('verify', $proof->getProof());
260
            $responseModel = ChainpointProof::create()->setValue($response);
261
            $isVerified = $responseModel->isVerified();
262
263
            if (!$isVerified) {
264
                return self::STATUS_VERIFIED_FAIL;
265
            }
266
267
            // OK, so we have an intact local full proof, let's ensure it still
268
            // matches a hash of the data it purports to represent
269
            $remoteHash = $responseModel->getHash();
270
            $reCalculated = $this->service->hash($record->getSource());
271
272
            if ($reCalculated !== $remoteHash) {
273
                return self::STATUS_LOCAL_HASH_INVALID;
274
            }
275
276
            // Tight coupling with ChainpointViz and our
277
            // reliance on getBtc*() for the proper function of this controller
278
            $chainpointViz = $this->visualiser
279
                ->setReceipt($proof->getProofJson())
280
                ->setChain('btc');
281
282
            // Setup data for display & manual re-verification
283
            $v3proof = ChainpointProof::create()->setValue($proof->getProofJson());
284
            $verificationData['ChainpointProof'] = $proof->getProofJson();
285
            $verificationData['MerkleRoot'] = $v3proof->getMerkleRoot('btc');
286
            $verificationData['BlockHeight'] = $v3proof->getBitcoinBlockHeight();
287
            $verificationData['UUID'] = $v3proof->getHashIdNode();
288
            $verificationData['TXID'] = $chainpointViz->getBtcTXID();
289
            $verificationData['OPRET'] = $chainpointViz->getBtcOpReturn();
290
            $verificationData['ChainpointViz'] = $this->getProofViz($chainpointViz);
291
            $verificationData['SubmittedAt'] = $v3proof->getSubmittedAt();
292
            $verificationData['Hashes'] = [
293
                'local' => $reCalculated,
294
                'remote' => $v3proof->getHash(),
295
            ];
296
297
            // All is well. As you were...
298
            return self::STATUS_VERIFIED_OK;
299
        }
300
301
        // Default status
302
        return self::STATUS_UNVERIFIED;
303
    }
304
305
    /**
306
     * Properly return JSON, allowing consumers to render returned JSON correctly.
307
     *
308
     * @param  string $json
309
     * @return void
310
     */
311
    private function renderJSON(string $json)
312
    {
313
        header('Content-Type: application/json');
314
        echo $json;
315
        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...
316
    }
317
318
    /**
319
     * Common date format in ISO 8601.
320
     *
321
     * @return string
322
     */
323
    private static function display_date(string $date) : string
324
    {
325
        return date('Y-m-d H:i:s', strtotime($date));
326
    }
327
328
    /**
329
     * Generate a visualisation of a chainpoint proof.
330
     *
331
     * @param  ChainpointViz $viz         A passed ChainpointViz object to work with.
332
     * @param  string $format             Any format accepted by Graphviz.
333
     * @return string                     A URI path to the location of the generated graphic.
334
     */
335
    private function getProofViz(ChainpointViz $viz, $format = 'svg')
336
    {
337
        $fileName = sprintf('chainpoint.%s', $format);
338
        $filePath = sprintf('%s/%s', ASSETS_PATH, $fileName);
339
        $fileHref = sprintf('/%s/%s', ASSETS_DIR, $fileName);
340
341
        $viz->setFilename($filePath);
342
        $viz->visualise();
343
344
        if (!file_exists($filePath)) {
345
            return '';
346
        }
347
348
        return $fileHref;
349
    }
350
351
    /**
352
     * Simple setter.
353
     *
354
     * @param  ChainpointViz $visualiser
355
     * @return VerifiableAdminController
356
     */
357
    public function setVisualiser($visualiser) : VerifiableAdminController
358
    {
359
        $this->visualiser = $visualiser;
0 ignored issues
show
Bug Best Practice introduced by
The property visualiser does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
360
361
        return $this;
362
    }
363
}
364