Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ECDSA Alg improvements #276

Merged
merged 7 commits into from
Jan 27, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ jobs:
- mv ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini{.disabled,}
- if [[ ! $(php -m | grep -si xdebug) ]]; then echo "xdebug required for coverage"; exit 1; fi
script:
- ./vendor/bin/infection --test-framework-options="--testsuite=unit" -s --threads=$(nproc) --min-msi=97 --min-covered-msi=98
- ./vendor/bin/infection --test-framework-options="--testsuite=unit" -s --threads=$(nproc) --min-msi=95 --min-covered-msi=95

- stage: Metrics and quality
env: STATIC_ANALYSIS
Expand Down
2 changes: 0 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,8 @@
],
"require": {
"php": "^7.2",
"ext-gmp": "*",
"ext-mbstring": "*",
"ext-openssl": "*",
"fgrosse/phpasn1": "^2.0",
"lcobucci/clock": "^1.0",
"lcobucci/jose-parsing": "~2.1"
},
Expand Down
3 changes: 0 additions & 3 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,3 @@ parameters:
paths:
- src
- test

ignoreErrors:
- '#FG\\ASN1\\Universal\\Integer constructor expects int, string given.#'
18 changes: 9 additions & 9 deletions src/Signer/Ecdsa.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,33 @@

namespace Lcobucci\JWT\Signer;

use Lcobucci\JWT\Signer\Ecdsa\Asn1;
use Lcobucci\JWT\Signer\Ecdsa\PointsManipulator;
use Lcobucci\JWT\Signer\Ecdsa\MultibyteStringConverter;
use Lcobucci\JWT\Signer\Ecdsa\SignatureConverter;
use const OPENSSL_KEYTYPE_EC;

abstract class Ecdsa extends OpenSSL
{
/**
* @var PointsManipulator
* @var SignatureConverter
*/
private $manipulator;
private $converter;

public function __construct(PointsManipulator $manipulator)
public function __construct(SignatureConverter $converter)
{
$this->manipulator = $manipulator;
$this->converter = $converter;
}

public static function create(): Ecdsa
{
return new static(new Asn1());
return new static(new MultibyteStringConverter());
}

/**
* {@inheritdoc}
*/
final public function sign(string $payload, Key $key): string
{
return $this->manipulator->fromEcPoint(
return $this->converter->fromAsn1(
$this->createSignature($key->getContent(), $key->getPassphrase(), $payload),
$this->getKeyLength()
);
Expand All @@ -41,7 +41,7 @@ final public function sign(string $payload, Key $key): string
final public function verify(string $expected, string $payload, Key $key): bool
{
return $this->verifySignature(
$this->manipulator->toEcPoint($expected, $this->getKeyLength()),
$this->converter->toAsn1($expected, $this->getKeyLength()),
$payload,
$key->getContent()
);
Expand Down
77 changes: 0 additions & 77 deletions src/Signer/Ecdsa/Asn1.php

This file was deleted.

140 changes: 140 additions & 0 deletions src/Signer/Ecdsa/MultibyteStringConverter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
<?php
declare(strict_types=1);

/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2018 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*
* @link https://github.com/web-token/jwt-framework/blob/v1.2/src/Component/Core/Util/ECSignature.php
*/
namespace Lcobucci\JWT\Signer\Ecdsa;

use InvalidArgumentException;
use const STR_PAD_LEFT;
use function assert;
use function bin2hex;
use function dechex;
use function hex2bin;
use function hexdec;
use function is_string;
use function mb_strlen;
use function mb_substr;
use function str_pad;

/**
* ECDSA signature converter using ext-mbstring
*
* @internal
*/
final class MultibyteStringConverter implements SignatureConverter
{
private const ASN1_SEQUENCE = '30';
private const ASN1_INTEGER = '02';
private const ASN1_MAX_SINGLE_BYTE = 128;
private const ASN1_LENGTH_2BYTES = '81';
private const ASN1_BIG_INTEGER_LIMIT = '7f';
private const ASN1_NEGATIVE_INTEGER = '00';
private const BYTE_SIZE = 2;

public function toAsn1(string $signature, int $length): string
{
$signature = bin2hex($signature);

if (self::octetLength($signature) !== $length) {
throw new InvalidArgumentException('Invalid signature length.');
}

$pointR = self::preparePositiveInteger(mb_substr($signature, 0, $length, '8bit'));
$pointS = self::preparePositiveInteger(mb_substr($signature, $length, null, '8bit'));

$lengthR = self::octetLength($pointR);
$lengthS = self::octetLength($pointS);

$totalLength = $lengthR + $lengthS + self::BYTE_SIZE + self::BYTE_SIZE;
$lengthPrefix = $totalLength > self::ASN1_MAX_SINGLE_BYTE ? self::ASN1_LENGTH_2BYTES : '';

$asn1 = hex2bin(
self::ASN1_SEQUENCE
. $lengthPrefix . dechex($totalLength)
. self::ASN1_INTEGER . dechex($lengthR) . $pointR
. self::ASN1_INTEGER . dechex($lengthS) . $pointS
);
assert(is_string($asn1));

return $asn1;
}

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_substr($data, 0, self::BYTE_SIZE, '8bit') === self::ASN1_NEGATIVE_INTEGER
&& mb_substr($data, 2, self::BYTE_SIZE, '8bit') <= self::ASN1_BIG_INTEGER_LIMIT) {
$data = mb_substr($data, 2, null, '8bit');
}

return $data;
}

public function fromAsn1(string $signature, int $length): string
{
$message = bin2hex($signature);
$position = 0;

if (self::readAsn1Content($message, $position, self::BYTE_SIZE) !== self::ASN1_SEQUENCE) {
throw new InvalidArgumentException('Invalid data. Should start with a sequence.');
}

if (self::readAsn1Content($message, $position, self::BYTE_SIZE) === self::ASN1_LENGTH_2BYTES) {
$position += self::BYTE_SIZE;
}

$pointR = self::retrievePositiveInteger(self::readAsn1Integer($message, $position));
$pointS = self::retrievePositiveInteger(self::readAsn1Integer($message, $position));

$points = hex2bin(str_pad($pointR, $length, '0', STR_PAD_LEFT) . str_pad($pointS, $length, '0', STR_PAD_LEFT));
assert(is_string($points));

return $points;
}

private static function readAsn1Content(string $message, int &$position, int $length): string
{
$content = mb_substr($message, $position, $length, '8bit');
$position += $length;

return $content;
}

private static function readAsn1Integer(string $message, int &$position): string
{
if (self::readAsn1Content($message, $position, self::BYTE_SIZE) !== self::ASN1_INTEGER) {
throw new InvalidArgumentException('Invalid data. Should contain an integer.');
}

$length = (int) hexdec(self::readAsn1Content($message, $position, self::BYTE_SIZE));

return self::readAsn1Content($message, $position, $length * self::BYTE_SIZE);
}

private static function retrievePositiveInteger(string $data): string
{
while (mb_substr($data, 0, self::BYTE_SIZE, '8bit') === self::ASN1_NEGATIVE_INTEGER
&& mb_substr($data, 2, self::BYTE_SIZE, '8bit') > self::ASN1_BIG_INTEGER_LIMIT) {
$data = mb_substr($data, 2, null, '8bit');
}

return $data;
}
}
25 changes: 0 additions & 25 deletions src/Signer/Ecdsa/PointsManipulator.php

This file was deleted.

30 changes: 30 additions & 0 deletions src/Signer/Ecdsa/SignatureConverter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);

namespace Lcobucci\JWT\Signer\Ecdsa;

/**
* Manipulates the result of a ECDSA signature (points R and S) according to the
* JWA specs.
*
* OpenSSL creates a signature using the ASN.1 format and, according the JWA specs,
* the signature for JWTs must be the concatenated values of points R and S (in
* big-endian octet order).
*
* @internal
*
* @see https://tools.ietf.org/html/rfc7518#page-9
* @see https://en.wikipedia.org/wiki/Abstract_Syntax_Notation_One
*/
interface SignatureConverter
{
/**
* Converts the signature generated by OpenSSL into what JWA defines
*/
public function fromAsn1(string $signature, int $length): string;

/**
* Converts the JWA signature into something OpenSSL understands
*/
public function toAsn1(string $points, int $length): string;
}
Loading