From 4263d031889d4aa84965b8593c32ecfb80bfa0a1 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Sun, 20 Jan 2013 21:44:33 -0500 Subject: [PATCH] Restructure signing code All sorts of signatures can be verified now, and it is easier to extract information from the verified signature packets. --- lib/openpgp.php | 140 +++++++++++++++++++++++++++++--------- lib/openpgp_crypt_rsa.php | 39 ++++++++--- phpunit.xml | 4 ++ tests/data/helloKey.gpg | Bin 0 -> 1598 bytes tests/phpseclib_suite.php | 15 +++- tests/suite.php | 18 ++--- 6 files changed, 163 insertions(+), 53 deletions(-) create mode 100644 tests/data/helloKey.gpg diff --git a/lib/openpgp.php b/lib/openpgp.php index d529ef2..6c0686e 100644 --- a/lib/openpgp.php +++ b/lib/openpgp.php @@ -171,36 +171,116 @@ class OpenPGP_Message implements IteratorAggregate, ArrayAccess { return $bytes; } - function signature_and_data($index=0) { + /** + * Extract signed objects from a well-formatted message + * + * Recurses into CompressedDataPacket + * + * + */ + function signatures() { $msg = $this; - while($msg[0] instanceof OpenPGP_CompressedDataPacket) $msg = $msg[0]; + while($msg[0] instanceof OpenPGP_CompressedDataPacket) $msg = $msg[0]->data; - $i = 0; - foreach($msg as $p) { - if($p instanceof OpenPGP_SignaturePacket) { - if($i == $index) $signature_packet = $p; - $i++; + $key = NULL; + $userid = NULL; + $subkey = NULL; + $sigs = array(); + $final_sigs = array(); + + foreach($msg as $idx => $p) { + if($p instanceof OpenPGP_LiteralDataPacket) { + return array(array($p, array_values(array_filter($msg->packets, function($p) { + return $p instanceof OpenPGP_SignaturePacket; + })))); + } else if($p instanceof OpenPGP_PublicSubkeyPacket || $p instanceof OpenPGP_SecretSubkeyPacket) { + if($userid) { + array_push($final_sigs, array($key, $userid, $sigs)); + $userid = NULL; + } else if($subkey) { + array_push($final_sigs, array($key, $subkey, $sigs)); + $key = NULL; + } + $sigs = array(); + $subkey = $p; + } else if($p instanceof OpenPGP_PublicKeyPacket) { + if($userid) { + array_push($final_sigs, array($key, $userid, $sigs)); + $userid = NULL; + } else if($subkey) { + array_push($final_sigs, array($key, $subkey, $sigs)); + $subkey = NULL; + } else if($key) { + array_push($final_sigs, array($key, $sigs)); + $key = NULL; + } + $sigs = array(); + $key = $p; + } else if($p instanceof OpenPGP_UserIDPacket) { + if($userid) { + array_push($final_sigs, array($key, $userid, $sigs)); + $userid = NULL; + } else if($key) { + array_push($final_sigs, array($key, $sigs)); + } + $sigs = array(); + $userid = $p; + } else if($p instanceof OpenPGP_SignaturePacket) { + $sigs[] = $p; } - if($p instanceof OpenPGP_LiteralDataPacket) $data_packet = $p; - if(isset($signature_packet) && isset($data_packet)) break; } - return array($signature_packet, $data_packet); + if($userid) { + array_push($final_sigs, array($key, $userid, $sigs)); + } else if($subkey) { + array_push($final_sigs, array($key, $subkey, $sigs)); + } else if($key) { + array_push($final_sigs, array($key, $sigs)); + } + + return $final_sigs; } /** - * Function to verify signature number $index - * $verifiers is an array of callbacks formatted like array('RSA' => array('SHA256' => CALLBACK)) that take two parameters: message and signature + * Function to extract verified signatures + * $verifiers is an array of callbacks formatted like array('RSA' => array('SHA256' => CALLBACK)) that take two parameters: raw message and signature packet */ - function verify($verifiers, $index=0) { - list($signature_packet, $data_packet) = $this->signature_and_data($index); - if(!$signature_packet || !$data_packet) return NULL; // No signature or no data + function verified_signatures($verifiers) { + $signed = $this->signatures(); + $vsigned = array(); - $verifier = $verifiers[$signature_packet->key_algorithm_name()][$signature_packet->hash_algorithm_name()]; - if(!$verifier) return NULL; // No verifier + foreach($signed as $sign) { + $signatures = array_pop($sign); + $vsigs = array(); - $data_packet->normalize(); - return call_user_func($verifier, $data_packet->data.$signature_packet->trailer, $signature_packet->data); + foreach($signatures as $sig) { + $verifier = $verifiers[$sig->key_algorithm_name()][$sig->hash_algorithm_name()]; + if($verifier && $this->verify_one($verifier, $sign, $sig)) { + $vsigs[] = $sig; + } + } + array_push($sign, $vsigs); + $vsigned[] = $sign; + } + + return $vsigned; + } + + function verify_one($verifier, $sign, $sig) { + if($sign[0] instanceof OpenPGP_LiteralDataPacket) { + $sign[0]->normalize(); + $raw = $sign[0]->data; + } else if(isset($sign[1]) && $sign[1] instanceof OpenPGP_UserIDPacket) { + $raw = implode('', array_merge($sign[0]->fingerprint_material(), array(chr(0xB4), + pack('N', strlen($sign[1]->body())), $sign[1]->body()))); + } else if(isset($sign[1]) && ($sign[1] instanceof OpenPGP_PublicSubkeyPacket || $sign[1] instanceof OpenPGP_SecretSubkeyPacket)) { + $raw = implode('', array_merge($sign[0]->fingerprint_material(), $sign[1]->fingerprint_material())); + } else if($sign[0] instanceof OpenPGP_PublicKeyPacket) { + $raw = implode('', $sign[0]->fingerprint_material()); + } else { + return NULL; + } + return call_user_func($verifier, $raw.$sig->trailer, $sig); } // IteratorAggregate interface @@ -285,8 +365,7 @@ class OpenPGP_Packet { return array($tag, 3, (($len - 192) << 8) + ord($input[2]) + 192); } if($len == 255) { // Five octet length - $unpacked = unpack('N', substr($input, 2, 4)); - return array($tag, 6, array_pop($unpacked)); + return array($tag, 6, reset(unpack('N', substr($input, 2, 4)))); } // TODO: Partial body lengths. 1 << ($len & 0x1F) } @@ -366,8 +445,7 @@ class OpenPGP_Packet { * @see http://php.net/manual/en/function.unpack.php */ function read_unpacked($count, $format) { - $unpacked = unpack($format, $this->read_bytes($count)); - return array_pop($unpacked); + return reset(unpack($format, $this->read_bytes($count))); } function read_byte() { @@ -457,8 +535,7 @@ class OpenPGP_SignaturePacket extends OpenPGP_Packet { $this->trailer = $this->body(true); $signer = $signers[$this->key_algorithm_name()][$this->hash_algorithm_name()]; $this->data = call_user_func($signer, $this->data.$this->trailer); - $unpacked = unpack('n', substr($this->data, 0, 2)); - $this->hash_head = array_pop($unpacked); + $this->hash_head = reset(unpack('n', substr($this->data, 0, 2))); } function read() { @@ -623,8 +700,7 @@ class OpenPGP_SignaturePacket extends OpenPGP_Packet { } if($len == 255) { // Five octet length $length_of_length = 5; - $unpacked = unpack('N', substr($input, 1, 4)); - $len = array_pop($unpacked); + $len = reset(unpack('N', substr($input, 1, 4))); } $input = substr($input, $length_of_length); // Chop off length header $tag = ord($input[0]); @@ -1520,27 +1596,27 @@ class OpenPGP_UserIDPacket extends OpenPGP_Packet { } function read() { - $this->text = $this->input; + $this->data = $this->input; // User IDs of the form: "name (comment) " - if (preg_match('/^([^\(]+)\(([^\)]+)\)\s+<([^>]+)>$/', $this->text, $matches)) { + if (preg_match('/^([^\(]+)\(([^\)]+)\)\s+<([^>]+)>$/', $this->data, $matches)) { $this->name = trim($matches[1]); $this->comment = trim($matches[2]); $this->email = trim($matches[3]); } // User IDs of the form: "name " - else if (preg_match('/^([^<]+)\s+<([^>]+)>$/', $this->text, $matches)) { + else if (preg_match('/^([^<]+)\s+<([^>]+)>$/', $this->data, $matches)) { $this->name = trim($matches[1]); $this->comment = NULL; $this->email = trim($matches[2]); } // User IDs of the form: "name" - else if (preg_match('/^([^<]+)$/', $this->text, $matches)) { + else if (preg_match('/^([^<]+)$/', $this->data, $matches)) { $this->name = trim($matches[1]); $this->comment = NULL; $this->email = NULL; } // User IDs of the form: "" - else if (preg_match('/^<([^>]+)>$/', $this->text, $matches)) { + else if (preg_match('/^<([^>]+)>$/', $this->data, $matches)) { $this->name = NULL; $this->comment = NULL; $this->email = trim($matches[2]); diff --git a/lib/openpgp_crypt_rsa.php b/lib/openpgp_crypt_rsa.php index 1e9456b..9b455ac 100644 --- a/lib/openpgp_crypt_rsa.php +++ b/lib/openpgp_crypt_rsa.php @@ -50,23 +50,40 @@ class OpenPGP_Crypt_RSA { // Pass a message to verify with this key, or a key (OpenPGP or Crypt_RSA) to check this message with // Second optional parameter to specify which signature to verify (if there is more than one) function verify($packet, $index=0) { + $self = $this; // For old PHP if(!is_object($packet)) $packet = OpenPGP_Message::parse($packet); - if($packet instanceof OpenPGP_Message && !($packet[0] instanceof OpenPGP_PublicKeyPacket)) { - list($signature_packet, $data_packet) = $packet->signature_and_data($index); - $key = $this->public_key($signature_packet->issuer()); - if(!$key || $signature_packet->key_algorithm_name() != 'RSA') return NULL; - $key->setHash(strtolower($signature_packet->hash_algorithm_name())); - return $packet->verify(array('RSA' => array($signature_packet->hash_algorithm_name() => function($m, $s) use($key) {return $key->verify($m, $s[0]);}))); + if(!$this->message) { + $m = $packet; + $verifier = function($m, $s) use($self) { + $key = $self->public_key($s->issuer()); + if(!$key) return false; + $key->setHash(strtolower($s->hash_algorithm_name())); + return $key->verify($m, reset($s->data)); + }; } else { - list($signature_packet, $data_packet) = $this->message->signature_and_data($index); - if(!$this->message || $signature_packet->key_algorithm_name() != 'RSA') return NULL; if(!($packet instanceof Crypt_RSA)) { $packet = new self($packet); - $packet = $packet->public_key($signature_packet->issuer()); } - $packet->setHash(strtolower($signature_packet->hash_algorithm_name())); - return $this->message->verify(array('RSA' => array($signature_packet->hash_algorithm_name() => function($m, $s) use($packet) {return $packet->verify($m, $s[0]);}))); + + $m = $this->message; + $verifier = function($m, $s) use($self, $packet) { + if(!($packet instanceof Crypt_RSA)) { + $key = $packet->public_key($s->issuer()); + } + if(!$key) return false; + $key->setHash(strtolower($s->hash_algorithm_name())); + return $key->verify($m, reset($s->data)); + }; } + + return $m->verified_signatures(array('RSA' => array( + 'MD5' => $verifier, + 'SHA1' => $verifier, + 'SHA224' => $verifier, + 'SHA256' => $verifier, + 'SHA384' => $verifier, + 'SHA512' => $verifier + ))); } // Pass a message to sign with this key, or a secret key to sign this message with diff --git a/phpunit.xml b/phpunit.xml index 8049722..41b0d95 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -11,5 +11,9 @@ tests/phpseclib_suite.php + + + tests/phpseclib_suite.php + diff --git a/tests/data/helloKey.gpg b/tests/data/helloKey.gpg new file mode 100644 index 0000000000000000000000000000000000000000..b1dd07809b2a0349d006c74d158edd5e3276b402 GIT binary patch literal 1598 zcmV-E2EqB20yzXvcM>E41mMg^G;*uss=TRo*CUKrHG%M>TL8dv3N}=1QwS*vK8EV7 znfH*;-+XYkE(F|H zE>9Dt_?R5$(PPfhu-8B4{@^&fBIWy$J|$Iu3x%L-B463Kb?}48vZ#c8Twq`nb(U+Y zK^p{zx=CmYrN&1l-ySeAl!1POV?ue&Vk=r_oPq{~iV@#Rdb ziLiCpBx~!syN17RfQY}LmJ``gl!7{tqU4wJsrxD!@qi3Vb)jtRzoL>S)FbI2>YG>@3 z8$>-iPR}TM+Ytp`3Ko4Q!JkZ3E(~Bwbt`AGr2=jw6e`o#X@#!-C69^U={epX1t1g& zwS&1Wa1Q)%-__R6>Q@|?{n<>oB-+749I0ij{5eorE58v_Li5bDo>1`7!Y2Ll2I6$kKx- z4wXFw-Z=9Ot>0YVE$W*1Ij$Wy0Yzkltg1V+eA>*JYV`WHC15$BUg`gcVG8>4nu#7w z9)6#lp^k=C0yA*9pz6`Eb!hH*;kL2ZThEKB7-V(rS#L<o z(X{2g)XRZWkt7zsxc!mmQgb{&twDs$^?{X>xTx!W|H7VgFg~F}q{ifLRLgRJ5$$4X z6TSQRa&oC*&@E8%w4`}~gH$dwRD2*T&#*$iRDr^*3hE!;LdJ@*wlux!`##U}kr@9l z8;JJ4IbY)9+=&@e*-AAjaM)nVq`#~w0&v%|(g?~-I;Inr4Z-TcOFfTg&g=!{6Y)6l zNq@9dI9|YN)RwOUYe+pQBNLv}r0KpU(5$v^*92xO$IC-m{PI~C&4Y^sl56w*FA5+7 z0&v}Y!gI1Vh&Okd#TX~D#V(|S7 z%M~qnI5Qs->&hM@H>?xvETFW&;v~kaHKx-4mk(}-4eD^3AdVD zQq*1&qycS|`1$ypqfW|}$PR3I2Xo5-%FoovQ6+f$@VnGK4%5G4MURT)yUqD6so1%aR+QQaaXI&`eiRXs|%ojv@mNabKl_< wOf#Z1qRP7SZPUy}d5R?Po`J)u(MTgyP%fShW?UQIcRi4g$O!Xly0|o9Yk;`(J^%m! literal 0 HcmV?d00001 diff --git a/tests/phpseclib_suite.php b/tests/phpseclib_suite.php index 4f23a84..90e0725 100644 --- a/tests/phpseclib_suite.php +++ b/tests/phpseclib_suite.php @@ -10,7 +10,7 @@ class MessageVerification extends PHPUnit_Framework_TestCase { $pkeyM = OpenPGP_Message::parse(file_get_contents(dirname(__FILE__) . '/data/' . $pkey)); $m = OpenPGP_Message::parse(file_get_contents(dirname(__FILE__) . '/data/' . $path)); $verify = new OpenPGP_Crypt_RSA($pkeyM); - $this->assertSame($verify->verify($m), TRUE); + $this->assertSame($verify->verify($m), $m->signatures()); } public function testUncompressedOpsRSA() { @@ -39,3 +39,16 @@ class MessageVerification extends PHPUnit_Framework_TestCase { } */ } + + +class KeyVerification extends PHPUnit_Framework_TestCase { + public function oneKeyRSA($path) { + $m = OpenPGP_Message::parse(file_get_contents(dirname(__FILE__) . '/data/' . $path)); + $verify = new OpenPGP_Crypt_RSA($m); + $this->assertSame($verify->verify($m), $m->signatures()); + } + + public function testHelloKey() { + $this->oneKeyRSA("helloKey.gpg"); + } +} diff --git a/tests/suite.php b/tests/suite.php index b417098..6ee09c0 100644 --- a/tests/suite.php +++ b/tests/suite.php @@ -378,15 +378,15 @@ class Fingerprint extends PHPUnit_Framework_TestCase { $this->oneFingerprint("000001-006.public_key", "421F28FEAAD222F856C8FFD5D4D54EA16F87040E"); } - public function test000016006public_key() { - $this->oneFingerprint("000016-006.public_key", "AF95E4D7BAC521EE9740BED75E9F1523413262DC"); - } + public function test000016006public_key() { + $this->oneFingerprint("000016-006.public_key", "AF95E4D7BAC521EE9740BED75E9F1523413262DC"); + } - public function test000027006public_key() { - $this->oneFingerprint("000027-006.public_key", "1EB20B2F5A5CC3BEAFD6E5CB7732CF988A63EA86"); - } + public function test000027006public_key() { + $this->oneFingerprint("000027-006.public_key", "1EB20B2F5A5CC3BEAFD6E5CB7732CF988A63EA86"); + } - public function test000035006public_key() { - $this->oneFingerprint("000035-006.public_key", "CB7933459F59C70DF1C3FBEEDEDC3ECF689AF56D"); - } + public function test000035006public_key() { + $this->oneFingerprint("000035-006.public_key", "CB7933459F59C70DF1C3FBEEDEDC3ECF689AF56D"); + } }