Added some comments.
[BearSSL] / src / x509 / asn1.t0
1 \ Copyright (c) 2016 Thomas Pornin <pornin@bolet.org>
2 \
3 \ Permission is hereby granted, free of charge, to any person obtaining
4 \ a copy of this software and associated documentation files (the
5 \ "Software"), to deal in the Software without restriction, including
6 \ without limitation the rights to use, copy, modify, merge, publish,
7 \ distribute, sublicense, and/or sell copies of the Software, and to
8 \ permit persons to whom the Software is furnished to do so, subject to
9 \ the following conditions:
10 \
11 \ The above copyright notice and this permission notice shall be
12 \ included in all copies or substantial portions of the Software.
13 \
14 \ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 \ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 \ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 \ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
18 \ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
19 \ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 \ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 \ SOFTWARE.
22
23 \ =======================================================================
24
25 \ This file contains code which is common to all engines that do some
26 \ ASN.1 decoding. It should not be compiled on its own, but only along
27 \ with another file (e.g. x509_minimal.t0) which uses it.
28 \
29 \ Users must define several things:
30 \
31 \ -- In the preamble, a macro called "CTX" that evaluates to the current
32 \ context structure.
33 \
34 \ -- In the preamble, a macro called "CONTEXT_NAME" that evaluates to the
35 \ context structure type. This will be invoked during compilation.
36 \
37 \ -- A word called "read8-low" ( -- x ) that reads the next byte, or -1
38 \ if the input buffer is empty. That word is usually written in C.
39 \
40 \ -- A word called "read-blob-inner" ( addr len -- addr len ) that is
41 \ the multi-byte version of read8-low.
42 \
43 \ -- A word called "skip-remaining-inner" ( lim -- lim ) which reads but
44 \ drops some input bytes.
45
46 preamble {
47
48 #include "inner.h"
49
50 }
51
52 \ Read next source character, skipping blanks.
53 : skip-blanks begin char dup 32 > if ret then drop again ;
54
55 : fail-oid
56 "Invalid OID" puts cr exitvm ;
57
58 \ Read a decimal integer, followed by either a dot or whitespace.
59 \ Note: this does not check for overflows.
60 : parse-number ( -- val nextchar )
61 char decval
62 begin
63 char
64 dup dup `. = swap 32 <= or if ret then
65 decval swap 10 * +
66 again ;
67
68 \ Encode a number in unsigned 7E format.
69 : encode7E ( val -- )
70 0 encode7E-inner ;
71
72 : encode7E-inner ( val eb -- )
73 swap dup 0x7F > if
74 dup 7 u>> 0x80 encode7E-inner 0x7F and
75 then
76 or data-add8 ;
77
78 \ Decode an OID from source, and encode it. First byte is length,
79 \ followed by encoded ASN.1 DER value. The OID is encoded in the
80 \ current data block.
81 : OID
82 \ Get current data address, and push a 0 for length.
83 current-data 0 data-add8
84 \ Skip blanks and get first digit, which must be 0, 1 or 2.
85 skip-blanks decval dup 2 > if fail-oid then
86 40 *
87 \ Next character must be a dot.
88 char `. <> if fail-oid then
89 \ Second group must be one or two digits.
90 parse-number { nextchar }
91 dup 40 >= if fail-oid then
92 + encode7E
93 \ While next character is a dot, keep encoding numbers.
94 begin nextchar `. = while
95 parse-number >nextchar
96 encode7E
97 repeat
98 \ Write back length in the first byte.
99 dup current-data swap - 1- swap data-set8
100 ; immediate
101
102 \ Define a new data word for an encoded OID. The OID is read from the
103 \ source.
104 : OID:
105 new-data-block next-word define-data-word postpone OID ;
106
107 \ Define a word that evaluates to the address of a field within the
108 \ context.
109 : addr:
110 next-word { field }
111 "addr-" field + 0 1 define-word
112 0 8191 "offsetof(CONTEXT_NAME, " field + ")" + make-CX
113 postpone literal postpone ; ;
114
115 addr: pad
116
117 \ Define a word that evaluates to an error code through a macro name.
118 : err:
119 next-word { name }
120 name 0 1 define-word
121 0 63 "BR_" name + make-CX postpone literal postpone ; ;
122
123 err: ERR_X509_INVALID_VALUE
124 err: ERR_X509_TRUNCATED
125 err: ERR_X509_EMPTY_CHAIN
126 err: ERR_X509_INNER_TRUNC
127 err: ERR_X509_BAD_TAG_CLASS
128 err: ERR_X509_BAD_TAG_VALUE
129 err: ERR_X509_INDEFINITE_LENGTH
130 err: ERR_X509_EXTRA_ELEMENT
131 err: ERR_X509_UNEXPECTED
132 err: ERR_X509_NOT_CONSTRUCTED
133 err: ERR_X509_NOT_PRIMITIVE
134 err: ERR_X509_PARTIAL_BYTE
135 err: ERR_X509_BAD_BOOLEAN
136 err: ERR_X509_OVERFLOW
137 err: ERR_X509_BAD_DN
138 err: ERR_X509_BAD_TIME
139 err: ERR_X509_UNSUPPORTED
140 err: ERR_X509_LIMIT_EXCEEDED
141 err: ERR_X509_WRONG_KEY_TYPE
142 err: ERR_X509_BAD_SIGNATURE
143 err: ERR_X509_EXPIRED
144 err: ERR_X509_DN_MISMATCH
145 err: ERR_X509_BAD_SERVER_NAME
146 err: ERR_X509_CRITICAL_EXTENSION
147 err: ERR_X509_NOT_CA
148 err: ERR_X509_FORBIDDEN_KEY_USAGE
149 err: ERR_X509_WEAK_PUBLIC_KEY
150
151 : KEYTYPE_RSA CX 0 15 { BR_KEYTYPE_RSA } ;
152 : KEYTYPE_EC CX 0 15 { BR_KEYTYPE_EC } ;
153
154 cc: fail ( err -- ! ) {
155 CTX->err = T0_POPi();
156 T0_CO();
157 }
158
159 \ Read one byte from the stream.
160 : read8-nc ( -- x )
161 begin
162 read8-low dup 0 >= if ret then
163 drop co
164 again ;
165
166 \ Read one byte, enforcing current read limit.
167 : read8 ( lim -- lim x )
168 dup ifnot ERR_X509_INNER_TRUNC fail then
169 1- read8-nc ;
170
171 \ Read all bytes from the current element, then close it (i.e. drop the
172 \ limit). Destination address is an offset within the context.
173 : read-blob ( lim addr -- )
174 swap
175 begin dup while read-blob-inner dup if co then repeat
176 2drop ;
177
178 \ Skip remaining bytes in the current structure, but do not close it
179 \ (thus, this leaves the value 0 on the stack).
180 : skip-remaining ( lim -- lim )
181 begin dup while skip-remaining-inner dup if co then repeat ;
182
183 : skip-remaining-inner ( lim -- lim )
184 0 over read-blob-inner -rot 2drop ;
185
186 cc: set8 ( val addr -- ) {
187 uint32_t addr = T0_POP();
188 *((unsigned char *)CTX + addr) = (unsigned char)T0_POP();
189 }
190
191 cc: set16 ( val addr -- ) {
192 uint32_t addr = T0_POP();
193 *(uint16_t *)((unsigned char *)CTX + addr) = T0_POP();
194 }
195
196 cc: set32 ( val addr -- ) {
197 uint32_t addr = T0_POP();
198 *(uint32_t *)((unsigned char *)CTX + addr) = T0_POP();
199 }
200
201 cc: get8 ( addr -- val ) {
202 uint32_t addr = T0_POP();
203 T0_PUSH(*((unsigned char *)CTX + addr));
204 }
205
206 cc: get16 ( addr -- val ) {
207 uint32_t addr = T0_POP();
208 T0_PUSH(*(uint16_t *)((unsigned char *)CTX + addr));
209 }
210
211 cc: get32 ( addr -- val ) {
212 uint32_t addr = T0_POP();
213 T0_PUSH(*(uint32_t *)((unsigned char *)CTX + addr));
214 }
215
216 \ Read an ASN.1 tag. This function returns the "constructed" status
217 \ and the tag value. The constructed status is a boolean (-1 for
218 \ constructed, 0 for primitive). The tag value is either 0 to 31 for
219 \ a universal tag, or 32+x for a contextual tag of value x. Tag classes
220 \ "application" and "private" are rejected. Universal tags beyond 30
221 \ are rejected. Contextual tags beyond 30 are rejected. Thus, accepted
222 \ tags will necessarily fit on exactly one byte. This does not support
223 \ the whole of ASN.1/BER, but is sufficient for certificate parsing.
224 : read-tag ( lim -- lim constructed value )
225 read8 { fb }
226
227 \ Constructed flag is bit 5.
228 fb 5 >> 0x01 and neg
229
230 \ Class is in bits 6 and 7. Accepted classes are 00 (universal)
231 \ and 10 (context). We check that bit 6 is 0, and shift back
232 \ bit 7 so that we get 0 (universal) or 32 (context).
233 fb 6 >> dup 0x01 and if ERR_X509_BAD_TAG_CLASS fail then
234 4 <<
235
236 \ Tag value is in bits 0..4. If the value is 31, then this is
237 \ an extended tag, encoded over subsequent bytes, and we do
238 \ not support that.
239 fb 0x1F and dup 0x1F = if ERR_X509_BAD_TAG_VALUE fail then
240 + ;
241
242 \ Read a tag, but only if not at the end of the current object. If there
243 \ is no room for another element (limit is zero), then this will push a
244 \ synthetic "no tag" value (primitive, with value -1).
245 : read-tag-or-end ( lim -- lim constructed value )
246 dup ifnot 0 -1 ret then
247 read-tag ;
248
249 \ Compare the read tag with the provided value. If equal, then the
250 \ element is skipped, and a new tag is read (or end of object).
251 : iftag-skip ( lim constructed value ref -- lim constructed value )
252 over = if
253 2drop
254 read-length-open-elt skip-close-elt
255 read-tag-or-end
256 then ;
257
258 \ Read an ASN.1 length. This supports only definite lengths (theoretically,
259 \ certificates may use an indefinite length for the outer structure, using
260 \ DER only in the TBS, but this never happens in practice, except in a
261 \ single example certificate from 15 years ago that also fails to decode
262 \ properly for other reasons).
263 : read-length ( lim -- lim length )
264 read8
265 \ Lengths in 0x00..0x7F get encoded as a single byte.
266 dup 0x80 < if ret then
267
268 \ If the byte is 0x80 then this is an indefinite length, and we
269 \ do not support that.
270 0x80 - dup ifnot ERR_X509_INDEFINITE_LENGTH fail then
271
272 \ Masking out bit 7, this yields the number of bytes over which
273 \ the value is encoded. Since the total certicate length must
274 \ fit over 3 bytes (this is a consequence of SSL/TLS message
275 \ format), we can reject big lengths and keep the length in a
276 \ single integer.
277 { n } 0
278 begin n 0 > while n 1- >n
279 dup 0xFFFFFF > if ERR_X509_INNER_TRUNC fail then
280 8 << swap read8 rot +
281 repeat ;
282
283 \ Open a sub-structure. This subtracts the length from the limit, and
284 \ pushes the length back as new limit.
285 : open-elt ( lim length -- lim_outer lim_inner )
286 dup2 < if ERR_X509_INNER_TRUNC fail then
287 dup { len } - len ;
288
289 \ Read a length and open the value as a sub-structure.
290 : read-length-open-elt ( lim -- lim_outer lim_inner )
291 read-length open-elt ;
292
293 \ Close a sub-structure. This verifies that there is no remaining
294 \ element to read.
295 : close-elt ( lim -- )
296 if ERR_X509_EXTRA_ELEMENT fail then ;
297
298 \ Skip remaining bytes in the current structure, then close it.
299 : skip-close-elt ( lim -- )
300 skip-remaining drop ;
301
302 \ Read a length and then skip the value.
303 : read-length-skip ( lim -- lim )
304 read-length-open-elt skip-close-elt ;
305
306 \ Check that a given tag is constructed and has the expected value.
307 : check-tag-constructed ( constructed value refvalue -- )
308 = ifnot ERR_X509_UNEXPECTED fail then
309 check-constructed ;
310
311 \ Check that the top value is true; report a "not constructed"
312 \ error otherwise.
313 : check-constructed ( constructed -- )
314 ifnot ERR_X509_NOT_CONSTRUCTED fail then ;
315
316 \ Check that a given tag is primitive and has the expected value.
317 : check-tag-primitive ( constructed value refvalue -- )
318 = ifnot ERR_X509_UNEXPECTED fail then
319 check-primitive ;
320
321 \ Check that the top value is true; report a "not primitive"
322 \ error otherwise.
323 : check-primitive ( constructed -- )
324 if ERR_X509_NOT_PRIMITIVE fail then ;
325
326 \ Check that the tag is for a constructed SEQUENCE.
327 : check-sequence ( constructed value -- )
328 0x10 check-tag-constructed ;
329
330 \ Read a tag, check that it is for a constructed SEQUENCE, and open
331 \ it as a sub-element.
332 : read-sequence-open ( lim -- lim_outer lim_inner )
333 read-tag check-sequence read-length-open-elt ;
334
335 \ Read the next element as a BIT STRING with no ignore bits, and open
336 \ it as a sub-element.
337 : read-bits-open ( lim -- lim_outer lim_inner )
338 read-tag 0x03 check-tag-primitive
339 read-length-open-elt
340 read8 if ERR_X509_PARTIAL_BYTE fail then ;
341
342 OID: rsaEncryption 1.2.840.113549.1.1.1
343
344 OID: sha1WithRSAEncryption 1.2.840.113549.1.1.5
345 OID: sha224WithRSAEncryption 1.2.840.113549.1.1.14
346 OID: sha256WithRSAEncryption 1.2.840.113549.1.1.11
347 OID: sha384WithRSAEncryption 1.2.840.113549.1.1.12
348 OID: sha512WithRSAEncryption 1.2.840.113549.1.1.13
349
350 OID: id-sha1 1.3.14.3.2.26
351 OID: id-sha224 2.16.840.1.101.3.4.2.4
352 OID: id-sha256 2.16.840.1.101.3.4.2.1
353 OID: id-sha384 2.16.840.1.101.3.4.2.2
354 OID: id-sha512 2.16.840.1.101.3.4.2.3
355
356 OID: id-ecPublicKey 1.2.840.10045.2.1
357
358 OID: ansix9p256r1 1.2.840.10045.3.1.7
359 OID: ansix9p384r1 1.3.132.0.34
360 OID: ansix9p521r1 1.3.132.0.35
361
362 OID: ecdsa-with-SHA1 1.2.840.10045.4.1
363 OID: ecdsa-with-SHA224 1.2.840.10045.4.3.1
364 OID: ecdsa-with-SHA256 1.2.840.10045.4.3.2
365 OID: ecdsa-with-SHA384 1.2.840.10045.4.3.3
366 OID: ecdsa-with-SHA512 1.2.840.10045.4.3.4
367
368 \ Read a "small value". This assumes that the tag has just been read
369 \ and processed, but not the length. The first pad byte is set to the
370 \ value length; the encoded value iself follows. If the value length
371 \ exceeds 255 bytes, then a single 0 is written in the pad, and this
372 \ method returns false (0). Otherwise, it returns true (-1).
373 \ Either way, the element is fully read.
374 : read-small-value ( lim -- lim bool )
375 read-length-open-elt
376 dup 255 > if skip-close-elt 0 addr-pad set8 0 ret then
377 dup addr-pad set8
378 addr-pad 1+ read-blob
379 -1 ;
380
381 \ Read an OID as a "small value" (tag, length and value). A boolean
382 \ value is returned, which is true (-1) if the OID value fits on the pad,
383 \ false (0) otherwise.
384 : read-OID ( lim -- lim bool )
385 read-tag 0x06 check-tag-primitive read-small-value ;
386
387 \ Read a value and interpret it as an INTEGER or ENUMERATED value. If
388 \ the integer value does not fit on an unsigned 32-bit value, an error
389 \ is reported. This function assumes that the tag has just been read
390 \ and processed, but not the length.
391 : read-small-int-value ( lim -- lim x )
392 read-length-open-elt
393 dup ifnot ERR_X509_OVERFLOW fail then
394 read8 dup 0x80 >= if ERR_X509_OVERFLOW fail then
395 { x }
396 begin dup while
397 read8 x dup 0xFFFFFF >= if ERR_X509_OVERFLOW fail then
398 8 << + >x
399 repeat
400 drop x ;
401
402 \ Compare the OID in the pad with an OID in the constant data block.
403 \ Returned value is -1 on equality, 0 otherwise.
404 cc: eqOID ( addrConst -- bool ) {
405 const unsigned char *a2 = &t0_datablock[T0_POP()];
406 const unsigned char *a1 = &CTX->pad[0];
407 size_t len = a1[0];
408 int x;
409 if (len == a2[0]) {
410 x = -(memcmp(a1 + 1, a2 + 1, len) == 0);
411 } else {
412 x = 0;
413 }
414 T0_PUSH((uint32_t)x);
415 }
416
417 \ Compare two blobs in the context. Returned value is -1 on equality, 0
418 \ otherwise.
419 cc: eqblob ( addr1 addr2 len -- bool ) {
420 size_t len = T0_POP();
421 const unsigned char *a2 = (const unsigned char *)CTX + T0_POP();
422 const unsigned char *a1 = (const unsigned char *)CTX + T0_POP();
423 T0_PUSHi(-(memcmp(a1, a2, len) == 0));
424 }
425
426 \ Check that a value is in a given range (inclusive).
427 : between? ( x min max -- bool )
428 { min max } dup min >= swap max <= and ;
429
430 \ Convert the provided byte value into a number in the 0..9 range,
431 \ assuming that it is an ASCII digit. A non-digit triggers an error
432 \ (a "bad time" error since this is used in date/time decoding).
433 : digit-dec ( char -- value )
434 `0 - dup 0 9 between? ifnot ERR_X509_BAD_TIME fail then ;
435
436 \ Read two ASCII digits and return the value in the 0..99 range. An
437 \ error is reported if the characters are not ASCII digits.
438 : read-dec2 ( lim -- lim x )
439 read8 digit-dec 10 * { x } read8 digit-dec x + ;
440
441 \ Read two ASCII digits and check that the value is in the provided
442 \ range (inclusive).
443 : read-dec2-range ( lim min max -- lim x )
444 { min max }
445 read-dec2 dup min max between? ifnot ERR_X509_BAD_TIME fail then ;
446
447 \ Maximum days in a month and accumulated day count. Each
448 \ 16-bit value contains the month day count in its lower 5 bits. The first
449 \ 12 values are for a normal year, the other 12 for a leap year.
450 data: month-to-days
451 hexb| 001F 03FC 077F 0B5E 0F1F 12FE 16BF 1A9F 1E7E 223F 261E 29DF |
452 hexb| 001F 03FD 079F 0B7E 0F3F 131E 16DF 1ABF 1E9E 225F 263E 29FF |
453
454 \ Read a date (UTCTime or GeneralizedTime). The date value is converted
455 \ to a day count and a second count. The day count starts at 0 for
456 \ January 1st, 0 AD (that's they year before 1 AD, also known as 1 BC)
457 \ in a proleptic Gregorian calendar (i.e. Gregorian rules are assumed to
458 \ extend indefinitely in the past). The second count is between 0 and
459 \ 86400 (inclusive, in case of a leap second).
460 : read-date ( lim -- lim days seconds )
461 \ Read tag; must be UTCTime or GeneralizedTime. Year count is
462 \ 4 digits with GeneralizedTime, 2 digits with UTCTime.
463 read-tag
464 dup 0x17 0x18 between? ifnot ERR_X509_BAD_TIME fail then
465 0x18 = { y4d }
466 check-primitive
467 read-length-open-elt
468
469 \ We compute the days and seconds counts during decoding, in
470 \ order to minimize the number of needed temporary variables.
471 { ; days seconds x }
472
473 \ Year is 4-digit with GeneralizedTime. With UTCTime, the year
474 \ is in the 1950..2049 range, and only the last two digits are
475 \ present in the encoding.
476 read-dec2
477 y4d if
478 100 * >x read-dec2 x +
479 else
480 dup 50 < if 100 + then 1900 +
481 then
482 >x
483 x 365 * x 3 + 4 / + x 99 + 100 / - x 399 + 400 / + >days
484
485 \ Month is 1..12. Number of days in a months depend on the
486 \ month and on the year (year count is in x at that point).
487 1 12 read-dec2-range
488 1- 1 <<
489 x 4 % 0= x 100 % 0<> x 400 % 0= or and if 24 + then
490 month-to-days + data-get16
491 dup 5 >> days + >days
492 0x1F and
493
494 \ Day. At this point, the TOS contains the maximum day count for
495 \ the current month.
496 1 swap read-dec2-range
497 days + 1- >days
498
499 \ Hour, minute and seconds. Count of seconds is allowed to go to
500 \ 60 in case of leap seconds (in practice, leap seconds really
501 \ occur only at the very end of the day, so this computation is
502 \ exact for a real leap second, and a spurious leap second only
503 \ implies a one-second shift that we can ignore).
504 0 23 read-dec2-range 3600 * >seconds
505 0 59 read-dec2-range 60 * seconds + >seconds
506 0 60 read-dec2-range seconds + >seconds
507
508 \ At this point, we may have fractional seconds. This should
509 \ happen only with GeneralizedTime, but we accept it for UTCTime
510 \ too (and, anyway, we ignore these fractional seconds).
511 read8 dup `. = if
512 drop
513 begin read8 dup `0 `9 between? while drop repeat
514 then
515
516 \ The time zone should be 'Z', not followed by anything. Other
517 \ time zone indications are not DER and thus not supposed to
518 \ appear in certificates.
519 `Z <> if ERR_X509_BAD_TIME fail then
520 close-elt
521 days seconds ;
522
523 \ Read an INTEGER (tag, length and value). The INTEGER is supposed to be
524 \ positive; its unsigned big-endian encoding is stored in the provided
525 \ in-context buffer. Returned value is the decoded length. If the integer
526 \ did not fit, or the value is negative, then an error is reported.
527 : read-integer ( lim addr len -- lim dlen )
528 rot read-tag 0x02 check-tag-primitive -rot
529 read-integer-next ;
530
531 \ Identical to read-integer, but the tag has already been read and checked.
532 : read-integer-next ( lim addr len -- lim dlen )
533 dup { addr len origlen }
534 read-length-open-elt
535 \ Read first byte; sign bit must be 0.
536 read8 dup 0x80 >= if ERR_X509_OVERFLOW fail then
537 \ Skip leading bytes of value 0. If there are only bytes of
538 \ value 0, then return.
539 begin dup 0 = while
540 drop dup ifnot drop 0 ret then
541 read8
542 repeat
543 \ At that point, we have the first non-zero byte on the stack.
544 begin
545 len dup ifnot ERR_X509_LIMIT_EXCEEDED fail then 1- >len
546 addr set8 addr 1+ >addr
547 dup while read8
548 repeat
549 drop origlen len - ;
550
551 \ Read a BOOLEAN value. This should be called immediately after reading
552 \ the tag.
553 : read-boolean ( lim constructed value -- lim bool )
554 0x01 check-tag-primitive
555 read-length 1 <> if ERR_X509_BAD_BOOLEAN fail then
556 read8 0<> ;
557
558 \ Identify an elliptic curve: read the OID, then check it against the
559 \ known curve OID.
560 : read-curve-ID ( lim -- lim curve )
561 read-OID ifnot ERR_X509_UNSUPPORTED fail then
562 choice
563 ansix9p256r1 eqOID uf 23 enduf
564 ansix9p384r1 eqOID uf 24 enduf
565 ansix9p521r1 eqOID uf 25 enduf
566 ERR_X509_UNSUPPORTED fail
567 endchoice ;
568
569 \ A convenient debug word: print the current data stack contents.
570 cc: DEBUG ( -- ) {
571 extern int printf(const char *fmt, ...);
572 uint32_t *p;
573
574 printf("<stack:");
575 for (p = &CTX->dp_stack[0]; p != dp; p ++) {
576 printf(" %lu", (unsigned long)*p);
577 }
578 printf(" >\n");
579 }