public function verify($options = array()) { if (!isset($options['version'])) { $options['version'] = Version::latest(); } $signatureAttachment = null; $signatureIndex = 0; foreach ($this->getAttachments() as $attachment) { if ($attachment->getUsageType() === self::SIGNATURE_USAGE_TYPE) { $signatureAttachment = $attachment; break; } $signatureIndex++; } if ($signatureAttachment === null) { return array('success' => false, 'reason' => "Unable to locate signature attachment (usage type)"); } try { $jws = JWS::load($signatureAttachment->getContent()); } catch (\InvalidArgumentException $e) { return array('success' => false, 'reason' => 'Failed to load JWS: ' . $e); } $header = $jws->getHeader(); // // there is a JWS spec security issue with allowing non-RS algorithms // to be specified and it is against the Tin Can spec anyways so we // want to fail hard on non-RS algorithms // if (!in_array($header['alg'], array('RS256', 'RS384', 'RS512'), true)) { throw new \InvalidArgumentException("Refusing to verify signature: Invalid signing algorithm ('" . $options['algorithm'] . "')"); } if (isset($options['publicKey'])) { $publicKeyFile = $options['publicKey']; } else { if (isset($header['x5c'])) { $cert = "-----BEGIN CERTIFICATE-----\r\n" . chunk_split($header['x5c'][0], 64, "\r\n") . "-----END CERTIFICATE-----\r\n"; $cert = openssl_x509_read($cert); if (!$cert) { return array('success' => false, 'reason' => 'failed to read cert in x5c: ' . openssl_error_string()); } $publicKeyFile = openssl_pkey_get_public($cert); if (!$publicKeyFile) { return array('success' => false, 'reason' => 'x5c failed to provide public key: ' . openssl_error_string()); } } else { return array('success' => false, 'reason' => 'No public key found or provided for verification'); } } if (!$jws->verify($publicKeyFile)) { return array('success' => false, 'reason' => 'Failed to verify signature'); } $payload = $jws->getPayload(); // // serializing this statement as if it was going to be // made into a signature should provide us with what we // can expect in the payload, if the two don't match then // the signature isn't valid, it also gives us a clone // that we can then manipulate without affecting the // original instance // // use the version from the payload as it indicates the // version in use when the statement was serialized to // begin with // $version = $payload['version'] ? $payload['version'] : Version::latest(); $serialization = $this->serializeForSignature($version); // // remove the signature attachment before comparing the // serializations, if it was the only attachment and the // signature doesn't include the 'attachments' property // then unset it as well // unset($serialization['attachments'][$signatureIndex]); if (count($serialization['attachments']) === 0 && !isset($payload['attachments'])) { unset($serialization['attachments']); } // // authority and stored are most often populated by the LRS, // and presumably for signature purposes are *never* included // in the signature so we are safe to remove them here // unset($serialization['stored']); unset($serialization['authority']); // // the payload 'version' is instructive of how to serialize the // statement for comparison, that 'version' is not required and // when not set we need to remove the 'version' in the serialization // which will be the current latest supported by the library // which shouldn't be compared against what is in the signature // if (!isset($payload['version'])) { unset($serialization['version']); } // // a statement can be signed without having first provided an // id, in that case the id is set by the receiving LRS, so if // the serialization has one, presumably from retrieval from // an LRS, remove it so that it is not compared // // if the statement did provide an id before signing then the // LRS should have maintained that id, so they can be compared // if (!isset($payload['id'])) { unset($serialization['id']); } // // the same applies to timestamp // if (!isset($payload['timestamp'])) { unset($serialization['timestamp']); } // // now we can construct an object from both the payload and the // serialization of this instance and compare the two for a match // in meaning // $fromSerialization = new self($serialization); $comparison = $fromSerialization->compareWithSignature(new self($payload)); if (!$comparison['success']) { return array('success' => false, 'reason' => 'Statement to signature comparison failed: ' . $comparison['reason']); } return array('success' => true, 'jws' => $jws); }