Moodle authentication plugin for Macaroons

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. <?php
  2. namespace Macaroons;
  3. use Macaroons\Exceptions\SignatureMismatchException;
  4. use Macaroons\Exceptions\CaveatUnsatisfiedException;
  5. class Verifier
  6. {
  7. private $predicates = array();
  8. private $callbacks = array();
  9. private $calculatedSignature;
  10. /**
  11. * return predicates to verify
  12. * @return Array|array
  13. */
  14. public function getPredicates()
  15. {
  16. return $this->predicates;
  17. }
  18. /**
  19. * returns verifier callbacks
  20. * @return Array|array
  21. */
  22. public function getCallbacks()
  23. {
  24. return $this->callbacks;
  25. }
  26. /**
  27. * sets array of predicates
  28. * @param Array $predicates
  29. */
  30. public function setPredicates(Array $predicates)
  31. {
  32. $this->predicates = $predicates;
  33. }
  34. /**
  35. * set array of callbacks
  36. * @param Array $callbacks
  37. */
  38. public function setCallbacks(Array $callbacks)
  39. {
  40. $this->callbacks = $callbacks;
  41. }
  42. /**
  43. * adds a predicate to the verifier
  44. * @param string
  45. */
  46. public function satisfyExact($predicate)
  47. {
  48. if (!isset($predicate))
  49. throw new \InvalidArgumentException('Must provide predicate');
  50. array_push($this->predicates, $predicate);
  51. }
  52. /**
  53. * adds a callback to array of callbacks
  54. * $callback can be anything that is callable including objects
  55. * that implement __invoke
  56. * See http://php.net/manual/en/language.types.callable.php for more details
  57. * @param function|object|array
  58. */
  59. public function satisfyGeneral($callback)
  60. {
  61. if (!isset($callback))
  62. throw new \InvalidArgumentException('Must provide a callback function');
  63. if (!is_callable($callback))
  64. throw new \InvalidArgumentException('Callback must be a function');
  65. array_push($this->callbacks, $callback);
  66. }
  67. /**
  68. * [verify description]
  69. * @param Macaroon $macaroon
  70. * @param string $key
  71. * @param Array $dischargeMacaroons
  72. * @return boolean
  73. */
  74. public function verify(Macaroon $macaroon, $key, Array $dischargeMacaroons = array())
  75. {
  76. $key = Utils::generateDerivedKey($key);
  77. return $this->verifyDischarge(
  78. $macaroon,
  79. $macaroon,
  80. $key,
  81. $dischargeMacaroons
  82. );
  83. }
  84. /**
  85. * [verifyDischarge description]
  86. * @param Macaroon $rootMacaroon
  87. * @param Macaroon $macaroon
  88. * @param string $key
  89. * @param Array|array $dischargeMacaroons
  90. * @return boolean|throws SignatureMismatchException
  91. */
  92. public function verifyDischarge(Macaroon $rootMacaroon, Macaroon $macaroon, $key, Array $dischargeMacaroons = array())
  93. {
  94. $this->calculatedSignature = Utils::hmac($key, $macaroon->getIdentifier());
  95. $this->verifyCaveats($macaroon, $dischargeMacaroons);
  96. if ($rootMacaroon != $macaroon)
  97. {
  98. $this->calculatedSignature = $rootMacaroon->bindSignature(strtolower(Utils::hexlify($this->calculatedSignature)));
  99. }
  100. $signature = Utils::unhexlify($macaroon->getSignature());
  101. if ($this->signaturesMatch($this->calculatedSignature, $signature) === FALSE)
  102. {
  103. throw new SignatureMismatchException('Signatures do not match.');
  104. }
  105. return true;
  106. }
  107. /**
  108. * verifies all first and third party caveats of macaroon are valid
  109. * @param Macaroon
  110. * @param Array
  111. */
  112. private function verifyCaveats(Macaroon $macaroon, Array $dischargeMacaroons = array())
  113. {
  114. foreach ($macaroon->getCaveats() as $caveat)
  115. {
  116. $caveatMet = false;
  117. if ($caveat->isFirstParty())
  118. $caveatMet = $this->verifyFirstPartyCaveat($caveat);
  119. else if ($caveat->isThirdParty())
  120. $caveatMet = $this->verifyThirdPartyCaveat($caveat, $macaroon, $dischargeMacaroons);
  121. if (!$caveatMet)
  122. throw new CaveatUnsatisfiedException("Caveat not met. Unable to satisfy: {$caveat->getCaveatId()}");
  123. }
  124. }
  125. private function verifyFirstPartyCaveat(Caveat $caveat)
  126. {
  127. $caveatMet = false;
  128. if (in_array($caveat->getCaveatId(), $this->predicates))
  129. $caveatMet = true;
  130. else
  131. {
  132. foreach ($this->callbacks as $callback)
  133. {
  134. if ($callback($caveat->getCaveatId()))
  135. $caveatMet = true;
  136. }
  137. }
  138. if ($caveatMet)
  139. $this->calculatedSignature = Utils::signFirstPartyCaveat($this->calculatedSignature, $caveat->getCaveatId());
  140. return $caveatMet;
  141. }
  142. private function verifyThirdPartyCaveat(Caveat $caveat, Macaroon $rootMacaroon, Array $dischargeMacaroons)
  143. {
  144. $caveatMet = false;
  145. $dischargesMatchingCaveat = array_filter($dischargeMacaroons, function($discharge) use ($rootMacaroon, $caveat) {
  146. return $discharge->getIdentifier() === $caveat->getCaveatId();
  147. });
  148. $caveatMacaroon = array_shift($dischargesMatchingCaveat);
  149. if (!$caveatMacaroon)
  150. throw new CaveatUnsatisfiedException("Caveat not met. No discharge macaroon found for identifier: {$caveat->getCaveatId()}");
  151. $caveatKey = $this->extractCaveatKey($this->calculatedSignature, $caveat);
  152. $caveatMacaroonVerifier = new Verifier();
  153. $caveatMacaroonVerifier->setPredicates($this->predicates);
  154. $caveatMacaroonVerifier->setCallbacks($this->callbacks);
  155. $caveatMet = $caveatMacaroonVerifier->verifyDischarge(
  156. $rootMacaroon,
  157. $caveatMacaroon,
  158. $caveatKey,
  159. $dischargeMacaroons
  160. );
  161. if ($caveatMet)
  162. {
  163. $this->calculatedSignature = Utils::signThirdPartyCaveat(
  164. $this->calculatedSignature,
  165. $caveat->getVerificationId(),
  166. $caveat->getCaveatId()
  167. );
  168. }
  169. return $caveatMet;
  170. }
  171. /**
  172. * returns the derived key from the caveat verification id
  173. * @param string $signature
  174. * @param Caveat $caveat
  175. * @return string
  176. */
  177. private function extractCaveatKey($signature, Caveat $caveat)
  178. {
  179. $verificationHash = $caveat->getVerificationId();
  180. $nonce = substr($verificationHash, 0, \Sodium\CRYPTO_SECRETBOX_NONCEBYTES);
  181. $verificationId = substr($verificationHash, \Sodium\CRYPTO_SECRETBOX_NONCEBYTES);
  182. $key = Utils::truncateOrPad($signature);
  183. return \Sodium\crypto_secretbox_open($verificationId, $nonce, $key);
  184. }
  185. /**
  186. * compares the calculated signature of a macaroon and the macaroon supplied
  187. * by the client
  188. * The user supplied string MUST be the second argument or this will leak
  189. * the length of the actual signature
  190. * @param string $a known signature from our key and macaroon metadata
  191. * @param string $b signature from macaroon we are verifying (from the client)
  192. * @return boolean
  193. */
  194. private function signaturesMatch($a, $b)
  195. {
  196. $ret = strlen($a) ^ strlen($b);
  197. $ret |= array_sum(unpack("C*", $a^$b));
  198. return !$ret;
  199. }
  200. }