master
ブランチ master
LittleBeast / src / Std / Lib / Crypto / Asn1.php
11.5 KB、324行 生 Blame 履歴
1<?php
2/*************************************************************
3# 076 License
4
5Copyright (c) テクニカル諏訪子
6
7Permission is hereby granted to any person obtaining a copy of the software
8Little Beast (the "Software") to use, modify, merge, copy, publish, distribute,
9sublicense, and/or sell copies of the Software, subject to the following conditions:
10
11 1. **Origin Attribution**:
12 - You must not misrepresent the origin of the Software; you must not claim
13 you created the original Software.
14 - If the Software is used in a product, you must either:
15 a. Provide clear attribution in the product's documentation, user interface,
16 or other visible areas, **OR**
17 b. Pay the original developers a fee they specify in writing.
18 2. **Usage Restriction**:
19 - The Software, or any derivative works, dependencies, or libraries
20 incorporating it, must not be used for censorship or to suppress freedom of
21 speech, expression, or creativity. Prohibited uses include, but are not
22 limited to:
23 - Censorship of so-called "hate speech", visuals, non-mainstream opinions,
24 ideas, or objective reality.
25 - Tools or systems designed to restrict access to information or
26 artistic works.
27 3. **Notice Preservation**:
28 - This license and the above copyright notice must remain intact in all copies
29 of the source code.
30
31THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
32INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
33PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
34HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
35CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
36OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
37 */
38namespace Std\Lib\Crypto;
39
40/**
41 * ASN.1(DER)の読み書きとPEM
42 *
43 * 読む:
44 * $node = Asn1::parse($der); // ['tag' => 0x30, 'value' => 中身, 'raw' => タグ・長さ・中身]
45 * foreach (Asn1::children($node) as $c) … // 構造型の中の要素
46 * 書く:
47 * $der = Asn1::seq(Asn1::int(1), Asn1::oid('1.2.840.113549.1.1.1'), Asn1::null());
48 */
49class Asn1 {
50 public const BOOLEAN = 0x01;
51 public const INTEGER = 0x02;
52 public const BIT_STRING = 0x03;
53 public const OCTET_STRING = 0x04;
54 public const NULL = 0x05;
55 public const OID = 0x06;
56 public const UTF8_STRING = 0x0C;
57 public const PRINTABLE_STRING = 0x13;
58 public const T61_STRING = 0x14;
59 public const IA5_STRING = 0x16;
60 public const UTC_TIME = 0x17;
61 public const GENERALIZED_TIME = 0x18;
62 public const UNIVERSAL_STRING = 0x1C;
63 public const BMP_STRING = 0x1E;
64 public const SEQUENCE = 0x30;
65 public const SET = 0x31;
66
67 private const MAX_DEPTH = 32;
68
69 /**
70 * DERの要素を1つ読む
71 *
72 * @param string $der データ
73 * @param int $pos 位置(読んだ分だけ進める)
74 * @return array tag(最初のバイト)、value(中身)、raw(全体)
75 * @throws \UnexpectedValueException 壊れたDERの場合
76 */
77 public static function read(string $der, int &$pos): array {
78 $len = strlen($der);
79 if ($pos + 2 > $len) throw new \UnexpectedValueException('ASN.1:データが足りない');
80
81 $start = $pos;
82 $tag = ord($der[$pos++]);
83 if (($tag & 0x1F) === 0x1F) throw new \UnexpectedValueException('ASN.1:長いタグは未対応');
84
85 $l = ord($der[$pos++]);
86 if ($l & 0x80) {
87 $n = $l & 0x7F;
88 if ($n === 0 || $n > 4 || $pos + $n > $len) throw new \UnexpectedValueException('ASN.1:不正な長さ');
89 $l = 0;
90 for ($i = 0; $i < $n; $i++) $l = ($l << 8) | ord($der[$pos++]);
91 }
92 if ($pos + $l > $len) throw new \UnexpectedValueException('ASN.1:長さがデータを超える');
93
94 $value = substr($der, $pos, $l);
95 $pos += $l;
96 return ['tag' => $tag, 'value' => $value, 'raw' => substr($der, $start, $pos - $start)];
97 }
98
99 /**
100 * DER全体を1つの要素として読む(後ろに余計なデータがあれば例外)
101 *
102 * @param string $der
103 * @return array
104 * @throws \UnexpectedValueException
105 */
106 public static function parse(string $der): array {
107 $pos = 0;
108 $node = self::read($der, $pos);
109 if ($pos !== strlen($der)) throw new \UnexpectedValueException('ASN.1:余計なデータ');
110 return $node;
111 }
112
113 /**
114 * 構造型(SEQUENCE、SET、[n]等)の中の要素
115 *
116 * @param array $node
117 * @return array 要素の配列
118 * @throws \UnexpectedValueException
119 */
120 public static function children(array $node): array {
121 if (!($node['tag'] & 0x20)) throw new \UnexpectedValueException('ASN.1:構造型ではない');
122 $out = [];
123 $pos = 0;
124 $v = $node['value'];
125 while ($pos < strlen($v)) $out[] = self::read($v, $pos);
126 return $out;
127 }
128
129 /**
130 * 種類を確認して、構造型の中の要素を返す
131 *
132 * @param array $node
133 * @param int $tag 期待するタグ
134 * @param int $min 最低の要素の数
135 * @return array
136 * @throws \UnexpectedValueException
137 */
138 public static function expect(array $node, int $tag, int $min = 0): array {
139 if ($node['tag'] !== $tag) throw new \UnexpectedValueException(sprintf('ASN.1:0x%02Xを期待したが0x%02X', $tag, $node['tag']));
140 $c = self::children($node);
141 if (count($c) < $min) throw new \UnexpectedValueException('ASN.1:要素が足りない');
142 return $c;
143 }
144
145 ///////////////
146 // 値
147
148 /** INTEGERを読む(2の補数) */
149 public static function toInt(array $node): \GMP {
150 if ($node['tag'] !== self::INTEGER || $node['value'] === '') throw new \UnexpectedValueException('ASN.1:INTEGERではない');
151 $v = $node['value'];
152 $n = gmp_import($v);
153 if (ord($v[0]) & 0x80) $n = gmp_sub($n, gmp_pow(2, 8 * strlen($v)));
154 return $n;
155 }
156
157 /** OBJECT IDENTIFIERを読む(「1.2.840.113549」の形) */
158 public static function toOid(array $node): string {
159 if ($node['tag'] !== self::OID || $node['value'] === '') throw new \UnexpectedValueException('ASN.1:OIDではない');
160 $parts = [];
161 $n = 0;
162 $v = $node['value'];
163 for ($i = 0; $i < strlen($v); $i++) {
164 $b = ord($v[$i]);
165 if ($n > (PHP_INT_MAX >> 7)) throw new \UnexpectedValueException('ASN.1:OIDが大き過ぎる');
166 $n = ($n << 7) | ($b & 0x7F);
167 if (!($b & 0x80)) {
168 if (empty($parts)) {
169 $first = min(2, intdiv($n, 40));
170 $parts[] = $first;
171 $parts[] = $n - 40 * $first;
172 } else {
173 $parts[] = $n;
174 }
175 $n = 0;
176 }
177 }
178 return implode('.', $parts);
179 }
180
181 /** BIT STRINGの中身(未使用ビットの数のバイトを除く) */
182 public static function toBits(array $node): string {
183 if ($node['tag'] !== self::BIT_STRING || $node['value'] === '') throw new \UnexpectedValueException('ASN.1:BIT STRINGではない');
184 return substr($node['value'], 1);
185 }
186
187 /** 文字列の型をUTF-8にする */
188 public static function toString(array $node): string {
189 $v = $node['value'];
190 return match ($node['tag']) {
191 self::BMP_STRING => mb_convert_encoding($v, 'UTF-8', 'UTF-16BE'),
192 self::UNIVERSAL_STRING => mb_convert_encoding($v, 'UTF-8', 'UTF-32BE'),
193 self::T61_STRING => mb_check_encoding($v, 'UTF-8') ? $v : mb_convert_encoding($v, 'UTF-8', 'ISO-8859-1'),
194 default => $v,
195 };
196 }
197
198 /** UTCTime・GeneralizedTimeをUNIX時間にする */
199 public static function toTime(array $node): int {
200 $v = $node['value'];
201 if ($node['tag'] === self::UTC_TIME && preg_match('/^(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})Z$/', $v, $m)) {
202 $y = (int)$m[1];
203 $y += $y < 50 ? 2000 : 1900;
204 } else if ($node['tag'] === self::GENERALIZED_TIME && preg_match('/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(?:\.\d+)?Z$/', $v, $m)) {
205 $y = (int)$m[1];
206 } else {
207 throw new \UnexpectedValueException('ASN.1:未対応の時間');
208 }
209 return gmmktime((int)$m[4], (int)$m[5], (int)$m[6], (int)$m[2], (int)$m[3], $y);
210 }
211
212 ///////////////
213 // 書く
214
215 /** タグ・長さ・中身 */
216 public static function tlv(int $tag, string $value): string {
217 $l = strlen($value);
218 if ($l < 0x80) return chr($tag).chr($l).$value;
219 $b = ltrim(pack('N', $l), "\0");
220 return chr($tag).chr(0x80 | strlen($b)).$b.$value;
221 }
222
223 public static function seq(string ...$items): string {
224 return self::tlv(self::SEQUENCE, implode('', $items));
225 }
226
227 public static function set(string ...$items): string {
228 return self::tlv(self::SET, implode('', $items));
229 }
230
231 /** INTEGER(負数も) */
232 public static function int(\GMP|int $n): string {
233 $n = $n instanceof \GMP ? $n : gmp_init($n);
234 if (gmp_sign($n) >= 0) {
235 $b = gmp_export($n);
236 if ($b === '' || (ord($b[0]) & 0x80)) $b = "\0".$b;
237 return self::tlv(self::INTEGER, $b);
238 }
239 $len = strlen(gmp_export(gmp_abs($n))) + 1;
240 $b = substr(str_pad(gmp_export(gmp_add(gmp_pow(2, 8 * $len), $n)), $len, "\0", STR_PAD_LEFT), -$len);
241 while (strlen($b) > 1 && $b[0] === "\xFF" && (ord($b[1]) & 0x80)) $b = substr($b, 1);
242 return self::tlv(self::INTEGER, $b);
243 }
244
245 public static function oid(string $oid): string {
246 $p = array_map('intval', explode('.', $oid));
247 $out = '';
248 $nums = array_merge([40 * $p[0] + $p[1]], array_slice($p, 2));
249 foreach ($nums as $n) {
250 $b = chr($n & 0x7F);
251 for ($n >>= 7; $n > 0; $n >>= 7) $b = chr(0x80 | ($n & 0x7F)).$b;
252 $out .= $b;
253 }
254 return self::tlv(self::OID, $out);
255 }
256
257 public static function null(): string {
258 return "\x05\x00";
259 }
260
261 public static function bool(bool $v): string {
262 return self::tlv(self::BOOLEAN, $v ? "\xFF" : "\x00");
263 }
264
265 public static function bits(string $v): string {
266 return self::tlv(self::BIT_STRING, "\0".$v);
267 }
268
269 public static function octets(string $v): string {
270 return self::tlv(self::OCTET_STRING, $v);
271 }
272
273 public static function utf8(string $v): string {
274 return self::tlv(self::UTF8_STRING, $v);
275 }
276
277 /** 2050年までUTCTime、それ以降はGeneralizedTime(RFC 5280) */
278 public static function time(int $t): string {
279 $y = (int)gmdate('Y', $t);
280 return $y < 2050 && $y >= 1950
281 ? self::tlv(self::UTC_TIME, gmdate('ymdHis', $t).'Z')
282 : self::tlv(self::GENERALIZED_TIME, gmdate('YmdHis', $t).'Z');
283 }
284
285 /** 明示的なタグ[n] */
286 public static function explicit(int $n, string $inner): string {
287 return self::tlv(0xA0 | $n, $inner);
288 }
289
290 ///////////////
291 // PEM
292
293 /**
294 * DERをPEMにする
295 *
296 * @param string $der
297 * @param string $label 例:PRIVATE KEY、PUBLIC KEY、CERTIFICATE
298 * @return string
299 */
300 public static function pem(string $der, string $label): string {
301 return "-----BEGIN {$label}-----\n".chunk_split(base64_encode($der), 64, "\n")."-----END {$label}-----\n";
302 }
303
304 /**
305 * PEMのブロックを全部読む
306 *
307 * @param string $text PEM(複数のブロックも可)
308 * @return array ['label' => …, 'der' => …]の配列
309 */
310 public static function unpem(string $text): array {
311 preg_match_all('/-----BEGIN ([A-Z0-9 ]+)-----\s*(.*?)-----END \1-----/s', $text, $m, PREG_SET_ORDER);
312 $out = [];
313 foreach ($m as $b) {
314 // 「Proc-Type:」等のヘッダーがある(暗号化された)PEM
315 if (str_contains($b[2], ':')) {
316 $out[] = ['label' => $b[1], 'der' => '', 'encrypted' => true];
317 continue;
318 }
319 $der = base64_decode(preg_replace('/\s+/', '', $b[2]), true);
320 if ($der !== false) $out[] = ['label' => $b[1], 'der' => $der];
321 }
322 return $out;
323 }
324}