3 * @copyright Copyright (C) 2010-2023, the Friendica project
5 * @license GNU AGPL version 3 or any later version
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU Affero General Public License as
9 * published by the Free Software Foundation, either version 3 of the
10 * License, or (at your option) any later version.
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU Affero General Public License for more details.
17 * You should have received a copy of the GNU Affero General Public License
18 * along with this program. If not, see <https://www.gnu.org/licenses/>.
22 namespace Friendica\Util;
24 use Friendica\Core\Logger;
25 use Friendica\Model\APContact;
28 * Implements JSON-LD signatures
30 * Ported from Osada: https://framagit.org/macgirvin/osada
35 * Checks if element 'signature' is found and not empty
40 public static function isSigned(array $data): bool
42 return !empty($data['signature']);
46 * Returns actor (signer) from given data
49 * @return mixed Returns actor or false on error
51 public static function getSigner(array $data)
53 if (!self::isSigned($data)) {
57 $actor = JsonLD::fetchElement($data, 'actor', 'id');
58 if (empty($actor) || !is_string($actor)) {
62 $profile = APContact::getByURL($actor);
63 if (empty($profile['pubkey'])) {
66 $pubkey = $profile['pubkey'];
68 $ohash = self::hash(self::signableOptions($data['signature']));
69 $dhash = self::hash(self::signableData($data));
71 $x = Crypto::rsaVerify($ohash . $dhash, base64_decode($data['signature']['signatureValue']), $pubkey);
72 Logger::notice('LD-verify', ['verified' => (int)$x, 'actor' => $profile['url']]);
82 * Signs given data by owner's signature
84 * @param array $data Data to sign
85 * @param array $owner Owner information, like URL
86 * @return array Merged array of $data and signature
88 public static function sign(array $data, array $owner): array
91 'type' => 'RsaSignature2017',
92 'nonce' => Strings::getRandomHex(64),
93 'creator' => $owner['url'] . '#main-key',
94 'created' => DateTimeFormat::utcNow(DateTimeFormat::ATOM),
97 $ohash = self::hash(self::signableOptions($options));
98 $dhash = self::hash(self::signableData($data));
99 $options['signatureValue'] = base64_encode(Crypto::rsaSign($ohash . $dhash, $owner['uprvkey']));
101 return array_merge($data, ['signature' => $options]);
105 * Removes element 'signature' from array
108 * @return array With no element 'signature'
110 private static function signableData(array $data): array
112 unset($data['signature']);
117 * Removes some elements and adds '@context' to it
119 * @param array $options
120 * @return array With some removed elements and added '@context' element
122 private static function signableOptions(array $options): array
124 $newopts = ['@context' => 'https://w3id.org/identity/v1'];
126 unset($options['type']);
127 unset($options['id']);
128 unset($options['signatureValue']);
130 return array_merge($newopts, $options);
134 * Hashes normalized object
137 * @return string SHA256 hash
139 private static function hash($obj): string
141 return hash('sha256', JsonLD::normalize($obj));