authoritative_part = explode("\nCST ", $str, 2)[0]; if($header[0] != "PPCL") { throw new Exception("Header does not start with PPCL magic number", 1); } $this->identifier = base64_decode($header[1]); $this->serial = (int)$header[2]; $authoritative = true; foreach ($lines as $line) { $entry = explode(" ", $line); if($entry[0] == "PPCL") { continue; } if($entry[0] == "SSK" && $authoritative) { $this->shared_signature_key = base64_decode($entry[1]); } else if($entry[0] == "NAM" && $authoritative) { $this->name = explode(" ", $line, 2)[1]; } else if($entry[0] == "DOM" && $authoritative) { array_push($this->domains, $entry[1]); } else if($entry[0] == "MEM" && $authoritative) { array_push($this->members, new CollectionMember($entry[1], base64_decode($entry[2]), base64_decode($entry[3]), base64_decode($entry[4]))); } else if($entry[0] == "AGT" && $authoritative) { array_push($this->agents, new CollectionAgent($entry[1], base64_decode($entry[2]), base64_decode($entry[3]))); } else if($entry[0] == "SIG" && $authoritative) { $this->signature = base64_decode($entry[1]); $authoritative = false; } else if($entry[0] == "CST") { $this->current_state_token = base64_decode($entry[1]); } else if($entry[0] == "MOD") { $this->last_modified = new DateTime($entry[1]); } else if($entry[0] == "PUB") { array_push($this->publications, new CollectionPublication($line, $this->members)); } else if($entry[0] == "SSG") { $this->shared_signature = base64_decode($entry[1]); } } // Verify authoritative signature $parts = explode("\nSIG ", $str, 2); $hash = hash("sha512", $parts[0], true); $sig_data = sodium_crypto_sign_open($this->signature, $this->identifier); if($hash != $sig_data) { throw new Exception("Invalid authoritative signature", 1); } // Verify shared signature $parts = explode("\nSSG ", $str, 2); $hash = hash("sha512", $parts[0], true); $sig_data = sodium_crypto_sign_open($this->shared_signature, $this->shared_signature_key); if($hash != $sig_data) { throw new Exception("Invalid shared signature", 1); } } public function to_string() { $str = $this->authoritative_part; $now = new DateTime(); $token = random_bytes(256); $str .= "\nCST " . base64_encode($token) . "\nMOD " . $now->format(DateTime::ATOM) . "\n"; usort($this->publications, function ($a, $b) { return $a->timestamp <=> $b->timestamp; }); foreach($this->publications as $pub) { $str .= "\n" . $pub->raw_data; } $checksum = hash("sha512", $str, true); $agent = null; foreach($this->agents as $agt) { if($agt->public_key == base64_decode(PPCL_AGENT_PUBLIC_KEY)) { $agent = $agt; break; } } if($agent == null) { throw new Exception("Public key defined in config (PPCL_AGENT_PUBLIC_KEY) not present in PPCL file", 1); } $key = sodium_crypto_box_keypair_from_secretkey_and_publickey(base64_decode(PPCL_AGENT_SECRET_KEY), $agent->public_key); $secret = sodium_crypto_box_seal_open($agent->collection_secret, $key); if($secret == null) { throw new Exception("Could not decrypt collection secret", 1); } $shared_signature = sodium_crypto_sign($checksum, $secret); $str .= "\nSSG " . base64_encode($shared_signature); return $str; } } class CollectionMember { public $name; public $signing_public_key; public $sealing_public_key; public $collection_secret; public function __construct($name, $sign_key, $seal_key, $secret) { $this->name = $name; $this->signing_public_key = $sign_key; $this->sealing_public_key = $seal_key; $this->collection_secret = $secret; } } class CollectionAgent { public $name; public $public_key; public $collection_secret; public function __construct($name, $key, $secret) { $this->name = $name; $this->public_key = $key; $this->collection_secret = $secret; } } class CollectionPublication { public $name; public $member_name; public $timestamp; public $signature; public $checksum; public $raw_data; public function __construct($line, $members) { $this->raw_data = $line; $parts = explode(": ", $line, 2); $params = explode(" ", $parts[1]); $this->name = substr($parts[0], 4); $this->member_name = $params[0]; $this->timestamp = new DateTime($params[1]); $this->signature = base64_decode($params[2]); $member = null; foreach($members as $m) { if($m->name == $this->member_name) { $member = $m; break; } } if($member == null) { throw new Exception("PUB entry references member (" . $this->member_name . ") that is not present in the collection", 1); } $hash_data = $this->name . ": " . $this->member_name . " " . $params[1]; $hash = hash("sha512", $hash_data, true); $sig_data = sodium_crypto_sign_open($this->signature, $member->signing_public_key); if(substr($sig_data, 0, 64) != $hash) { throw new Exception("Invalid member signature on publication " . $this->name , 1); } $this->checksum = substr($sig_data, 64, 64); } public function verify_ppub() { $file_hash = hash_file("sha512", PUBLICATION_DIR . "/" . $this->name, true); return $file_hash == $this->checksum; } } ?>