/** * @param CertificateValidatorInterface|NULL $certValidator * @param string $blob * @return AppMetasMessage * Validated message. * @throws InvalidMessageException */ public static function decode($certValidator, $blob) { $parts = explode(Constants::PROTOCOL_DELIM, $blob, 4); if (count($parts) != 4) { throw new InvalidMessageException('Invalid message: insufficient parameters'); } list($wireProt, $wireCert, $wireSig, $wireEnvelope) = $parts; if ($wireProt != self::NAME) { throw new InvalidMessageException('Invalid message: wrong protocol name'); } if ($certValidator !== NULL) { $certValidator->validateCert($wireCert); $wireCertX509 = new \File_X509(); $wireCertX509->loadX509($wireCert); $cn = $wireCertX509->getDNProp('CN'); if (count($cn) != 1 || $cn[0] != Constants::OFFICIAL_APPMETAS_CN) { throw new InvalidMessageException('Invalid message: signed by unauthorized party'); } $isValid = UserError::adapt('Civi\\Cxn\\Rpc\\Exception\\InvalidMessageException', function () use($wireCertX509, $wireEnvelope, $wireSig) { return AppMetasMessage::getRsaFromCert($wireCertX509)->verify($wireEnvelope, base64_decode($wireSig)); }); if (!$isValid) { throw new InvalidMessageException("Invalid message: incorrect signature"); } } $envelope = json_decode($wireEnvelope, TRUE); if (empty($envelope)) { throw new InvalidMessageException("Invalid message: malformed envelope"); } if (Time::getTime() > $envelope['ttl']) { throw new InvalidMessageException("Invalid message: expired"); } return new AppMetasMessage($wireCert, NULL, json_decode($envelope['r'], TRUE)); }
/** * @param array $caKeyPair * @param string $caCert * PEM-encoded cert. * @param string $csr * PEM-encoded CSR. * @param int $serialNumber * @return string * PEM-encoded cert. */ public static function signCSR($caKeyPair, $caCert, $csr, $serialNumber = 1) { $privKey = new \Crypt_RSA(); $privKey->loadKey($caKeyPair['privatekey']); $subject = new \File_X509(); $subject->loadCSR($csr); $issuer = new \File_X509(); $issuer->loadX509($caCert); $issuer->setPrivateKey($privKey); $x509 = new \File_X509(); $x509->setSerialNumber($serialNumber, 10); $x509->setEndDate(date('c', strtotime(Constants::APP_DURATION, Time::getTime()))); $result = $x509->sign($issuer, $subject, Constants::CERT_SIGNATURE_ALGORITHM); return $x509->saveX509($result); }
protected static function validate($certPem, $caCertPem, $crlPem = NULL, $crlDistCertPem = NULL) { $caCertObj = X509Util::loadCACert($caCertPem); $certObj = new \File_X509(); $certObj->loadCA($caCertPem); if ($crlPem !== NULL) { $crlObj = new \File_X509(); if ($crlDistCertPem) { $crlDistCertObj = X509Util::loadCrlDistCert($crlDistCertPem, NULL, $caCertPem); if ($crlDistCertObj->getSubjectDN(FILE_X509_DN_STRING) !== $caCertObj->getSubjectDN(FILE_X509_DN_STRING)) { throw new InvalidCertException(sprintf("CRL distributor (%s) does not act on behalf of this CA (%s)", $crlDistCertObj->getSubjectDN(FILE_X509_DN_STRING), $caCertObj->getSubjectDN(FILE_X509_DN_STRING))); } try { self::validate($crlDistCertPem, $caCertPem); } catch (InvalidCertException $ie) { throw new InvalidCertException("CRL distributor has an invalid certificate", 0, $ie); } $crlObj->loadCA($crlDistCertPem); } $crlObj->loadCA($caCertPem); $crlObj->loadCRL($crlPem); if (!$crlObj->validateSignature()) { throw new InvalidCertException("CRL signature is invalid"); } } $parsedCert = $certObj->loadX509($certPem); if ($crlPem !== NULL) { if (empty($parsedCert)) { throw new InvalidCertException("Identity is invalid. Empty certificate."); } if (empty($parsedCert['tbsCertificate']['serialNumber'])) { throw new InvalidCertException("Identity is invalid. No serial number."); } $revoked = $crlObj->getRevoked($parsedCert['tbsCertificate']['serialNumber']->toString()); if (!empty($revoked)) { throw new InvalidCertException("Identity is invalid. Certificate revoked."); } } if (!$certObj->validateSignature()) { throw new InvalidCertException("Identity is invalid. Certificate is not signed by proper CA."); } if (!$certObj->validateDate(Time::getTime())) { throw new ExpiredCertException("Identity is invalid. Certificate expired."); } }
/** * Validate the signature and date of the message, then * decrypt it. * * @param string $secret * @param string $body * @param string $signature * @return string * Plain text. * @throws InvalidMessageException */ public static function authenticateThenDecrypt($secret, $body, $signature) { $keys = self::deriveAesKeys($secret); $localHmac = hash_hmac('sha256', $body, $keys['auth']); if (!self::hash_compare($signature, $localHmac)) { throw new InvalidMessageException("Incorrect hash"); } list($jsonEnvelope, $jsonEncrypted) = explode(Constants::PROTOCOL_DELIM, $body, 2); if (strlen($jsonEnvelope) > Constants::MAX_ENVELOPE_BYTES) { throw new InvalidMessageException("Oversized envelope"); } $envelope = json_decode($jsonEnvelope, TRUE); if (!$envelope) { throw new InvalidMessageException("Malformed envelope"); } if (!is_numeric($envelope['ttl']) || Time::getTime() > $envelope['ttl']) { throw new InvalidMessageException("Invalid TTL"); } if (!is_string($envelope['iv']) || strlen($envelope['iv']) !== Constants::AES_BYTES * 2 || !preg_match('/^[a-f0-9]+$/', $envelope['iv'])) { // AES_BYTES (32) ==> bin2hex ==> 2 hex digits (4-bit) per byte (8-bit) throw new InvalidMessageException("Malformed initialization vector"); } $jsonPlaintext = UserError::adapt('Civi\\Cxn\\Rpc\\Exception\\InvalidMessageException', function () use($jsonEncrypted, $envelope, $keys) { $cipher = new \Crypt_AES(CRYPT_AES_MODE_CBC); $cipher->setKeyLength(Constants::AES_BYTES); $cipher->setKey($keys['enc']); $cipher->setIV(BinHex::hex2bin($envelope['iv'])); return $cipher->decrypt($jsonEncrypted); }); return $jsonPlaintext; }