From cf83bc8c5d7b19fd4cc420ed05d658ecb8829a77 Mon Sep 17 00:00:00 2001 From: eater <=@eater.me> Date: Wed, 27 Nov 2019 19:44:33 +0100 Subject: [PATCH] Add stack --- resources/generic.h | 27 +++ resources/openssl.h | 5 +- src/OpenSSL.php | 17 ++ src/OpenSSL/BIO.php | 19 ++- src/OpenSSL/C/CBackedObject.php | 50 +++++- src/OpenSSL/C/CBackedObjectWithOwner.php | 24 ++- src/OpenSSL/C/Memory.php | 6 + src/OpenSSL/PKCS7.php | 13 +- src/OpenSSL/Stack.php | 204 +++++++++++++++++++++++ src/OpenSSL/Stack/X509Stack.php | 24 +++ tests/BIOTest.php | 11 ++ tests/PKCS7Test.php | 2 +- tests/StackTest.php | 131 +++++++++++++++ 13 files changed, 521 insertions(+), 12 deletions(-) create mode 100644 src/OpenSSL/Stack.php create mode 100644 src/OpenSSL/Stack/X509Stack.php create mode 100644 tests/StackTest.php diff --git a/resources/generic.h b/resources/generic.h index 1a0a03b..707de17 100644 --- a/resources/generic.h +++ b/resources/generic.h @@ -15,3 +15,30 @@ struct buf_mem_st { typedef struct buf_mem_st BUF_MEM; typedef long int time_t; + +// Stack functions +int sk_num(const _STACK *); + +void *sk_value(const _STACK *, int); +void *sk_set(_STACK *, int, void *); + +_STACK *sk_new(int (*cmp)(const void *, const void *)); +_STACK *sk_new_null(void); +void sk_free(_STACK *); +void sk_pop_free(_STACK *st, void (*func)(void *)); +int sk_insert(_STACK *sk, void *data, int where); +void *sk_delete(_STACK *st, int loc); +void *sk_delete_ptr(_STACK *st, void *p); +int sk_find(_STACK *st, void *data); +int sk_find_ex(_STACK *st, void *data); +int sk_push(_STACK *st, void *data); +int sk_unshift(_STACK *st, void *data); +void *sk_shift(_STACK *st); +void *sk_pop(_STACK *st); +void sk_zero(_STACK *st); +int (*sk_set_cmp_func(_STACK *sk, + int (*c)(const void *, const void *)))(const void *, + const void *); +_STACK *sk_dup(_STACK *st); +void sk_sort(_STACK *st); +int sk_is_sorted(const _STACK *st); diff --git a/resources/openssl.h b/resources/openssl.h index 3b2f51f..1f4d6a9 100644 --- a/resources/openssl.h +++ b/resources/openssl.h @@ -6,4 +6,7 @@ void OPENSSL_add_all_algorithms_conf(void); void OPENSSL_add_all_algorithms_noconf(void); void ERR_load_crypto_strings(void); -void ERR_free_strings(void); \ No newline at end of file +void ERR_free_strings(void); + +unsigned long ERR_get_error(void); +char *ERR_error_string(unsigned long e, char *buf); \ No newline at end of file diff --git a/src/OpenSSL.php b/src/OpenSSL.php index 2622545..0b85c09 100644 --- a/src/OpenSSL.php +++ b/src/OpenSSL.php @@ -3,6 +3,7 @@ namespace Cijber; +use Cijber\OpenSSL\FFIWrapper; use Cijber\OpenSSL\Instance; use FFI; @@ -49,4 +50,20 @@ class OpenSSL { return static::getStdLib()->malloc($size); } + + public static function consumeErrors(): array + { + $ffi = static::getFFI(); + $errs = []; + while (0 !== ($code = $ffi->ERR_get_error())) { + $errs[] = FFI::string($ffi->ERR_error_string($code, null)); + } + + return $errs; + } + + public static function addressOf(FFI\CData $data): int + { + return FFI::cast("long long", $data)->cdata; + } } diff --git a/src/OpenSSL/BIO.php b/src/OpenSSL/BIO.php index 2cf145a..b64442f 100644 --- a/src/OpenSSL/BIO.php +++ b/src/OpenSSL/BIO.php @@ -205,7 +205,8 @@ class BIO extends CBackedObjectWithOwner public static function new(): BIO { $ffi = OpenSSL::getFFI(); - $bio = $ffi->BIO_new($ffi->BIO_s_mem()); + $mem = $ffi->BIO_s_mem(); + $bio = $ffi->BIO_new($mem); return new BIO($ffi, $bio); } @@ -253,6 +254,8 @@ class BIO extends CBackedObjectWithOwner */ public function write(string $data): int { + $this->ensureNotFreed(); + $len = $this->ffi->BIO_write($this->cObj, $data, strlen($data)); if ($len === -2) { throw new RuntimeException("Can't wrote to this BIO"); @@ -276,6 +279,8 @@ class BIO extends CBackedObjectWithOwner */ public function getType(): int { + $this->ensureNotFreed(); + return $this->ffi->BIO_method_type($this->cObj); } @@ -287,6 +292,8 @@ class BIO extends CBackedObjectWithOwner */ public function read(int $chunkSize = 4096): string { + $this->ensureNotFreed(); + $data = OpenSSL\C\Memory::new($chunkSize); $len = $this->ffi->BIO_read($this->cObj, $data->get(), $chunkSize); if ($len === -2) { @@ -311,6 +318,8 @@ class BIO extends CBackedObjectWithOwner */ public function tell() { + $this->ensureNotFreed(); + if (($this->getType() & self::TYPE_FILE) !== self::TYPE_FILE) { throw new RuntimeException("Can't tell on non-file BIO"); } @@ -329,6 +338,8 @@ class BIO extends CBackedObjectWithOwner */ public function reset(): void { + $this->ensureNotFreed(); + $res = (int)$this->ctrl(self::CTRL_RESET, 0, null); if (($this->getType() & self::TYPE_FILE) === self::TYPE_FILE && $res === 0) { @@ -349,6 +360,8 @@ class BIO extends CBackedObjectWithOwner */ public function seek(int $offset) { + $this->ensureNotFreed(); + if (($this->getType() & self::TYPE_FILE) !== self::TYPE_FILE) { throw new RuntimeException("Can't seek in non-file BIO"); } @@ -367,6 +380,8 @@ class BIO extends CBackedObjectWithOwner */ public function eof(): bool { + $this->ensureNotFreed(); + return (int)$this->ctrl(self::CTRL_EOF, 0, null) === 1; } @@ -380,6 +395,8 @@ class BIO extends CBackedObjectWithOwner */ public function ctrl(int $prop, int $larg = 0, $parg = null) { + $this->ensureNotFreed(); + return $this->ffi->BIO_ctrl($this->cObj, $prop, $larg, $parg); } } diff --git a/src/OpenSSL/C/CBackedObject.php b/src/OpenSSL/C/CBackedObject.php index 6dc1d07..923c870 100644 --- a/src/OpenSSL/C/CBackedObject.php +++ b/src/OpenSSL/C/CBackedObject.php @@ -11,15 +11,19 @@ class CBackedObject const TYPE = "void*"; protected CData $cObj; + protected bool $managed = true; + protected int $refCount = 0; protected bool $freed = false; /** * CBackedObject constructor. * @param CData $cObj + * @param bool $managed */ - protected function __construct(CData $cObj) + protected function __construct(CData $cObj, bool $managed = true) { $this->cObj = $cObj; + $this->managed = $managed; } /** @@ -30,17 +34,30 @@ class CBackedObject $this->freed = true; } + public function isFreed(): bool + { + return $this->freed; + } + + public function ensureNotFreed() + { + if ($this->isFreed()) { + throw new \RuntimeException("object " . get_class($this) . " already freed, can't be used"); + } + } + /** * Free backing C object, object is useless after this operation */ - final public function free() + final public function free(): bool { - if ($this->freed) { - return; + if ($this->freed || (!$this->managed || $this->refCount > 0)) { + return false; } $this->freeObject(); $this->freed(); + return true; } protected function freeObject() @@ -52,4 +69,29 @@ class CBackedObject { $this->free(); } + + public function unmanaged() + { + $this->managed = false; + } + + public function pushRefCount() + { + $this->refCount++; + } + + public function decreaseRefCount() + { + $this->refCount--; + } + + public function getRefCount() + { + return $this->refCount; + } + + public function managed() + { + $this->managed = true; + } } diff --git a/src/OpenSSL/C/CBackedObjectWithOwner.php b/src/OpenSSL/C/CBackedObjectWithOwner.php index a145d68..abba79d 100644 --- a/src/OpenSSL/C/CBackedObjectWithOwner.php +++ b/src/OpenSSL/C/CBackedObjectWithOwner.php @@ -3,17 +3,30 @@ namespace Cijber\OpenSSL\C; +use Cijber\OpenSSL\FFIWrapper; use FFI; use FFI\CData; class CBackedObjectWithOwner extends CBackedObject { + static private array $known = []; + private int $address = -1; + protected FFI $ffi; protected function __construct(FFI $ffi, CData $cObj) { parent::__construct($cObj); + $x = FFI::cast("long long", $cObj); + $this->address = $x->cdata; $this->ffi = $ffi; + static::$known[$this->address] = $this; + } + + public function freed() + { + unset(static::$known[$this->address]); + parent::freed(); } /** @@ -26,6 +39,15 @@ class CBackedObjectWithOwner extends CBackedObject */ public static function cast(FFI $ffi, CData $cData) { - return new static($ffi, FFI::cast(static::TYPE, $cData)); + /** + * Cast first, so it acts like a pointer + */ + $casted = $ffi->cast(static::TYPE, $cData); + $address = FFI::cast("long long", $casted)->cdata; + if (array_key_exists($address, static::$known)) { + return static::$known[$address]; + } + + return new static($ffi, $casted); } } diff --git a/src/OpenSSL/C/Memory.php b/src/OpenSSL/C/Memory.php index 1d9c3f6..8b4d47a 100644 --- a/src/OpenSSL/C/Memory.php +++ b/src/OpenSSL/C/Memory.php @@ -24,11 +24,15 @@ class Memory extends CBackedObject public function get(): CData { + $this->ensureNotFreed(); + return $this->cObj; } public function string(int $length, int $offset = 0) { + $this->ensureNotFreed(); + return substr(FFI::string($this->cObj, $length + $offset), $offset); } @@ -42,6 +46,8 @@ class Memory extends CBackedObject public function pointer(): CData { + $this->ensureNotFreed(); + return FFI::addr($this->cObj); } } diff --git a/src/OpenSSL/PKCS7.php b/src/OpenSSL/PKCS7.php index 611defe..6410537 100644 --- a/src/OpenSSL/PKCS7.php +++ b/src/OpenSSL/PKCS7.php @@ -6,6 +6,7 @@ namespace Cijber\OpenSSL; use Cijber\OpenSSL; use Cijber\OpenSSL\C\Memory; use FFI; +use RuntimeException; class PKCS7 extends OpenSSL\C\CBackedObjectWithOwner { @@ -33,8 +34,10 @@ class PKCS7 extends OpenSSL\C\CBackedObjectWithOwner public function toSigned(): PKCS7\Signed { + $this->ensureNotFreed(); + if ($this->getType() !== self::NID_SIGNED) { - throw new \RuntimeException("This PKCS7 isn't of type signed"); + throw new RuntimeException("This PKCS7 isn't of type signed"); } return new PKCS7\Signed($this); @@ -47,6 +50,8 @@ class PKCS7 extends OpenSSL\C\CBackedObjectWithOwner */ public function toDER(): string { + $this->ensureNotFreed(); + // Create NULL uint8_t* $buf = $this->ffi->new("uint8_t*"); // Get pointer from it (ptr being now uint8_t**) @@ -54,7 +59,7 @@ class PKCS7 extends OpenSSL\C\CBackedObjectWithOwner // Give NULL pointer to OpenSSL and let it fill it up $len = $this->ffi->i2d_PKCS7($this->cObj, $ptr); if ($len < 0) { - throw new \RuntimeException("Failed to create DER from PKCS7 object"); + throw new RuntimeException("Failed to create DER from PKCS7 object"); } // Read string from pointer @@ -99,10 +104,10 @@ class PKCS7 extends OpenSSL\C\CBackedObjectWithOwner $res = $ffi->d2i_PKCS7(null, $mem->pointer(), $derLen); if ($res === null) { - throw new \RuntimeException("Failed loading DER"); + throw new RuntimeException("Failed loading DER"); } $mem->freed(); - return new static($ffi, $res); + return static::cast($ffi, $res); } } diff --git a/src/OpenSSL/Stack.php b/src/OpenSSL/Stack.php new file mode 100644 index 0000000..9d55ed3 --- /dev/null +++ b/src/OpenSSL/Stack.php @@ -0,0 +1,204 @@ +sk_new_null(); + return new static($ffi, $cObj); + } + + protected function freeObject() + { + $this->ffi->sk_free($this->cObj); + } + + public function count(): int + { + return $this->ffi->sk_num($this->cObj); + } + + public function get(int $offset) + { + $res = $this->ffi->sk_value($this->cObj, $offset); + + if ($res === null) { + throw new RuntimeException("Failed to retrieve item from stack"); + } + + return $this->spawn($res); + } + + public function set(int $offset, CBackedObject $object): void + { + /*** + * Make sure ref count is updated for old object + */ + /** @var CBackedObjectWithOwner $obj */ + $obj = $this->get($offset); + $res = $this->ffi->sk_set($this->cObj, $offset, $object->cObj); + + if ($res === null) { + throw new RuntimeException("Failed to set item on stack"); + } + + $obj->decreaseRefCount(); + } + + public function shift() + { + $cObj = $this->ffi->sk_shift($this->cObj); + return $this->handleResult($cObj); + } + + protected function handleResult(?CData $cObj) + { + if ($cObj === null) { + return null; + } + + /** @var CBackedObjectWithOwner $obj */ + $obj = $this->spawn($cObj); + $obj->decreaseRefCount(); + return $obj; + } + + public function pop() + { + $cObj = $this->ffi->sk_pop($this->cObj); + return $this->handleResult($cObj); + } + + public function push(CBackedObject $object): int + { + $this->ensureCorrect($object); + $idx = $this->ffi->sk_push($this->cObj, $object->cObj); + if ($idx === 0) { + throw new RuntimeException("Failed to insert element"); + } + + $object->pushRefCount(); + + return $idx; + } + + private function ensureCorrect($object) + { + $expectedClassName = static::CLASSNAME; + + if (!($object instanceof $expectedClassName)) { + throw new InvalidArgumentException("Expected object of class $expectedClassName got object of class " . get_class($object)); + } + } + + public function unshift(CBackedObject $object): int + { + $this->ensureCorrect($object); + + $idx = $this->ffi->sk_unshift($this->cObj, $object->cObj); + if ($idx === 0) { + throw new RuntimeException("Failed to insert element"); + } + + $object->pushRefCount(); + + return $idx; + } + + public function freeAll() + { + $this->ffi->sk_pop_free($this->cObj, function (CData $cObj) { + /** @var CBackedObject $cObj */ + $cObj = $this->spawn($cObj); + $cObj->decreaseRefCount(); + $cObj->free(); + }); + + $this->freed(); + } + + public function offsetExists($offset) + { + return $offset >= 0 && $offset < $this->count(); + } + + public function offsetGet($offset) + { + return $this->get($offset); + } + + public function offsetSet($offset, $value) + { + if ($offset === null) { + $this->push($value); + return; + } + + $this->set($offset, $value); + } + + public function offsetUnset($offset) + { + return $this->delete($offset); + } + + public function delete($offset) + { + $obj = $this->ffi->sk_delete($this->cObj, $offset); + if ($obj === null) { + throw new RuntimeException("Failed to delete element $offset from stack"); + } + + /** @var CBackedObjectWithOwner $phpObj */ + $phpObj = $this->spawn($obj); + $phpObj->decreaseRefCount(); + + return $phpObj; + } + + public function getIterator() + { + for ($i = 0; $i < $this->count(); $i++) { + yield $this[$i]; + } + } + + public function __clone() + { + $cObj = $this->ffi->sk_dup($this->cObj); + + if ($cObj === null) { + throw new RuntimeException("Failed to clone stack"); + } + + /** @var CBackedObjectWithOwner $obj */ + foreach ($this as $obj) { + $obj->pushRefCount(); + } + + return new static($this->ffi, $cObj); + } +} diff --git a/src/OpenSSL/Stack/X509Stack.php b/src/OpenSSL/Stack/X509Stack.php new file mode 100644 index 0000000..827b922 --- /dev/null +++ b/src/OpenSSL/Stack/X509Stack.php @@ -0,0 +1,24 @@ +ffi, $cData); + } +} diff --git a/tests/BIOTest.php b/tests/BIOTest.php index e9d3919..ca3211a 100644 --- a/tests/BIOTest.php +++ b/tests/BIOTest.php @@ -6,6 +6,7 @@ namespace Cijber\OpenSSL\Tests; use Cijber\OpenSSL\BIO; use PHPUnit\Framework\TestCase; +use RuntimeException; class BIOTest extends TestCase { @@ -46,4 +47,14 @@ class BIOTest extends TestCase $part = $bio->read(3); $this->assertEquals("Hel", $part); } + + public function testUsingObjectAfterFree() + { + $bio = BIO::buffer("Hello world"); + $this->assertTrue($bio->free()); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage("object Cijber\OpenSSL\BIO already freed, can't be used"); + $bio->getType(); + } } \ No newline at end of file diff --git a/tests/PKCS7Test.php b/tests/PKCS7Test.php index ded2b74..66f9434 100644 --- a/tests/PKCS7Test.php +++ b/tests/PKCS7Test.php @@ -20,8 +20,8 @@ class PKCS7Test extends TestCase $der = file_get_contents(__DIR__ . "/data/pkcs7/1.RSA"); $pkcs7 = PKCS7::loadFromDER($der); $newDer = $pkcs7->toDER(); - $this->assertEquals($der, $newDer); $this->assertEquals(PKCS7::NID_SIGNED, $pkcs7->getType()); + $this->assertEquals($der, $newDer); } public function testLoadingGarbageDER() diff --git a/tests/StackTest.php b/tests/StackTest.php new file mode 100644 index 0000000..a6bc604 --- /dev/null +++ b/tests/StackTest.php @@ -0,0 +1,131 @@ +assertCount(0, $stack); + } + + function testPush() + { + $stack = Stack\X509Stack::new(); + $x509 = X509::new(); + $stack->push($x509); + $this->assertCount(1, $stack); + $x509Item = $stack->get(0); + $this->assertSame($x509, $x509Item); + } + + function testSet() + { + $stack = Stack\X509Stack::new(); + $a = X509::new(); + $b = X509::new(); + + $stack->push($a); + $stack->set(0, $b); + $this->assertNotSame($a, $stack->get(0)); + $this->assertCount(1, $stack); + $this->assertEquals(0, $a->getRefCount()); + } + + function testDelete() + { + $stack = Stack\X509Stack::new(); + $a = X509::new(); + $c = X509::new(); + $stack->push($a); + $stack->push($c); + $this->assertEquals(1, $a->getRefCount()); + $b = $stack->delete(0); + $this->assertSame($a, $b); + $this->assertCount(1, $stack); + $this->assertEquals(0, $a->getRefCount()); + unset($stack[0]); + $this->assertCount(0, $stack); + $this->assertEquals(0, $c->getRefCount()); + + $this->expectException(\RuntimeException::class); + $stack->delete(1); + } + + function testRetention() + { + $stack = Stack\X509Stack::new(); + $x509 = X509::new(); + $stack->push($x509); + $this->assertCount(1, $stack); + $x509Item = $stack->get(0); + $this->assertSame($x509, $x509Item); + $this->assertEquals(1, $x509->getRefCount()); + unset($x509Item, $x509); + + /** @var X509 $x509 */ + $x509 = $stack->get(0); + $this->assertFalse($x509->free()); + $stack->freeAll(); + $this->assertEquals(0, $x509->getRefCount()); + $this->assertTrue($stack->isFreed()); + $this->assertTrue($x509->isFreed()); + } + + function testAddressingCorrect() + { + $ffi = OpenSSL::getFFI(); + $x509 = $ffi->X509_new(); + $address = OpenSSL::addressOf($x509); + $st = $ffi->sk_new_null(); + $ffi->sk_push($st, $x509); + $second = $ffi->cast("X509*", $ffi->sk_value($st, 0)); + $secondAddress = OpenSSL::addressOf($second); + $this->assertEquals($address, $secondAddress); + } + + function testForeach() + { + $stack = Stack\X509Stack::new(); + $a = X509::new(); + $b = X509::new(); + $c = X509::new(); + $stack->push($a); + $stack->push($b); + $stack->push($c); + + $x = 0; + foreach ($stack as $x509) { + $x++; + $this->assertEquals(X509::class, get_class($x509)); + } + + } + + function testPop() + { + $stack = Stack\X509Stack::new(); + $a = X509::new(); + $b = X509::new(); + $c = X509::new(); + $stack->push($a); + $stack->push($b); + $stack[] = $c; + + $i = 0; + while ($x509 = $stack->pop()) { + $this->assertEquals(X509::class, get_class($x509)); + $i++; + } + + $this->assertEquals(3, $i); + } +} \ No newline at end of file