Changeset 2723045
- Timestamp:
- 05/12/2022 10:55:08 PM (4 years ago)
- File:
-
- 1 edited
-
virtual-public-square/trunk/psqr.php (modified) (15 diffs)
Legend:
- Unmodified
- Added
- Removed
-
virtual-public-square/trunk/psqr.php
r2702058 r2723045 7 7 Plugin URI: https://vpsqr.com/ 8 8 Description: Virtual Public Squares operate on identity. Add self-hosted, cryptographically verifiable, decentralized identity to your site and authors. 9 Version: 0.1. 09 Version: 0.1.1 10 10 Author: Virtual Public Square 11 11 Author URI: https://vpsqr.com … … 17 17 This plugin allows admins to upload DID:PSQR documents for their wordpress users and 18 18 resolves 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) 19 You can generate DID:PSQR docs using the NodeJS CLI client "psqr" (https://www.npmjs.com/package/psqr) 20 20 that can be installed with the command "npm i -g psqr". 21 21 Then go to the user settings page to upload user specific docs. … … 23 23 */ 24 24 25 require_once(plugin_dir_path(__FILE__) . '/lib/autoload.php'); 26 27 use Jose\Component\Core\AlgorithmManager; 28 use Jose\Component\Core\JWK; 29 use Jose\Component\Signature\Algorithm\ES384; 30 use Jose\Component\Signature\JWSVerifier; 31 use Jose\Component\Signature\Serializer\CompactSerializer; 32 use Jose\Component\Signature\Serializer\JWSSerializerManager; 33 25 34 if ( !class_exists( 'PSQR' ) ) { 35 26 36 class PSQR 27 37 { … … 33 43 ]; 34 44 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 35 70 private $available_dids = []; 71 private JWSSerializerManager $serializer_manager; 72 private JWSVerifier $jws_verifier; 36 73 37 74 function __construct() { … … 39 76 // create necessary directories 40 77 $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 ]); 41 86 42 87 // setup did and api response … … 89 134 90 135 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); 98 137 } 99 138 … … 102 141 103 142 function api_put_response($request) { 104 $body = json_decode($request->get_body() );143 $body = json_decode($request->get_body(), false); 105 144 $name = $request->get_url_params()['name']; 106 145 … … 131 170 $response = $this->store_identity('/author/' . $name, $body); 132 171 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); 140 173 } 141 174 142 175 return new WP_REST_RESPONSE(['message' => 'did:psqr document successfully uploaded']); 143 176 } 144 177 145 178 // function to retrieve did file data as an object 146 179 // don't pass a path to get base identity … … 153 186 if (file_exists($full_path) === false) { 154 187 return false; 155 } 156 188 } 189 157 190 // retrieve and parse file data 158 191 $file_data = file_get_contents($full_path); 159 $identity_obj = json_decode($file_data );160 192 $identity_obj = json_decode($file_data, false); 193 161 194 // return empty object if no data found 162 195 if ($identity_obj === null) { 163 196 return false; 164 197 } 165 198 166 199 return $identity_obj; 167 200 } … … 170 203 // basic validation, need more thorough validation later 171 204 if (isset($identity->psqr) === false || 172 isset($identity->psqr->publicIdentity) === false || 205 isset($identity->psqr->publicIdentity) === false || 173 206 isset($identity->psqr->publicKeys) === false || 174 207 isset($identity->psqr->permissions) === false) { … … 178 211 ]; 179 212 } 213 214 215 return [ 216 'valid' => true, 217 'message' => 'Valid DID:PSQR structure' 218 ]; 180 219 } 181 220 … … 185 224 $base_path = trailingslashit( $upload_dir['basedir'] ) . 'psqr-identities/'; 186 225 $full_path = $base_path . $path . '/'; 187 226 188 227 // create the directory if necessary 189 228 if (is_dir($full_path) === false) { … … 193 232 } 194 233 } 195 234 196 235 return file_put_contents($full_path . 'identity.json', json_encode($file_data)); 197 236 } 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 199 330 // setup action to return identity.json on request 200 331 function rewrite_request($query) { 201 332 $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 204 341 $headers = array_change_key_case(array_map('strtolower', getallheaders()), CASE_LOWER); 205 342 $accept_header = $headers['accept']; 206 343 207 344 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 208 353 $identity = $this->get_identity(); 209 354 210 355 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 220 359 wp_send_json($identity); 221 360 } elseif (isset($query->query_vars['author_name'])) { 222 361 $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 } 223 371 224 372 foreach (PSQR::VALID_HEADERS as $val) { 225 373 if (strpos($accept_header, $val) !== false) { 226 $file_path = '/author/' . $author_name;227 374 $identity = $this->get_identity($file_path); 228 375 229 376 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); 237 378 } 238 379 239 380 wp_send_json($identity); 240 381 } 241 382 } 242 383 } 243 384 244 385 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); 245 436 } 246 437 … … 271 462 $path = '/wp-json/psqr/v' . $this::VERSION . '/author/' . $user->user_login; 272 463 $did = 'did:psqr:' . $_SERVER['HTTP_HOST'] . $path; 273 464 274 465 // if identity dir is present, show link 275 466 if (in_array($user->user_login, $this->available_dids)) { … … 281 472 282 473 // 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__)) . 284 475 wp_enqueue_style('did-upload-style', plugins_url( "css/upload-modal.css", __FILE__)) . ' 285 476 <button class="button js-show-did-upload"
Note: See TracChangeset
for help on using the changeset viewer.