diff --git a/.github/workflows/integrate.yml b/.github/workflows/integrate.yml index 56d7749..6d10b17 100644 --- a/.github/workflows/integrate.yml +++ b/.github/workflows/integrate.yml @@ -65,7 +65,7 @@ jobs: uses: "shivammathur/setup-php@v2" with: php-version: "${{ matrix.php-version }}" - extensions: "ctype, dom, json, libxml, mbstring, openssl, phar, simplexml, tokenizer, xml, xmlwriter" + extensions: "ctype, dom, json, libxml, mbstring, openssl, phar, simplexml, tokenizer, xml, xmlwriter, zlib" coverage: "xdebug" - name: "Checkout code" @@ -99,7 +99,7 @@ jobs: uses: "shivammathur/setup-php@v2" with: php-version: "8.1" - extensions: "ctype, dom, json, libxml, mbstring, openssl, phar, simplexml, tokenizer, xml, xmlwriter" + extensions: "ctype, dom, json, libxml, mbstring, openssl, phar, simplexml, tokenizer, xml, xmlwriter, zlib" coverage: "none" - name: "Checkout code" @@ -131,7 +131,7 @@ jobs: uses: "shivammathur/setup-php@v2" with: php-version: "8.1" - extensions: "ctype, dom, json, libxml, mbstring, openssl, phar, simplexml, tokenizer, xml, xmlwriter" + extensions: "ctype, dom, json, libxml, mbstring, openssl, phar, simplexml, tokenizer, xml, xmlwriter, zlib" coverage: "none" - name: "Checkout code" @@ -164,7 +164,7 @@ jobs: uses: "shivammathur/setup-php@v2" with: php-version: "8.1" - extensions: "ctype, dom, json, libxml, mbstring, openssl, phar, simplexml, tokenizer, xml, xmlwriter" + extensions: "ctype, dom, json, libxml, mbstring, openssl, phar, simplexml, tokenizer, xml, xmlwriter, zlib" coverage: "xdebug" - name: "Checkout code" @@ -193,7 +193,7 @@ jobs: uses: "shivammathur/setup-php@v2" with: php-version: "8.1" - extensions: "ctype, dom, json, libxml, mbstring, openssl, phar, simplexml, tokenizer, xml, xmlwriter" + extensions: "ctype, dom, json, libxml, mbstring, openssl, phar, simplexml, tokenizer, xml, xmlwriter, zlib" coverage: "xdebug" - name: "Checkout code" diff --git a/composer.json b/composer.json index baab9c3..f5dda07 100644 --- a/composer.json +++ b/composer.json @@ -29,18 +29,19 @@ } }, "require-dev": { - "infection/infection": "^0.26.12", + "infection/infection": "^0.27", "phpstan/phpstan": "^1.7", "phpstan/phpstan-deprecation-rules": "^1.0", "phpstan/phpstan-phpunit": "^1.1", "phpstan/phpstan-strict-rules": "^1.2", "phpunit/phpunit": "^10.0", - "rector/rector": "^0.15", + "rector/rector": "^0.16", "symplify/easy-coding-standard": "^11.0", "symfony/phpunit-bridge": "^6.1", "ekino/phpstan-banned-code": "^1.0", "php-parallel-lint/php-parallel-lint": "^1.3", - "qossmic/deptrac-shim": "^1.0" + "qossmic/deptrac-shim": "^1.0", + "spomky-labs/cbor-php": "^3.0" }, "autoload-dev": { "psr-4": { @@ -49,11 +50,13 @@ }, "config": { "allow-plugins": { - "infection/extension-installer": false + "infection/extension-installer": true, + "phpstan/extension-installer": true } }, "suggest": { "ext-gmp": "For better performance, please install either GMP (recommended) or BCMath extension", - "ext-bcmath": "For better performance, please install either GMP (recommended) or BCMath extension" + "ext-bcmath": "For better performance, please install either GMP (recommended) or BCMath extension", + "spomky-labs/cbor-php": "For COSE Signature support" } } diff --git a/src/Signature/CoseSign1Tag.php b/src/Signature/CoseSign1Tag.php new file mode 100644 index 0000000..38863ca --- /dev/null +++ b/src/Signature/CoseSign1Tag.php @@ -0,0 +1,110 @@ +get(0); + $unprotectedHeader = $object->get(1); + $payload = $object->get(2); + $signature = $object->get(3); + + Assertion::isInstanceOf( + $protectedHeader, + ByteStringObject::class, + 'Not a valid CoseSign1 object. The item 1 shall be a ByteString object.' + ); + Assertion::isInstanceOf( + $unprotectedHeader, + MapObject::class, + 'Not a valid CoseSign1 object. The item 2 shall be a Map object.' + ); + Assertion::isInstanceOf( + $payload, + ByteStringObject::class, + 'Not a valid CoseSign1 object. The item 3 shall be a ByteString object.' + ); + Assertion::isInstanceOf( + $signature, + ByteStringObject::class, + 'Not a valid CoseSign1 object. The item 4 shall be a ByteString object.' + ); + + parent::__construct($additionalInformation, $data, $object); + $this->protectedHeader = $protectedHeader; + $this->unprotectedHeader = $unprotectedHeader; + $this->payload = $payload; + $this->signature = $signature; + } + + public static function getTagId(): int + { + return self::TAG_ID; + } + + public static function createFromLoadedData(int $additionalInformation, ?string $data, CBORObject $object): Base + { + return new self($additionalInformation, $data, $object); + } + + public static function create( + MapObject $protectedHeader, + MapObject $unprotectedHeader, + MapObject $payload, + ByteStringObject $signature + ): self { + $protectedHeaderAsBytesString = ByteStringObject::create((string) $protectedHeader); + $payloadAsBytesString = ByteStringObject::create((string) $payload); + $object = ListObject::create([ + $protectedHeaderAsBytesString, + $unprotectedHeader, + $payloadAsBytesString, + $signature, + ]); + + return new self(self::TAG_ID, null, $object); + } + + public function getProtectedHeader(): ByteStringObject + { + return $this->protectedHeader; + } + + public function getUnprotectedHeader(): MapObject + { + return $this->unprotectedHeader; + } + + public function getPayload(): ByteStringObject + { + return $this->payload; + } + + public function getSignature(): ByteStringObject + { + return $this->signature; + } +} diff --git a/src/Signature/Signature1.php b/src/Signature/Signature1.php new file mode 100644 index 0000000..4c67b9d --- /dev/null +++ b/src/Signature/Signature1.php @@ -0,0 +1,45 @@ +add(new TextStringObject('Signature1')); + $structure->add($this->protectedHeader); + $structure->add(new ByteStringObject('')); + $structure->add($this->payload); + + return (string) $structure; + } + + public static function create(ByteStringObject $protectedHeader, ByteStringObject $payload): self + { + return new self($protectedHeader, $payload); + } + + public function getProtectedHeader(): ByteStringObject + { + return $this->protectedHeader; + } + + public function getPayload(): ByteStringObject + { + return $this->payload; + } +} diff --git a/tests/Algorithm/Mac/HS256Test.php b/tests/Algorithm/Mac/HS256Test.php new file mode 100644 index 0000000..c10778f --- /dev/null +++ b/tests/Algorithm/Mac/HS256Test.php @@ -0,0 +1,80 @@ + $k, + SymmetricKey::TYPE => SymmetricKey::TYPE_OCT, + ]); + $hash = $algorithm->hash($data, $key); + + static::assertSame(5, HS256::identifier()); + static::assertSame($k, $key->k()); + static::assertSame($expectedHash, $hash); + } + + /** + * @test + * @dataProvider getVectors + */ + public function aMacCanBeVerified(string $k, string $data, string $hash): void + { + $algorithm = new HS256(); + $key = SymmetricKey::create([ + SymmetricKey::DATA_K => $k, + SymmetricKey::TYPE => SymmetricKey::TYPE_OCT, + ]); + $isValid = $algorithm->verify($data, $key, $hash); + + static::assertTrue($isValid); + } + + /** + * @test + */ + public function theKeyIsNotAcceptable(): void + { + static::expectException(InvalidArgumentException::class); + static::expectExceptionMessage('Invalid key. Must be of type symmetric'); + $algorithm = new HS256(); + $key = OkpKey::create([ + OkpKey::TYPE => SymmetricKey::TYPE_OKP, + OkpKey::DATA_CURVE => OkpKey::CURVE_X25519, + OkpKey::DATA_X => '', + ]); + $algorithm->hash( + 'eyJhbGciOiJIUzI1NiIsImtpZCI6IjAxOGMwYWU1LTRkOWItNDcxYi1iZmQ2LWVlZjMxNGJjNzAzNyJ9.SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb3V0IHlvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGUgcm9hZCwgYW5kIGlmIHlvdSBkb24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXigJlzIG5vIGtub3dpbmcgd2hlcmUgeW91IG1pZ2h0IGJlIHN3ZXB0IG9mZiB0by4', + $key + ); + } + + /** + * @return array[] + */ + public function getVectors(): iterable + { + yield [ + base64_decode('hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG+Onbc6mxCcYg', true), + 'eyJhbGciOiJIUzI1NiIsImtpZCI6IjAxOGMwYWU1LTRkOWItNDcxYi1iZmQ2LWVlZjMxNGJjNzAzNyJ9.SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb3V0IHlvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGUgcm9hZCwgYW5kIGlmIHlvdSBkb24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXigJlzIG5vIGtub3dpbmcgd2hlcmUgeW91IG1pZ2h0IGJlIHN3ZXB0IG9mZiB0by4', + base64_decode('s0h6KThzkfBBBkLspW1h84VsJZFTsPPqMDA7g1Md7p0', true), + ]; + } +} diff --git a/tests/Signature/CoseSign1Test.php b/tests/Signature/CoseSign1Test.php new file mode 100644 index 0000000..6e51e06 --- /dev/null +++ b/tests/Signature/CoseSign1Test.php @@ -0,0 +1,106 @@ +getDecoder(); + + //When + $cbor = $decoder->decode($stream); //We decode the data + + //Then + static::assertInstanceOf(CoseSign1Tag::class, $cbor, 'Invalid object'); + + return $cbor; + } + + /** + * @test + * @depends theCovidVaccinationPassCanBeLoaded + */ + public function theCovidVaccinationPassCanBeVerified(CoseSign1Tag $cbor): void + { + //Given + $structure = Signature1::create($cbor->getProtectedHeader(), $cbor->getPayload()); + $derSignature = ECSignature::toAsn1($cbor->getSignature()->normalize(), 64); + + //When + $isValid = openssl_verify((string) $structure, $derSignature, $this->getCertificate(), 'sha256'); + + //Then + static::assertSame(1, $isValid, 'Invalid signature'); + } + + private function getDecoder(): Decoder + { + $tagObjectManager = TagManager::create() + ->add(CoseSign1Tag::class) + ; + return Decoder::create($tagObjectManager, OtherObjectManager::create()); + } + + private function getCertificate(): string + { + return <<<'CODE_SAMPLE' +-----BEGIN CERTIFICATE----- +MIIGXjCCBBagAwIBAgIQQ50Ye2SIZLH9KhoLQeBFLjA9BgkqhkiG9w0BAQowMKAN +MAsGCWCGSAFlAwQCA6EaMBgGCSqGSIb3DQEBCDALBglghkgBZQMEAgOiAwIBQDBg +MQswCQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSEwHwYDVQQDExhE +LVRSVVNUIFRlc3QgQ0EgMi0yIDIwMTkxFzAVBgNVBGETDk5UUkRFLUhSQjc0MzQ2 +MB4XDTIxMDUwNjE5MjEzMFoXDTIyMDUwOTE5MjEzMFowfjELMAkGA1UEBhMCREUx +FDASBgNVBAoTC1ViaXJjaCBHbWJIMRQwEgYDVQQDEwtVYmlyY2ggR21iSDEOMAwG +A1UEBwwFS8O2bG4xHDAaBgNVBGETE0RUOkRFLVVHTk9UUFJPVklERUQxFTATBgNV +BAUTDENTTTAxNzI0OTU3MzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABBAvcrr3 +ib8nS7E6vmdWJ6k7d6rqBHlD0U41OdMP2dJf9xqec4uOlwfJdOriwncgcWRpmli7 +vbFVP9w9dxX++ESjggJfMIICWzAfBgNVHSMEGDAWgBRQdpKgGuyBrpHC3agJUmg3 +3lGETzAtBggrBgEFBQcBAwQhMB8wCAYGBACORgEBMBMGBgQAjkYBBjAJBgcEAI5G +AQYCMIH+BggrBgEFBQcBAQSB8TCB7jArBggrBgEFBQcwAYYfaHR0cDovL3N0YWdp +bmcub2NzcC5kLXRydXN0Lm5ldDBHBggrBgEFBQcwAoY7aHR0cDovL3d3dy5kLXRy +dXN0Lm5ldC9jZ2ktYmluL0QtVFJVU1RfVGVzdF9DQV8yLTJfMjAxOS5jcnQwdgYI +KwYBBQUHMAKGamxkYXA6Ly9kaXJlY3RvcnkuZC10cnVzdC5uZXQvQ049RC1UUlVT +VCUyMFRlc3QlMjBDQSUyMDItMiUyMDIwMTksTz1ELVRydXN0JTIwR21iSCxDPURF +P2NBQ2VydGlmaWNhdGU/YmFzZT8wFwYDVR0gBBAwDjAMBgorBgEEAaU0AgICMIG/ +BgNVHR8EgbcwgbQwgbGgga6ggauGcGxkYXA6Ly9kaXJlY3RvcnkuZC10cnVzdC5u +ZXQvQ049RC1UUlVTVCUyMFRlc3QlMjBDQSUyMDItMiUyMDIwMTksTz1ELVRydXN0 +JTIwR21iSCxDPURFP2NlcnRpZmljYXRlcmV2b2NhdGlvbmxpc3SGN2h0dHA6Ly9j +cmwuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3RfdGVzdF9jYV8yLTJfMjAxOS5jcmww +HQYDVR0OBBYEFHgZ4+qwUzVKynAvnUl5YL6XWUK9MA4GA1UdDwEB/wQEAwIGwDA9 +BgkqhkiG9w0BAQowMKANMAsGCWCGSAFlAwQCA6EaMBgGCSqGSIb3DQEBCDALBglg +hkgBZQMEAgOiAwIBQAOCAgEAHNnaBolwPHWiEZ6QKD6iIFFQhEiYzWvQxxvas1NQ +Sd/Xhw1Bth81aG5HRV1GCciD7Pa0yRl3wN3Dlixw2zdaU76kJlwYoXBbP6c0BQxV +lMFgWPEmG4Gt4+CrmcJ7EsrtYHeCZ7WiOuV1PJ2Pdb1Rsj1sxAhJxkv3I4eQrwlu +b3qHbQaT6uXV9X2V3qyqKPi0X12vzr9c0ca8D5GDD4+PgdGTraGU029YVeEKLe+F +qEgYVsEo0l9eSzNLp8HYuHr++5OU63pSBpTJmW7gI39VHkiEwZE87RkbuVQvFYcT +5rmqM9TIgcJVtHoUozhsitoMjL7zlx5aFTHMxnqSh7D7H0kwXgYM/wM8TQ++AV2v +gRK5q0mGp2MJPWuWRjtWrjxth71dF+pQr3Ls6hJXg1yMweVLzkd8mIzTnmtgtwP2 +pFgrSP1zW1B8ThBtb7ldXfcenP7qlOG/JyldLxy2hJjYgRST1TCPQMfeJ3yF/ONo +fxPMAefqfoadzm7BFPHBNOkaJIZ09+QJqZAS+pIoYFImrswjiykn5ZruspEYj0Tc +P5wzV01e+KTaHweT3Ii+j7ZJcUha+9OosmkhTc02g2BxzliB+PmexyY9JZkXPA8V +xF/0c/gGysbrPQtz3n09XfX/JX9Hh0cMPs4YZHk5xUpLsrKPivSCR1wJC7tCvC6J +1Xk= +-----END CERTIFICATE----- +CODE_SAMPLE; + } +} diff --git a/tests/Signature/ECSignature.php b/tests/Signature/ECSignature.php new file mode 100644 index 0000000..ebf0fd1 --- /dev/null +++ b/tests/Signature/ECSignature.php @@ -0,0 +1,72 @@ + self::ASN1_MAX_SINGLE_BYTE ? self::ASN1_LENGTH_2BYTES : ''; + + return hex2bin( + self::ASN1_SEQUENCE + . $lengthPrefix . dechex($totalLength) + . self::ASN1_INTEGER . dechex($lengthR) . $pointR + . self::ASN1_INTEGER . dechex($lengthS) . $pointS + ); + } + + private static function octetLength(string $data): int + { + return (int) (mb_strlen($data, '8bit') / self::BYTE_SIZE); + } + + private static function preparePositiveInteger(string $data): string + { + if (mb_substr($data, 0, self::BYTE_SIZE, '8bit') > self::ASN1_BIG_INTEGER_LIMIT) { + return self::ASN1_NEGATIVE_INTEGER . $data; + } + + while (mb_strpos($data, self::ASN1_NEGATIVE_INTEGER, 0, '8bit') === 0 + && mb_substr($data, 2, self::BYTE_SIZE, '8bit') <= self::ASN1_BIG_INTEGER_LIMIT) { + $data = mb_substr($data, 2, null, '8bit'); + } + + return $data; + } +}