Plugin Directory

Changeset 2723045


Ignore:
Timestamp:
05/12/2022 10:55:08 PM (4 years ago)
Author:
psqr
Message:

v 0.1.1

File:
1 edited

Legend:

Unmodified
Added
Removed
  • virtual-public-square/trunk/psqr.php

    r2702058 r2723045  
    77Plugin URI: https://vpsqr.com/
    88Description: Virtual Public Squares operate on identity. Add self-hosted, cryptographically verifiable, decentralized identity to your site and authors.
    9 Version: 0.1.0
     9Version: 0.1.1
    1010Author: Virtual Public Square
    1111Author URI: https://vpsqr.com
     
    1717This plugin allows admins to upload DID:PSQR documents for their wordpress users and
    1818resolves requests for the did:psqr bare domain (to /.well-known/psqr) and specific user profiles (to /author/{name}).
    19 You can generate DID:PSQR docs using the NodeJS CLI client "psqr" (https://www.npmjs.com/package/psqr) 
     19You can generate DID:PSQR docs using the NodeJS CLI client "psqr" (https://www.npmjs.com/package/psqr)
    2020that can be installed with the command "npm i -g psqr".
    2121Then go to the user settings page to upload user specific docs.
     
    2323*/
    2424
     25require_once(plugin_dir_path(__FILE__) . '/lib/autoload.php');
     26
     27use Jose\Component\Core\AlgorithmManager;
     28use Jose\Component\Core\JWK;
     29use Jose\Component\Signature\Algorithm\ES384;
     30use Jose\Component\Signature\JWSVerifier;
     31use Jose\Component\Signature\Serializer\CompactSerializer;
     32use Jose\Component\Signature\Serializer\JWSSerializerManager;
     33
    2534if ( !class_exists( 'PSQR' ) ) {
     35
    2636    class PSQR
    2737    {
     
    3343        ];
    3444
     45        // set some standard responses
     46        const RESPONSES = [
     47            'did_missing' => [
     48                'code'    => 'did_missing',
     49                'message' => 'The specified DID:PSQR identity could not be found',
     50                'data'    => [
     51                    'status' => 404
     52                ]
     53            ],
     54            'invalid_jws' => [
     55                'code'    => 'invalid_jws',
     56                'message' => 'The provided JWS was not signed correctly or is somehow invalid',
     57                'data'    => [
     58                    'status' => 401
     59                ]
     60            ],
     61            'did_error' => [
     62                'code'    => 'did_error',
     63                'message' => 'There was an error storing the did.',
     64                'data'    => [
     65                    'status' => 400
     66                ]
     67            ]
     68        ];
     69
    3570        private $available_dids = [];
     71        private JWSSerializerManager $serializer_manager;
     72        private JWSVerifier $jws_verifier;
    3673
    3774        function __construct() {
     
    3976            // create necessary directories
    4077            $this->setup_dirs();
     78
     79            $algorithmManager  = new AlgorithmManager([new ES384()]);
     80            $this->jws_verifier = new JWSVerifier(
     81                $algorithmManager
     82            );
     83            $this->serializer_manager = new JWSSerializerManager([
     84                new CompactSerializer(),
     85            ]);
    4186
    4287            // setup did and api response
     
    89134
    90135            if ($identity === false) {
    91                 wp_send_json([
    92                     'code'    => 'did_missing',
    93                     'message' => 'The specified DID:PSQR identity could not be found',
    94                     'data'    => [
    95                         'status' => 404
    96                     ]
    97                 ], 404);
     136                wp_send_json(PSQR::RESPONSES['did_missing'], 404);
    98137            }
    99138
     
    102141
    103142        function api_put_response($request) {
    104             $body = json_decode($request->get_body());
     143            $body = json_decode($request->get_body(), false);
    105144            $name = $request->get_url_params()['name'];
    106145
     
    131170            $response = $this->store_identity('/author/' . $name, $body);
    132171            if ($response === false) {
    133                 wp_send_json([
    134                     'code'    => 'did_error',
    135                     'message' => 'There was an error storing the did.',
    136                     'data'    => [
    137                         'status' => 400
    138                     ]
    139                 ], 400);
     172                wp_send_json(PSQR::RESPONSES['did_error'], 400);
    140173            }
    141174
    142175            return new WP_REST_RESPONSE(['message' => 'did:psqr document successfully uploaded']);
    143176        }
    144    
     177
    145178        // function to retrieve did file data as an object
    146179        // don't pass a path to get base identity
     
    153186            if (file_exists($full_path) === false) {
    154187                return false;
    155             } 
    156    
     188            }
     189
    157190            // retrieve and parse file data
    158191            $file_data = file_get_contents($full_path);
    159             $identity_obj = json_decode($file_data);
    160    
     192            $identity_obj = json_decode($file_data, false);
     193
    161194            // return empty object if no data found
    162195            if ($identity_obj === null) {
    163196                return false;
    164197            }
    165    
     198
    166199            return $identity_obj;
    167200        }
     
    170203            // basic validation, need more thorough validation later
    171204            if (isset($identity->psqr) === false ||
    172                 isset($identity->psqr->publicIdentity) === false || 
     205                isset($identity->psqr->publicIdentity) === false ||
    173206                isset($identity->psqr->publicKeys) === false ||
    174207                isset($identity->psqr->permissions) === false) {
     
    178211                ];
    179212            }
     213
     214
     215            return [
     216                'valid' => true,
     217                'message' => 'Valid DID:PSQR structure'
     218            ];
    180219        }
    181220
     
    185224            $base_path = trailingslashit( $upload_dir['basedir'] ) . 'psqr-identities/';
    186225            $full_path = $base_path . $path . '/';
    187            
     226
    188227            // create the directory if necessary
    189228            if (is_dir($full_path) === false) {
     
    193232                }
    194233            }
    195        
     234
    196235            return file_put_contents($full_path . 'identity.json', json_encode($file_data));
    197236        }
    198    
     237
     238        function delete_identity($path) {
     239            // determine absolute file path
     240            $upload_dir = wp_upload_dir();
     241            $base_path = trailingslashit( $upload_dir['basedir'] ) . 'psqr-identities/';
     242            $full_path = $base_path . $path . '/';
     243
     244            // if dir doesn't exist return false
     245            if (is_dir($full_path) === false) {
     246                return false;
     247            }
     248
     249            $rm_file = unlink($full_path . 'identity.json');
     250            if ($rm_file === false) {
     251                return false;
     252            }
     253
     254            return rmdir($full_path);
     255        }
     256
     257        /**
     258         * validate jws token string with pubkey from specified did.
     259         *
     260         * @param string $path path from request url
     261         * @param string $token jws token string
     262         *
     263         * @return bool is it valid
     264         */
     265        function validate_update(string $path, string $token): bool
     266        {
     267            $kid;
     268            $jws;
     269            try {
     270                $jws = $this->serializer_manager->unserialize($token);
     271                $kid = $jws->getSignatures()[0]->getProtectedHeader()['kid'];
     272            } catch (\Throwable $th) {
     273                return false;
     274            }
     275
     276            // get didDoc specified in header
     277            $matches;
     278            preg_match('/did:psqr:[^\/]+([^#]+)/', $kid, $matches);
     279            $kidPath = $matches[1];
     280
     281            // fail if path from signature doesn't match request path
     282            if ($path !== $kidPath) {
     283                return false;
     284            }
     285
     286            $didDoc = $this->get_identity($path);
     287
     288            if ($didDoc === false) {
     289                return false;
     290            }
     291
     292
     293            // try to find valid public keys
     294            $keys   = $didDoc->psqr->publicKeys;
     295            $pubKey = false;
     296
     297            for ($j = 0; $j < \count($keys); ++$j) {
     298                $k = $keys[$j];
     299                if ($k->kid === $kid) {
     300                    $pubKey = new JWK((array) $k, 0);
     301
     302                    break;
     303                }
     304            }
     305            // return false if no pubKey was found
     306            if ($pubKey === false) {
     307                return false;
     308            }
     309
     310            // verify key used has admin permission
     311            $perms    = $didDoc->psqr->permissions;
     312            $keyGrant = false;
     313
     314            for ($i = 0; $i < \count($perms); ++$i) {
     315                $p = $perms[$i];
     316                if ($p->kid === $kid) {
     317                    $keyGrant = $p->grant;
     318
     319                    break;
     320                }
     321            }
     322            // return false if no grant was found or doesn't contain admin
     323            if ($keyGrant === false || in_array('admin', $keyGrant) === false) {
     324                return false;
     325            }
     326
     327            return $this->jws_verifier->verifyWithKey($jws, $pubKey, 0);
     328        }
     329
    199330        // setup action to return identity.json on request
    200331        function rewrite_request($query) {
    201332            $path = $query->request;
    202    
    203             // get all headers and make all keys and values lowercase.
     333            $method = $_SERVER['REQUEST_METHOD'];
     334
     335            // retrieve, sanitize, and validate JWS string if present
     336            $jws_matches;
     337            $raw_input = file_get_contents('php://input');
     338            preg_match('/[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+/', $raw_input, $jws_matches);
     339            $jws = empty($jws_matches) ? '' : $jws_matches[0];
     340
    204341            $headers = array_change_key_case(array_map('strtolower', getallheaders()), CASE_LOWER);
    205342            $accept_header = $headers['accept'];
    206    
     343
    207344            if ($path === '.well-known/psqr') {
     345                if (strtoupper($method) === 'PUT') {
     346                    return $this->update_did('', $jws);
     347                }
     348
     349                if (strtoupper($method) === 'DELETE') {
     350                    return $this->delete_did('', $jws);
     351                }
     352
    208353                $identity = $this->get_identity();
    209354
    210355                if ($identity === false) {
    211                     wp_send_json([
    212                         'code'    => 'did_missing',
    213                         'message' => 'The specified DID:PSQR identity could not be found',
    214                         'data'    => [
    215                             'status' => 404
    216                         ]
    217                     ], 404);
    218                 }
    219    
     356                    wp_send_json(PSQR::RESPONSES['did_missing'], 404);
     357                }
     358
    220359                wp_send_json($identity);
    221360            } elseif (isset($query->query_vars['author_name'])) {
    222361                $author_name = $query->query_vars['author_name'];
     362                $file_path = '/author/' . $author_name;
     363
     364                if (strtoupper($method) === 'PUT') {
     365                    return $this->update_did($file_path, $jws);
     366                }
     367
     368                if (strtoupper($method) === 'DELETE') {
     369                    return $this->delete_did($file_path, $jws);
     370                }
    223371
    224372                foreach (PSQR::VALID_HEADERS as $val) {
    225373                    if (strpos($accept_header, $val) !== false) {
    226                         $file_path = '/author/' . $author_name;
    227374                        $identity = $this->get_identity($file_path);
    228375
    229376                        if ($identity === false) {
    230                             wp_send_json([
    231                                 'code'    => 'did_missing',
    232                                 'message' => 'The specified DID:PSQR identity could not be found',
    233                                 'data'    => [
    234                                     'status' => 404
    235                                 ]
    236                             ], 404);
     377                            wp_send_json(PSQR::RESPONSES['did_missing'], 404);
    237378                        }
    238    
     379
    239380                        wp_send_json($identity);
    240381                    }
    241382                }
    242383            }
    243    
     384
    244385            return $query;
     386        }
     387
     388        function update_did(string $path, string $body)
     389        {
     390            $signature_valid = $this->validate_update($path, $body);
     391
     392            if ($signature_valid === false) {
     393                wp_send_json(PSQR::RESPONSES['invalid_jws'], 401);
     394            }
     395
     396            $jws = $this->serializer_manager->unserialize($body);
     397            $newDid = json_decode($jws->getPayload(), false);
     398
     399            // validate doc
     400            $valid_resp = $this->validate_identity($newDid);
     401            if ($valid_resp['valid'] === false) {
     402                wp_send_json([
     403                    'code'    => 'did_invalid',
     404                    'message' => $valid_resp['message'],
     405                    'data'    => [
     406                        'status' => 400
     407                    ]
     408                ], 400);
     409            }
     410
     411            $response = $this->store_identity($path, $newDid);
     412            if ($response === false) {
     413                wp_send_json(PSQR::RESPONSES['did_error'], 400);
     414            }
     415
     416            wp_send_json($newDid, 200);
     417        }
     418
     419        public function delete_did(string $path, string $body)
     420        {
     421            $signature_valid = $this->validate_update($path, $body);
     422
     423            if ($signature_valid === false) {
     424                wp_send_json(PSQR::RESPONSES['invalid_jws'], 401);
     425            }
     426
     427            $response = $this->delete_identity($path);
     428            if ($response === false) {
     429                wp_send_json(PSQR::RESPONSES['did_error'], 400);
     430            }
     431
     432            wp_send_json([
     433                'code'    => 'did_deleted',
     434                'message' => 'DID was successfully deleted',
     435            ], 200);
    245436        }
    246437
     
    271462                $path = '/wp-json/psqr/v' . $this::VERSION . '/author/' . $user->user_login;
    272463                $did = 'did:psqr:' . $_SERVER['HTTP_HOST'] . $path;
    273                
     464
    274465                // if identity dir is present, show link
    275466                if (in_array($user->user_login, $this->available_dids)) {
     
    281472
    282473                    // set button html
    283                     $btn_html = wp_enqueue_script('did-upload', plugins_url( "js/upload.js", __FILE__)) . 
     474                    $btn_html = wp_enqueue_script('did-upload', plugins_url( "js/upload.js", __FILE__)) .
    284475                    wp_enqueue_style('did-upload-style', plugins_url( "css/upload-modal.css", __FILE__)) . '
    285476                        <button class="button js-show-did-upload"
Note: See TracChangeset for help on using the changeset viewer.