ECSignature   A
last analyzed

Complexity

Total Complexity 17

Size/Duplication

Total Lines 111
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 0

Importance

Changes 0
Metric Value
wmc 17
lcom 1
cbo 0
dl 0
loc 111
rs 10
c 0
b 0
f 0

7 Methods

Rating   Name   Duplication   Size   Complexity  
A toAsn1() 0 24 3
A fromAsn1() 0 18 3
A octetLength() 0 4 1
A preparePositiveInteger() 0 13 4
A readAsn1Content() 0 7 1
A readAsn1Integer() 0 10 2
A retrievePositiveInteger() 0 9 3
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * The MIT License (MIT)
7
 *
8
 * Copyright (c) 2014-2019 Spomky-Labs
9
 *
10
 * This software may be modified and distributed under the terms
11
 * of the MIT license.  See the LICENSE file for details.
12
 */
13
14
namespace Jose\Component\Core\Util;
15
16
use InvalidArgumentException;
17
use const STR_PAD_LEFT;
18
19
/**
20
 * @internal
21
 */
22
final class ECSignature
23
{
24
    private const ASN1_SEQUENCE = '30';
25
    private const ASN1_INTEGER = '02';
26
    private const ASN1_MAX_SINGLE_BYTE = 128;
27
    private const ASN1_LENGTH_2BYTES = '81';
28
    private const ASN1_BIG_INTEGER_LIMIT = '7f';
29
    private const ASN1_NEGATIVE_INTEGER = '00';
30
    private const BYTE_SIZE = 2;
31
32
    /**
33
     * @throws InvalidArgumentException if the length of the signature is invalid
34
     */
35
    public static function toAsn1(string $signature, int $length): string
36
    {
37
        $signature = bin2hex($signature);
38
39
        if (self::octetLength($signature) !== $length) {
40
            throw new InvalidArgumentException('Invalid signature length.');
41
        }
42
43
        $pointR = self::preparePositiveInteger(mb_substr($signature, 0, $length, '8bit'));
44
        $pointS = self::preparePositiveInteger(mb_substr($signature, $length, null, '8bit'));
45
46
        $lengthR = self::octetLength($pointR);
47
        $lengthS = self::octetLength($pointS);
48
49
        $totalLength = $lengthR + $lengthS + self::BYTE_SIZE + self::BYTE_SIZE;
50
        $lengthPrefix = $totalLength > self::ASN1_MAX_SINGLE_BYTE ? self::ASN1_LENGTH_2BYTES : '';
51
52
        return hex2bin(
53
            self::ASN1_SEQUENCE
54
            .$lengthPrefix.dechex($totalLength)
55
            .self::ASN1_INTEGER.dechex($lengthR).$pointR
56
            .self::ASN1_INTEGER.dechex($lengthS).$pointS
57
        );
58
    }
59
60
    /**
61
     * @throws InvalidArgumentException if the signature is not an ASN.1 sequence
62
     */
63
    public static function fromAsn1(string $signature, int $length): string
64
    {
65
        $message = bin2hex($signature);
66
        $position = 0;
67
68
        if (self::ASN1_SEQUENCE !== self::readAsn1Content($message, $position, self::BYTE_SIZE)) {
69
            throw new InvalidArgumentException('Invalid data. Should start with a sequence.');
70
        }
71
72
        if (self::ASN1_LENGTH_2BYTES === self::readAsn1Content($message, $position, self::BYTE_SIZE)) {
73
            $position += self::BYTE_SIZE;
74
        }
75
76
        $pointR = self::retrievePositiveInteger(self::readAsn1Integer($message, $position));
77
        $pointS = self::retrievePositiveInteger(self::readAsn1Integer($message, $position));
78
79
        return hex2bin(str_pad($pointR, $length, '0', STR_PAD_LEFT).str_pad($pointS, $length, '0', STR_PAD_LEFT));
80
    }
81
82
    private static function octetLength(string $data): int
83
    {
84
        return (int) (mb_strlen($data, '8bit') / self::BYTE_SIZE);
85
    }
86
87
    private static function preparePositiveInteger(string $data): string
88
    {
89
        if (mb_substr($data, 0, self::BYTE_SIZE, '8bit') > self::ASN1_BIG_INTEGER_LIMIT) {
90
            return self::ASN1_NEGATIVE_INTEGER.$data;
91
        }
92
93
        while (0 === mb_strpos($data, self::ASN1_NEGATIVE_INTEGER, 0, '8bit')
94
            && mb_substr($data, 2, self::BYTE_SIZE, '8bit') <= self::ASN1_BIG_INTEGER_LIMIT) {
95
            $data = mb_substr($data, 2, null, '8bit');
96
        }
97
98
        return $data;
99
    }
100
101
    private static function readAsn1Content(string $message, int &$position, int $length): string
102
    {
103
        $content = mb_substr($message, $position, $length, '8bit');
104
        $position += $length;
105
106
        return $content;
107
    }
108
109
    /**
110
     * @throws InvalidArgumentException if the data is not an integer
111
     */
112
    private static function readAsn1Integer(string $message, int &$position): string
113
    {
114
        if (self::ASN1_INTEGER !== self::readAsn1Content($message, $position, self::BYTE_SIZE)) {
115
            throw new InvalidArgumentException('Invalid data. Should contain an integer.');
116
        }
117
118
        $length = (int) hexdec(self::readAsn1Content($message, $position, self::BYTE_SIZE));
119
120
        return self::readAsn1Content($message, $position, $length * self::BYTE_SIZE);
121
    }
122
123
    private static function retrievePositiveInteger(string $data): string
124
    {
125
        while (0 === mb_strpos($data, self::ASN1_NEGATIVE_INTEGER, 0, '8bit')
126
            && mb_substr($data, 2, self::BYTE_SIZE, '8bit') > self::ASN1_BIG_INTEGER_LIMIT) {
127
            $data = mb_substr($data, 2, null, '8bit');
128
        }
129
130
        return $data;
131
    }
132
}
133