/** * Handles a status line * * @param string $line the status line to handle. * * @return void */ public function handle($line) { $tokens = explode(' ', $line); switch ($tokens[0]) { case 'GOODSIG': case 'EXPSIG': case 'EXPKEYSIG': case 'REVKEYSIG': case 'BADSIG': $signature = new Crypt_GPG_Signature(); // if there was a signature id, set it on the new signature if ($this->signatureId != '') { $signature->setId($this->signatureId); $this->signatureId = ''; } // Detect whether fingerprint or key id was returned and set // signature values appropriately. Key ids are strings of either // 16 or 8 hexadecimal characters. Fingerprints are strings of 40 // hexadecimal characters. The key id is the last 16 characters of // the key fingerprint. if (strlen($tokens[1]) > 16) { $signature->setKeyFingerprint($tokens[1]); $signature->setKeyId(substr($tokens[1], -16)); } else { $signature->setKeyId($tokens[1]); } // get user id string $string = implode(' ', array_splice($tokens, 2)); $string = rawurldecode($string); $signature->setUserId(Crypt_GPG_UserId::parse($string)); $this->index++; $this->signatures[$this->index] = $signature; break; case 'ERRSIG': $signature = new Crypt_GPG_Signature(); // if there was a signature id, set it on the new signature if ($this->signatureId != '') { $signature->setId($this->signatureId); $this->signatureId = ''; } // Detect whether fingerprint or key id was returned and set // signature values appropriately. Key ids are strings of either // 16 or 8 hexadecimal characters. Fingerprints are strings of 40 // hexadecimal characters. The key id is the last 16 characters of // the key fingerprint. if (strlen($tokens[1]) > 16) { $signature->setKeyFingerprint($tokens[1]); $signature->setKeyId(substr($tokens[1], -16)); } else { $signature->setKeyId($tokens[1]); } $this->index++; $this->signatures[$this->index] = $signature; break; case 'VALIDSIG': if (!array_key_exists($this->index, $this->signatures)) { break; } $signature = $this->signatures[$this->index]; $signature->setValid(true); $signature->setKeyFingerprint($tokens[1]); if (strpos($tokens[3], 'T') === false) { $signature->setCreationDate($tokens[3]); } else { $signature->setCreationDate(strtotime($tokens[3])); } if (array_key_exists(4, $tokens)) { if (strpos($tokens[4], 'T') === false) { $signature->setExpirationDate($tokens[4]); } else { $signature->setExpirationDate(strtotime($tokens[4])); } } break; case 'SIG_ID': // note: signature id comes before new signature line and may not // exist for some signature types $this->signatureId = $tokens[1]; break; } }
/** * Handles error values in the status output from GPG * * This method is responsible for setting the * {@link self::$errorCode}. See <b>doc/DETAILS</b> in the * {@link http://www.gnupg.org/download/ GPG distribution} for detailed * information on GPG's status output. * * @param string $line the status line to handle. * * @return void */ public function handleStatus($line) { $tokens = explode(' ', $line); switch ($tokens[0]) { case 'NODATA': $this->errorCode = Crypt_GPG::ERROR_NO_DATA; break; case 'DECRYPTION_OKAY': // If the message is encrypted, this is the all-clear signal. $this->data['DecryptionOkay'] = true; $this->errorCode = Crypt_GPG::ERROR_NONE; break; case 'DELETE_PROBLEM': if ($tokens[1] == '1') { $this->errorCode = Crypt_GPG::ERROR_KEY_NOT_FOUND; break; } elseif ($tokens[1] == '2') { $this->errorCode = Crypt_GPG::ERROR_DELETE_PRIVATE_KEY; break; } break; case 'IMPORT_OK': $this->data['Import']['fingerprint'] = $tokens[2]; if (empty($this->data['Import']['fingerprints'])) { $this->data['Import']['fingerprints'] = array($tokens[2]); } else { if (!in_array($tokens[2], $this->data['Import']['fingerprints'])) { $this->data['Import']['fingerprints'][] = $tokens[2]; } } break; case 'IMPORT_RES': $this->data['Import']['public_imported'] = intval($tokens[3]); $this->data['Import']['public_unchanged'] = intval($tokens[5]); $this->data['Import']['private_imported'] = intval($tokens[11]); $this->data['Import']['private_unchanged'] = intval($tokens[12]); break; case 'NO_PUBKEY': case 'NO_SECKEY': $this->data['ErrorKeyId'] = $tokens[1]; if ($this->errorCode != Crypt_GPG::ERROR_MISSING_PASSPHRASE && $this->errorCode != Crypt_GPG::ERROR_BAD_PASSPHRASE) { $this->errorCode = Crypt_GPG::ERROR_KEY_NOT_FOUND; } // note: this message is also received if there are multiple // recipients and a previous key had a correct passphrase. $this->data['MissingKeys'][$tokens[1]] = $tokens[1]; // @FIXME: remove missing passphrase registered in ENC_TO handler // This is for GnuPG 2.1 unset($this->data['MissingPassphrases'][$tokens[1]]); break; case 'KEY_CONSIDERED': // In GnuPG 2.1.x exporting/importing a secret key requires passphrase // However, no NEED_PASSPRASE is returned, https://bugs.gnupg.org/gnupg/issue2667 // So, handling KEY_CONSIDERED and GET_HIDDEN is needed. if (!array_key_exists('KeyConsidered', $this->data)) { $this->data['KeyConsidered'] = $tokens[1]; } break; case 'USERID_HINT': // remember the user id for pretty exception messages // GnuPG 2.1.15 gives me: "USERID_HINT 0000000000000000 [?]" $keyId = $tokens[1]; if (strcspn($keyId, '0')) { $username = implode(' ', array_splice($tokens, 2)); $this->data['BadPassphrases'][$keyId] = $username; } break; case 'ENC_TO': // Now we know the message is encrypted. Set flag to check if // decryption succeeded. $this->data['DecryptionOkay'] = false; // this is the new key message $this->data['CurrentSubKeyId'] = $keyId = $tokens[1]; // For some reason in GnuPG 2.1.11 I get only ENC_TO and no // NEED_PASSPHRASE/MISSING_PASSPHRASE/USERID_HINT // This is not needed for GnuPG 2.1.15 if (!empty($_ENV['PINENTRY_USER_DATA'])) { $passphrases = json_decode($_ENV['PINENTRY_USER_DATA'], true); } else { $passphrases = array(); } // @TODO: Get user name/email $this->data['BadPassphrases'][$keyId] = $keyId; if (empty($passphrases) || empty($passphrases[$keyId])) { $this->data['MissingPassphrases'][$keyId] = $keyId; } break; case 'GOOD_PASSPHRASE': // if we got a good passphrase, remove the key from the list of // bad passphrases. if (isset($this->data['CurrentSubKeyId'])) { unset($this->data['BadPassphrases'][$this->data['CurrentSubKeyId']]); unset($this->data['MissingPassphrases'][$this->data['CurrentSubKeyId']]); } $this->needPassphrase--; break; case 'BAD_PASSPHRASE': $this->errorCode = Crypt_GPG::ERROR_BAD_PASSPHRASE; break; case 'MISSING_PASSPHRASE': if (isset($this->data['CurrentSubKeyId'])) { $this->data['MissingPassphrases'][$this->data['CurrentSubKeyId']] = $this->data['CurrentSubKeyId']; } $this->errorCode = Crypt_GPG::ERROR_MISSING_PASSPHRASE; break; case 'GET_HIDDEN': if ($tokens[1] == 'passphrase.enter' && isset($this->data['KeyConsidered'])) { $tokens[1] = $this->data['KeyConsidered']; } else { break; } // no break // no break case 'NEED_PASSPHRASE': $passphrase = $this->getPin($tokens[1]); $this->engine->sendCommand($passphrase); if ($passphrase === '') { $this->needPassphrase++; } break; case 'SIG_CREATED': $this->data['SigCreated'] = $line; break; case 'SIG_ID': // note: signature id comes before new signature line and may not // exist for some signature types $this->data['SignatureId'] = $tokens[1]; break; case 'EXPSIG': case 'EXPKEYSIG': case 'REVKEYSIG': case 'BADSIG': case 'ERRSIG': $this->errorCode = Crypt_GPG::ERROR_BAD_SIGNATURE; // no break // no break case 'GOODSIG': $signature = new Crypt_GPG_Signature(); // if there was a signature id, set it on the new signature if (!empty($this->data['SignatureId'])) { $signature->setId($this->data['SignatureId']); $this->data['SignatureId'] = ''; } // Detect whether fingerprint or key id was returned and set // signature values appropriately. Key ids are strings of either // 16 or 8 hexadecimal characters. Fingerprints are strings of 40 // hexadecimal characters. The key id is the last 16 characters of // the key fingerprint. if (mb_strlen($tokens[1], '8bit') > 16) { $signature->setKeyFingerprint($tokens[1]); $signature->setKeyId(mb_substr($tokens[1], -16, null, '8bit')); } else { $signature->setKeyId($tokens[1]); } // get user id string if ($tokens[0] != 'ERRSIG') { $string = implode(' ', array_splice($tokens, 2)); $string = rawurldecode($string); $signature->setUserId(Crypt_GPG_UserId::parse($string)); } $this->data['Signatures'][] = $signature; break; case 'VALIDSIG': if (empty($this->data['Signatures'])) { break; } $signature = end($this->data['Signatures']); $signature->setValid(true); $signature->setKeyFingerprint($tokens[1]); if (strpos($tokens[3], 'T') === false) { $signature->setCreationDate($tokens[3]); } else { $signature->setCreationDate(strtotime($tokens[3])); } if (array_key_exists(4, $tokens)) { if (strpos($tokens[4], 'T') === false) { $signature->setExpirationDate($tokens[4]); } else { $signature->setExpirationDate(strtotime($tokens[4])); } } break; case 'KEY_CREATED': if (isset($this->data['Handle']) && $tokens[3] == $this->data['Handle']) { $this->data['KeyCreated'] = $tokens[2]; } break; case 'KEY_NOT_CREATED': if (isset($this->data['Handle']) && $tokens[1] == $this->data['Handle']) { $this->errorCode = Crypt_GPG::ERROR_KEY_NOT_CREATED; } break; case 'PROGRESS': // todo: at some point, support reporting status async break; // GnuPG 2.1 uses FAILURE and ERROR responses // GnuPG 2.1 uses FAILURE and ERROR responses case 'FAILURE': case 'ERROR': $errnum = (int) $tokens[2]; $source = $errnum >> 24; $errcode = $errnum & 0xffffff; switch ($errcode) { case 11: // bad passphrase // bad passphrase case 87: // bad PIN $this->errorCode = Crypt_GPG::ERROR_BAD_PASSPHRASE; break; case 177: // no passphrase // no passphrase case 178: // no PIN $this->errorCode = Crypt_GPG::ERROR_MISSING_PASSPHRASE; break; case 58: $this->errorCode = Crypt_GPG::ERROR_NO_DATA; break; } break; } }
/** * @group fluent */ public function testFluentInterface() { $signature = new Crypt_GPG_Signature(); $returnedSignature = $signature->setId('KuhELanvhPRXozEjFWb2mam1q20'); $this->assertEquals($signature, $returnedSignature, 'Failed asserting fluent interface works for setId() method.'); $signature = new Crypt_GPG_Signature(); $returnedSignature = $signature->setKeyFingerprint('8D2299D9C5C211128B32BBB0C097D9EC94C06363'); $this->assertEquals($signature, $returnedSignature, 'Failed asserting fluent interface works for setKeyFingerprint() ' . 'method.'); $signature = new Crypt_GPG_Signature(); $returnedSignature = $signature->setKeyId('0C097D9EC94C06363'); $this->assertEquals($signature, $returnedSignature, 'Failed asserting fluent interface works for setKeyId() method'); $signature = new Crypt_GPG_Signature(); $returnedSignature = $signature->setCreationDate(1234567890); $this->assertEquals($signature, $returnedSignature, 'Failed asserting fluent interface works for setCreationDate() ' . 'method.'); $signature = new Crypt_GPG_Signature(); $returnedSignature = $signature->setExpirationDate(1234567890); $this->assertEquals($signature, $returnedSignature, 'Failed asserting fluent interface works for setExpirationDate() ' . 'method.'); $signature = new Crypt_GPG_Signature(); $returnedSignature = $signature->setValid(true); $this->assertEquals($signature, $returnedSignature, 'Failed asserting fluent interface works for setValid() method.'); $signature = new Crypt_GPG_Signature(); $returnedSignature = $signature->setUserId(new Crypt_GPG_UserId()); $this->assertEquals($signature, $returnedSignature, 'Failed asserting fluent interface works for setUserId() method.'); }
protected function assertSignatureEquals(Crypt_GPG_Signature $expected, Crypt_GPG_Signature $actual) { $expectedUserId = $expected->getUserId(); $actualUserId = $actual->getUserId(); $this->assertEquals($expectedUserId, $actualUserId, 'Signature user ids do not match.'); $expectedId = $expected->getId(); $actualId = $actual->getId(); $this->assertEquals(strlen($expectedId), strlen($actualId), 'Signature IDs are of different length.'); $this->assertEquals($expected->getKeyFingerprint(), $actual->getKeyFingerprint(), 'Signature key fingerprints do not match.'); $this->assertEquals($expected->getKeyId(), $actual->getKeyId(), 'Signature key IDs do not match.'); $this->assertEquals($expected->getCreationDate(), $actual->getCreationDate(), 'Signature creation dates do not match.'); $this->assertEquals($expected->getExpirationDate(), $actual->getExpirationDate(), 'Signature expiration dates do not match.'); $this->assertEquals($expected->isValid(), $actual->isValid(), 'Signature validity does match.'); }
/** * @group file */ public function testVerifyFileDualDetachedSignature() { // {{{ first signature $firstSignature = new Crypt_GPG_Signature(); $firstSignature->setId('T7+toJbsFr8KMTWN+M7lF3xSmmA'); $firstSignature->setKeyFingerprint('8D2299D9C5C211128B32BBB0C097D9EC94C06363'); $firstSignature->setKeyId('C097D9EC94C06363'); $firstSignature->setCreationDate(1221960707); $firstSignature->setExpirationDate(0); $firstSignature->setValid(true); $userId = new Crypt_GPG_UserId(); $userId->setName('First Keypair Test Key'); $userId->setComment('do not encrypt important data with this key'); $userId->setEmail('*****@*****.**'); $firstSignature->setUserId($userId); // }}} // {{{ second signature $secondSignature = new Crypt_GPG_Signature(); $secondSignature->setId('HJd1yvMbEbW5facuxkDtvwymKrw'); $secondSignature->setKeyFingerprint('880922DBEA733E906693E4A903CC890AFA1DAD4B'); $secondSignature->setKeyId('03CC890AFA1DAD4B'); $secondSignature->setCreationDate(1221960707); $secondSignature->setExpirationDate(0); $secondSignature->setValid(true); $userId = new Crypt_GPG_UserId(); $userId->setName('Second Keypair Test Key'); $userId->setComment('do not encrypt important data with this key'); $userId->setEmail('*****@*****.**'); $secondSignature->setUserId($userId); // }}} // {{{ signature data $signatureData = <<<TEXT -----BEGIN PGP SIGNATURE----- Version: GnuPG v1.4.6 (GNU/Linux) iD8DBQBI1aQDwJfZ7JTAY2MRAvkzAKDPnJ030GdYE15mE8smz2oV7zYziwCeJFxf UaTrAgP1Dck9DhHOBhvhwLuIPwMFAEjVpAMDzIkK+h2tSxEC+TMAn38yx3mXk6wP JaPThD7lRVE9ve57AJ0Yy7JwiT9sGXomln4JtRvuSpGtsg== =Gw9D -----END PGP SIGNATURE----- TEXT; // }}} $expectedSignatures = array($firstSignature, $secondSignature); $filename = $this->getDataFilename('testFileMedium.plain'); $signatures = $this->gpg->verifyFile($filename, $signatureData); $this->assertSignaturesEquals($expectedSignatures, $signatures); }
/** * Verify a message * * @param Message $message * @param string $fingerprint * @return bool * @throws \Exception */ public function verify(Message $message, string $fingerprint) : bool { $gnupg = new \Crypt_GPG($this->options); $gnupg->addSignKey($fingerprint); /** * @var \Crypt_GPG_Signature[] */ $verified = $gnupg->verify($message->getBodyText()); foreach ($verified as $sig) { if (false) { $sig = new \Crypt_GPG_Signature(); } if ($sig->isValid()) { return true; } } return false; }
/** * @group file */ public function testDecryptVerifyFileSignedOnly() { // {{{ signature $signature = new Crypt_GPG_Signature(); $signature->setId('vctnI/HnsRYmqcVwCJcJhS60lKU'); $signature->setKeyFingerprint('8D2299D9C5C211128B32BBB0C097D9EC94C06363'); $signature->setKeyId('C097D9EC94C06363'); $signature->setCreationDate(1221960707); $signature->setExpirationDate(0); $signature->setValid(true); $userId = new Crypt_GPG_UserId(); $userId->setName('First Keypair Test Key'); $userId->setComment('do not encrypt important data with this key'); $userId->setEmail('*****@*****.**'); $signature->setUserId($userId); // }}} $expectedMd5Sum = 'f96267d87551ee09bfcac16921e351c1'; $expectedResults = array('data' => null, 'signatures' => array($signature)); $inputFilename = $this->getDataFilename('testVerifyFileNormalSignedData.asc'); $outputFilename = $this->getTempFilename('testDecryptVerifyFileSignedData.plain'); $results = $this->gpg->decryptAndVerifyFile($inputFilename, $outputFilename); $this->assertDecryptAndVerifyResultsEquals($expectedResults, $results); $md5Sum = $this->getMd5Sum($outputFilename); $this->assertEquals($expectedMd5Sum, $md5Sum); }