ppcl.php 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  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("\nMOD ", $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], base64_decode($entry[2]), base64_decode($entry[3]), base64_decode($entry[4])));
  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 " . $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. }
  108. class CollectionMember {
  109. public $name;
  110. public $signing_public_key;
  111. public $sealing_public_key;
  112. public $collection_secret;
  113. public function __construct($name, $sign_key, $seal_key, $secret) {
  114. $this->name = $name;
  115. $this->signing_public_key = $sign_key;
  116. $this->sealing_public_key = $seal_key;
  117. $this->collection_secret = $secret;
  118. }
  119. }
  120. class CollectionAgent {
  121. public $name;
  122. public $public_key;
  123. public $collection_secret;
  124. public function __construct($name, $key, $secret) {
  125. $this->name = $name;
  126. $this->public_key = $key;
  127. $this->collection_secret = $secret;
  128. }
  129. }
  130. class CollectionPublication {
  131. public $name;
  132. public $member_name;
  133. public $timestamp;
  134. public $signature;
  135. public $checksum;
  136. public $raw_data;
  137. public function __construct($line, $members) {
  138. $this->raw_data = $line;
  139. $parts = explode(": ", $line, 2);
  140. $params = explode(" ", $parts[1]);
  141. $this->name = substr($parts[0], 4);
  142. $this->member_name = $params[0];
  143. $this->timestamp = new DateTime($params[1]);
  144. $this->signature = base64_decode($params[2]);
  145. $member = null;
  146. foreach($members as $m) {
  147. if($m->name == $this->member_name) {
  148. $member = $m;
  149. break;
  150. }
  151. }
  152. if($member == null) {
  153. throw new Exception("PUB entry references member (" . $this->member_name . ") that is not present in the collection", 1);
  154. }
  155. $hash_data = $this->name . ": " . $this->member_name . " " . $params[1];
  156. $hash = hash("sha512", $hash_data, true);
  157. $sig_data = sodium_crypto_sign_open($this->signature, $member->signing_public_key);
  158. if(substr($sig_data, 0, 64) != $hash) {
  159. throw new Exception("Invalid member signature on publication " . $this->name , 1);
  160. }
  161. $this->checksum = substr($sig_data, 64, 64);
  162. }
  163. public function verify_ppub() {
  164. $file_hash = hash_file("sha512", PUBLICATION_DIR . "/" . $this->name, true);
  165. return $file_hash == $this->checksum;
  166. }
  167. }
  168. ?>