ppcl.php 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. <?php
  2. class Ppcl {
  3. public $identifier;
  4. public $serial;
  5. public $domains = array();
  6. public $members = array();
  7. public $agents = array();
  8. public $last_modified;
  9. public $signature;
  10. public $shared_signature_key;
  11. public $shared_signature;
  12. public $name;
  13. public $current_state_token;
  14. public $publications = array();
  15. private $authoritative_part;
  16. public function from_string($str) {
  17. $lines = explode("\n", $str);
  18. $header = explode(" ", $lines[0]);
  19. $this->authoritative_part = explode("\nCST ", $str, 2)[0];
  20. if($header[0] != "PPCL") {
  21. throw new Exception("Header does not start with PPCL magic number", 1);
  22. }
  23. $this->identifier = base64_decode($header[1]);
  24. $this->serial = (int)$header[2];
  25. $authoritative = true;
  26. foreach ($lines as $line) {
  27. $entry = explode(" ", $line);
  28. if($entry[0] == "PPCL") {
  29. continue;
  30. }
  31. if($entry[0] == "SSK" && $authoritative) {
  32. $this->shared_signature_key = base64_decode($entry[1]);
  33. }
  34. else if($entry[0] == "NAM" && $authoritative) {
  35. $this->name = explode(" ", $line, 2)[1];
  36. }
  37. else if($entry[0] == "DOM" && $authoritative) {
  38. array_push($this->domains, $entry[1]);
  39. }
  40. else if($entry[0] == "MEM" && $authoritative) {
  41. array_push($this->members, new CollectionMember($entry[1], $entry[2], base64_decode($entry[3])));
  42. }
  43. else if($entry[0] == "AGT" && $authoritative) {
  44. array_push($this->agents, new CollectionAgent($entry[1], base64_decode($entry[2]), base64_decode($entry[3])));
  45. }
  46. else if($entry[0] == "SIG" && $authoritative) {
  47. $this->signature = base64_decode($entry[1]);
  48. $authoritative = false;
  49. }
  50. else if($entry[0] == "CST") {
  51. $this->current_state_token = base64_decode($entry[1]);
  52. }
  53. else if($entry[0] == "MOD") {
  54. $this->last_modified = new DateTime($entry[1]);
  55. }
  56. else if($entry[0] == "PUB") {
  57. array_push($this->publications, new CollectionPublication($line, $this->members));
  58. }
  59. else if($entry[0] == "SSG") {
  60. $this->shared_signature = base64_decode($entry[1]);
  61. }
  62. }
  63. // Verify authoritative signature
  64. $parts = explode("\nSIG ", $str, 2);
  65. $hash = hash("sha512", $parts[0], true);
  66. $sig_data = sodium_crypto_sign_open($this->signature, $this->identifier);
  67. if($hash != $sig_data) {
  68. throw new Exception("Invalid authoritative signature", 1);
  69. }
  70. // Verify shared signature
  71. $parts = explode("\nSSG ", $str, 2);
  72. $hash = hash("sha512", $parts[0], true);
  73. $sig_data = sodium_crypto_sign_open($this->shared_signature, $this->shared_signature_key);
  74. if($hash != $sig_data) {
  75. throw new Exception("Invalid shared signature", 1);
  76. }
  77. }
  78. public function to_string() {
  79. $str = $this->authoritative_part;
  80. $now = new DateTime();
  81. $token = random_bytes(256);
  82. $str .= "\nCST " . base64_encode($token) . "\nMOD " . $now->format(DateTime::ATOM) . "\n";
  83. usort($this->publications, function ($a, $b) { return $a->timestamp <=> $b->timestamp; });
  84. foreach($this->publications as $pub) {
  85. $str .= "\n" . $pub->raw_data;
  86. }
  87. $checksum = hash("sha512", $str, true);
  88. $agent = null;
  89. foreach($this->agents as $agt) {
  90. if($agt->public_key == base64_decode(PPCL_AGENT_PUBLIC_KEY)) {
  91. $agent = $agt;
  92. break;
  93. }
  94. }
  95. if($agent == null) {
  96. throw new Exception("Public key defined in config (PPCL_AGENT_PUBLIC_KEY) not present in PPCL file", 1);
  97. }
  98. $key = sodium_crypto_box_keypair_from_secretkey_and_publickey(base64_decode(PPCL_AGENT_SECRET_KEY), $agent->public_key);
  99. $secret = sodium_crypto_box_seal_open($agent->collection_secret, $key);
  100. if($secret == null) {
  101. throw new Exception("Could not decrypt collection secret", 1);
  102. }
  103. $shared_signature = sodium_crypto_sign($checksum, $secret);
  104. $str .= "\nSSG " . base64_encode($shared_signature);
  105. return $str;
  106. }
  107. public function get_publication($name) {
  108. foreach($this->publications as $pub) {
  109. if($pub->name == $name) {
  110. return $pub;
  111. }
  112. }
  113. return null;
  114. }
  115. }
  116. class CollectionMember {
  117. public $name;
  118. public $signing_public_key;
  119. public $sealing_public_key;
  120. public $collection_secret;
  121. public function __construct($name, $keys, $secret) {
  122. $this->name = $name;
  123. $this->collection_secret = $secret;
  124. $key_parts = explode(":", $keys);
  125. if($key_parts[0] != "CLMPK") {
  126. error_log($keys);
  127. throw new Exception("Invalid member public key");
  128. }
  129. $key_data = base64_decode($key_parts[1]);
  130. $this->signing_public_key = substr($key_data, 0, SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES);
  131. $this->sealing_public_key = substr($key_data, SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES, SODIUM_CRYPTO_BOX_PUBLICKEYBYTES);
  132. }
  133. }
  134. class CollectionAgent {
  135. public $name;
  136. public $public_key;
  137. public $collection_secret;
  138. public function __construct($name, $key, $secret) {
  139. $this->name = $name;
  140. $this->public_key = $key;
  141. $this->collection_secret = $secret;
  142. }
  143. }
  144. class CollectionPublication {
  145. public $name;
  146. public $member_name;
  147. public $timestamp;
  148. public $signature;
  149. public $checksum;
  150. public $raw_data;
  151. public function __construct($line, $members) {
  152. $this->raw_data = $line;
  153. $parts = explode(": ", $line, 2);
  154. $params = explode(" ", $parts[1]);
  155. $this->name = substr($parts[0], 4);
  156. $this->member_name = $params[0];
  157. $this->timestamp = new DateTime($params[1]);
  158. $this->signature = base64_decode($params[2]);
  159. $member = null;
  160. foreach($members as $m) {
  161. if($m->name == $this->member_name) {
  162. $member = $m;
  163. break;
  164. }
  165. }
  166. if($member == null) {
  167. throw new Exception("PUB entry references member (" . $this->member_name . ") that is not present in the collection", 1);
  168. }
  169. $hash_data = $this->name . ": " . $this->member_name . " " . $params[1];
  170. $hash = hash("sha512", $hash_data, true);
  171. $sig_data = sodium_crypto_sign_open($this->signature, $member->signing_public_key);
  172. if(substr($sig_data, 0, 64) != $hash) {
  173. throw new Exception("Invalid member signature on publication " . $this->name , 1);
  174. }
  175. $this->checksum = substr($sig_data, 64, 64);
  176. }
  177. public function verify_ppub() {
  178. $file_hash = hash_file("sha512", PUBLICATION_DIR . "/" . $this->name, true);
  179. return $file_hash == $this->checksum;
  180. }
  181. }
  182. ?>