1 /*
   2  * Copyright (c) 1998, 2011, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package sun.security.provider;
  27 
  28 import java.io.*;
  29 import java.util.*;
  30 import java.security.cert.*;
  31 import sun.security.x509.X509CertImpl;
  32 import sun.security.x509.X509CRLImpl;
  33 import sun.security.pkcs.PKCS7;
  34 import sun.security.provider.certpath.X509CertPath;
  35 import sun.security.provider.certpath.X509CertificatePair;
  36 import sun.security.util.DerValue;
  37 import sun.security.util.Cache;
  38 import sun.misc.BASE64Decoder;
  39 import sun.security.pkcs.ParsingException;
  40 
  41 /**
  42  * This class defines a certificate factory for X.509 v3 certificates &
  43  * certification paths, and X.509 v2 certificate revocation lists (CRLs).
  44  *
  45  * @author Jan Luehe
  46  * @author Hemma Prafullchandra
  47  * @author Sean Mullan
  48  *
  49  *
  50  * @see java.security.cert.CertificateFactorySpi
  51  * @see java.security.cert.Certificate
  52  * @see java.security.cert.CertPath
  53  * @see java.security.cert.CRL
  54  * @see java.security.cert.X509Certificate
  55  * @see java.security.cert.X509CRL
  56  * @see sun.security.x509.X509CertImpl
  57  * @see sun.security.x509.X509CRLImpl
  58  */
  59 
  60 public class X509Factory extends CertificateFactorySpi {
  61 
  62     public static final String BEGIN_CERT = "-----BEGIN CERTIFICATE-----";
  63     public static final String END_CERT = "-----END CERTIFICATE-----";
  64 
  65     private static final int ENC_MAX_LENGTH = 4096 * 1024; // 4 MB MAX
  66 
  67     private static final Cache<Object, X509CertImpl> certCache
  68         = Cache.newSoftMemoryCache(750);
  69     private static final Cache<Object, X509CRLImpl> crlCache
  70         = Cache.newSoftMemoryCache(750);
  71 
  72     /**
  73      * Generates an X.509 certificate object and initializes it with
  74      * the data read from the input stream <code>is</code>.
  75      *
  76      * @param is an input stream with the certificate data.
  77      *
  78      * @return an X.509 certificate object initialized with the data
  79      * from the input stream.
  80      *
  81      * @exception CertificateException on parsing errors.
  82      */
  83     public Certificate engineGenerateCertificate(InputStream is)
  84         throws CertificateException
  85     {
  86         if (is == null) {
  87             // clear the caches (for debugging)
  88             certCache.clear();
  89             X509CertificatePair.clearCache();
  90             throw new CertificateException("Missing input stream");
  91         }
  92         try {
  93             byte[] encoding = readOneBlock(is);
  94             if (encoding != null) {
  95                 X509CertImpl cert = getFromCache(certCache, encoding);
  96                 if (cert != null) {
  97                     return cert;
  98                 }
  99                 cert = new X509CertImpl(encoding);
 100                 addToCache(certCache, cert.getEncodedInternal(), cert);
 101                 return cert;
 102             } else {
 103                 throw new IOException("Empty input");
 104             }
 105         } catch (IOException ioe) {
 106             throw (CertificateException)new CertificateException
 107             ("Could not parse certificate: " + ioe.toString()).initCause(ioe);
 108         }
 109     }
 110 
 111     /**
 112      * Read from the stream until length bytes have been read or EOF has
 113      * been reached. Return the number of bytes actually read.
 114      */
 115     private static int readFully(InputStream in, ByteArrayOutputStream bout,
 116             int length) throws IOException {
 117         int read = 0;
 118         byte[] buffer = new byte[2048];
 119         while (length > 0) {
 120             int n = in.read(buffer, 0, length<2048?length:2048);
 121             if (n <= 0) {
 122                 break;
 123             }
 124             bout.write(buffer, 0, n);
 125             read += n;
 126             length -= n;
 127         }
 128         return read;
 129     }
 130 
 131     /**
 132      * Return an interned X509CertImpl for the given certificate.
 133      * If the given X509Certificate or X509CertImpl is already present
 134      * in the cert cache, the cached object is returned. Otherwise,
 135      * if it is a X509Certificate, it is first converted to a X509CertImpl.
 136      * Then the X509CertImpl is added to the cache and returned.
 137      *
 138      * Note that all certificates created via generateCertificate(InputStream)
 139      * are already interned and this method does not need to be called.
 140      * It is useful for certificates that cannot be created via
 141      * generateCertificate() and for converting other X509Certificate
 142      * implementations to an X509CertImpl.
 143      */
 144     public static synchronized X509CertImpl intern(X509Certificate c)
 145             throws CertificateException {
 146         if (c == null) {
 147             return null;
 148         }
 149         boolean isImpl = c instanceof X509CertImpl;
 150         byte[] encoding;
 151         if (isImpl) {
 152             encoding = ((X509CertImpl)c).getEncodedInternal();
 153         } else {
 154             encoding = c.getEncoded();
 155         }
 156         X509CertImpl newC = getFromCache(certCache, encoding);
 157         if (newC != null) {
 158             return newC;
 159         }
 160         if (isImpl) {
 161             newC = (X509CertImpl)c;
 162         } else {
 163             newC = new X509CertImpl(encoding);
 164             encoding = newC.getEncodedInternal();
 165         }
 166         addToCache(certCache, encoding, newC);
 167         return newC;
 168     }
 169 
 170     /**
 171      * Return an interned X509CRLImpl for the given certificate.
 172      * For more information, see intern(X509Certificate).
 173      */
 174     public static synchronized X509CRLImpl intern(X509CRL c)
 175             throws CRLException {
 176         if (c == null) {
 177             return null;
 178         }
 179         boolean isImpl = c instanceof X509CRLImpl;
 180         byte[] encoding;
 181         if (isImpl) {
 182             encoding = ((X509CRLImpl)c).getEncodedInternal();
 183         } else {
 184             encoding = c.getEncoded();
 185         }
 186         X509CRLImpl newC = getFromCache(crlCache, encoding);
 187         if (newC != null) {
 188             return newC;
 189         }
 190         if (isImpl) {
 191             newC = (X509CRLImpl)c;
 192         } else {
 193             newC = new X509CRLImpl(encoding);
 194             encoding = newC.getEncodedInternal();
 195         }
 196         addToCache(crlCache, encoding, newC);
 197         return newC;
 198     }
 199 
 200     /**
 201      * Get the X509CertImpl or X509CRLImpl from the cache.
 202      */
 203     private static synchronized <K,V> V getFromCache(Cache<K,V> cache,
 204             byte[] encoding) {
 205         Object key = new Cache.EqualByteArray(encoding);
 206         return cache.get(key);
 207     }
 208 
 209     /**
 210      * Add the X509CertImpl or X509CRLImpl to the cache.
 211      */
 212     private static synchronized <V> void addToCache(Cache<Object, V> cache,
 213             byte[] encoding, V value) {
 214         if (encoding.length > ENC_MAX_LENGTH) {
 215             return;
 216         }
 217         Object key = new Cache.EqualByteArray(encoding);
 218         cache.put(key, value);
 219     }
 220 
 221     /**
 222      * Generates a <code>CertPath</code> object and initializes it with
 223      * the data read from the <code>InputStream</code> inStream. The data
 224      * is assumed to be in the default encoding.
 225      *
 226      * @param inStream an <code>InputStream</code> containing the data
 227      * @return a <code>CertPath</code> initialized with the data from the
 228      *   <code>InputStream</code>
 229      * @exception CertificateException if an exception occurs while decoding
 230      * @since 1.4
 231      */
 232     public CertPath engineGenerateCertPath(InputStream inStream)
 233         throws CertificateException
 234     {
 235         if (inStream == null) {
 236             throw new CertificateException("Missing input stream");
 237         }
 238         try {
 239             byte[] encoding = readOneBlock(inStream);
 240             if (encoding != null) {
 241                 return new X509CertPath(new ByteArrayInputStream(encoding));
 242             } else {
 243                 throw new IOException("Empty input");
 244             }
 245         } catch (IOException ioe) {
 246             throw new CertificateException(ioe.getMessage());
 247         }
 248     }
 249 
 250     /**
 251      * Generates a <code>CertPath</code> object and initializes it with
 252      * the data read from the <code>InputStream</code> inStream. The data
 253      * is assumed to be in the specified encoding.
 254      *
 255      * @param inStream an <code>InputStream</code> containing the data
 256      * @param encoding the encoding used for the data
 257      * @return a <code>CertPath</code> initialized with the data from the
 258      *   <code>InputStream</code>
 259      * @exception CertificateException if an exception occurs while decoding or
 260      *   the encoding requested is not supported
 261      * @since 1.4
 262      */
 263     public CertPath engineGenerateCertPath(InputStream inStream,
 264         String encoding) throws CertificateException
 265     {
 266         if (inStream == null) {
 267             throw new CertificateException("Missing input stream");
 268         }
 269         try {
 270             byte[] data = readOneBlock(inStream);
 271             if (data != null) {
 272                 return new X509CertPath(new ByteArrayInputStream(data), encoding);
 273             } else {
 274                 throw new IOException("Empty input");
 275             }
 276         } catch (IOException ioe) {
 277             throw new CertificateException(ioe.getMessage());
 278         }
 279     }
 280 
 281     /**
 282      * Generates a <code>CertPath</code> object and initializes it with
 283      * a <code>List</code> of <code>Certificate</code>s.
 284      * <p>
 285      * The certificates supplied must be of a type supported by the
 286      * <code>CertificateFactory</code>. They will be copied out of the supplied
 287      * <code>List</code> object.
 288      *
 289      * @param certificates a <code>List</code> of <code>Certificate</code>s
 290      * @return a <code>CertPath</code> initialized with the supplied list of
 291      *   certificates
 292      * @exception CertificateException if an exception occurs
 293      * @since 1.4
 294      */
 295     public CertPath
 296         engineGenerateCertPath(List<? extends Certificate> certificates)
 297         throws CertificateException
 298     {
 299         return(new X509CertPath(certificates));
 300     }
 301 
 302     /**
 303      * Returns an iteration of the <code>CertPath</code> encodings supported
 304      * by this certificate factory, with the default encoding first.
 305      * <p>
 306      * Attempts to modify the returned <code>Iterator</code> via its
 307      * <code>remove</code> method result in an
 308      * <code>UnsupportedOperationException</code>.
 309      *
 310      * @return an <code>Iterator</code> over the names of the supported
 311      *         <code>CertPath</code> encodings (as <code>String</code>s)
 312      * @since 1.4
 313      */
 314     public Iterator<String> engineGetCertPathEncodings() {
 315         return(X509CertPath.getEncodingsStatic());
 316     }
 317 
 318     /**
 319      * Returns a (possibly empty) collection view of X.509 certificates read
 320      * from the given input stream <code>is</code>.
 321      *
 322      * @param is the input stream with the certificates.
 323      *
 324      * @return a (possibly empty) collection view of X.509 certificate objects
 325      * initialized with the data from the input stream.
 326      *
 327      * @exception CertificateException on parsing errors.
 328      */
 329     public Collection<? extends java.security.cert.Certificate>
 330             engineGenerateCertificates(InputStream is)
 331             throws CertificateException {
 332         if (is == null) {
 333             throw new CertificateException("Missing input stream");
 334         }
 335         try {
 336             return parseX509orPKCS7Cert(is);
 337         } catch (IOException ioe) {
 338             throw new CertificateException(ioe);
 339         }
 340     }
 341 
 342     /**
 343      * Generates an X.509 certificate revocation list (CRL) object and
 344      * initializes it with the data read from the given input stream
 345      * <code>is</code>.
 346      *
 347      * @param is an input stream with the CRL data.
 348      *
 349      * @return an X.509 CRL object initialized with the data
 350      * from the input stream.
 351      *
 352      * @exception CRLException on parsing errors.
 353      */
 354     public CRL engineGenerateCRL(InputStream is)
 355         throws CRLException
 356     {
 357         if (is == null) {
 358             // clear the cache (for debugging)
 359             crlCache.clear();
 360             throw new CRLException("Missing input stream");
 361         }
 362         try {
 363             byte[] encoding = readOneBlock(is);
 364             if (encoding != null) {
 365                 X509CRLImpl crl = getFromCache(crlCache, encoding);
 366                 if (crl != null) {
 367                     return crl;
 368                 }
 369                 crl = new X509CRLImpl(encoding);
 370                 addToCache(crlCache, crl.getEncodedInternal(), crl);
 371                 return crl;
 372             } else {
 373                 throw new IOException("Empty input");
 374             }
 375         } catch (IOException ioe) {
 376             throw new CRLException(ioe.getMessage());
 377         }
 378     }
 379 
 380     /**
 381      * Returns a (possibly empty) collection view of X.509 CRLs read
 382      * from the given input stream <code>is</code>.
 383      *
 384      * @param is the input stream with the CRLs.
 385      *
 386      * @return a (possibly empty) collection view of X.509 CRL objects
 387      * initialized with the data from the input stream.
 388      *
 389      * @exception CRLException on parsing errors.
 390      */
 391     public Collection<? extends java.security.cert.CRL> engineGenerateCRLs(
 392             InputStream is) throws CRLException
 393     {
 394         if (is == null) {
 395             throw new CRLException("Missing input stream");
 396         }
 397         try {
 398             return parseX509orPKCS7CRL(is);
 399         } catch (IOException ioe) {
 400             throw new CRLException(ioe.getMessage());
 401         }
 402     }
 403 
 404     /*
 405      * Parses the data in the given input stream as a sequence of DER
 406      * encoded X.509 certificates (in binary or base 64 encoded format) OR
 407      * as a single PKCS#7 encoded blob (in binary or base64 encoded format).
 408      */
 409     private Collection<? extends java.security.cert.Certificate>
 410         parseX509orPKCS7Cert(InputStream is)
 411         throws CertificateException, IOException
 412     {
 413         Collection<X509CertImpl> coll = new ArrayList<>();
 414         byte[] data = readOneBlock(is);
 415         if (data == null) {
 416             return new ArrayList<>(0);
 417         }
 418         try {
 419             PKCS7 pkcs7 = new PKCS7(data);
 420             X509Certificate[] certs = pkcs7.getCertificates();
 421             // certs are optional in PKCS #7
 422             if (certs != null) {
 423                 return Arrays.asList(certs);
 424             } else {
 425                 // no crls provided
 426                 return new ArrayList<>(0);
 427             }
 428         } catch (ParsingException e) {
 429             while (data != null) {
 430                 coll.add(new X509CertImpl(data));
 431                 data = readOneBlock(is);
 432             }
 433         }
 434         return coll;
 435     }
 436 
 437     /*
 438      * Parses the data in the given input stream as a sequence of DER encoded
 439      * X.509 CRLs (in binary or base 64 encoded format) OR as a single PKCS#7
 440      * encoded blob (in binary or base 64 encoded format).
 441      */
 442     private Collection<? extends java.security.cert.CRL>
 443         parseX509orPKCS7CRL(InputStream is)
 444         throws CRLException, IOException
 445     {
 446         Collection<X509CRLImpl> coll = new ArrayList<>();
 447         byte[] data = readOneBlock(is);
 448         if (data == null) {
 449             return new ArrayList<>(0);
 450         }
 451         try {
 452             PKCS7 pkcs7 = new PKCS7(data);
 453             X509CRL[] crls = pkcs7.getCRLs();
 454             // CRLs are optional in PKCS #7
 455             if (crls != null) {
 456                 return Arrays.asList(crls);
 457             } else {
 458                 // no crls provided
 459                 return new ArrayList<>(0);
 460             }
 461         } catch (ParsingException e) {
 462             while (data != null) {
 463                 coll.add(new X509CRLImpl(data));
 464                 data = readOneBlock(is);
 465             }
 466         }
 467         return coll;
 468     }
 469 
 470     /**
 471      * Returns an ASN.1 SEQUENCE from a stream, which might be a BER-encoded
 472      * binary block or a PEM-style BASE64-encoded ASCII data. In the latter
 473      * case, it's de-BASE64'ed before return.
 474      *
 475      * After the reading, the input stream pointer is after the BER block, or
 476      * after the newline character after the -----END SOMETHING----- line.
 477      *
 478      * @param is the InputStream
 479      * @returns byte block or null if end of stream
 480      * @throws IOException If any parsing error
 481      */
 482     private static byte[] readOneBlock(InputStream is) throws IOException {
 483 
 484         // The first character of a BLOCK.
 485         int c = is.read();
 486         if (c == -1) {
 487             return null;
 488         }
 489         if (c == DerValue.tag_Sequence) {
 490             ByteArrayOutputStream bout = new ByteArrayOutputStream(2048);
 491             bout.write(c);
 492             readBERInternal(is, bout, c);
 493             return bout.toByteArray();
 494         } else {
 495             // Read BASE64 encoded data, might skip info at the beginning
 496             char[] data = new char[2048];
 497             int pos = 0;
 498 
 499             // Step 1: Read until header is found
 500             int hyphen = (c=='-') ? 1: 0;   // count of consequent hyphens
 501             int last = (c=='-') ? -1: c;    // the char before hyphen
 502             while (true) {
 503                 int next = is.read();
 504                 if (next == -1) {
 505                     // We accept useless data after the last block,
 506                     // say, empty lines.
 507                     return null;
 508                 }
 509                 if (next == '-') {
 510                     hyphen++;
 511                 } else {
 512                     hyphen = 0;
 513                     last = next;
 514                 }
 515                 if (hyphen == 5 && (last==-1 || last=='\r' || last=='\n')) {
 516                     break;
 517                 }
 518             }
 519 
 520             // Step 2: Read the rest of header, determine the line end
 521             int end;
 522             StringBuffer header = new StringBuffer("-----");
 523             while (true) {
 524                 int next = is.read();
 525                 if (next == -1) {
 526                     throw new IOException("Incomplete data");
 527                 }
 528                 if (next == '\n') {
 529                     end = '\n';
 530                     break;
 531                 }
 532                 if (next == '\r') {
 533                     next = is.read();
 534                     if (next == -1) {
 535                         throw new IOException("Incomplete data");
 536                     }
 537                     if (next == '\n') {
 538                         end = '\n';
 539                     } else {
 540                         end = '\r';
 541                         data[pos++] = (char)next;
 542                     }
 543                     break;
 544                 }
 545                 header.append((char)next);
 546             }
 547 
 548             // Step 3: Read the data
 549             while (true) {
 550                 int next = is.read();
 551                 if (next == -1) {
 552                     throw new IOException("Incomplete data");
 553                 }
 554                 if (next != '-') {
 555                     data[pos++] = (char)next;
 556                     if (pos >= data.length) {
 557                         data = Arrays.copyOf(data, data.length+1024);
 558                     }
 559                 } else {
 560                     break;
 561                 }
 562             }
 563 
 564             // Step 4: Consume the footer
 565             StringBuffer footer = new StringBuffer("-");
 566             while (true) {
 567                 int next = is.read();
 568                 // Add next == '\n' for maximum safety, in case endline
 569                 // is not consistent.
 570                 if (next == -1 || next == end || next == '\n') {
 571                     break;
 572                 }
 573                 if (next != '\r') footer.append((char)next);
 574             }
 575 
 576             checkHeaderFooter(header.toString(), footer.toString());
 577 
 578             BASE64Decoder decoder = new BASE64Decoder();
 579             return decoder.decodeBuffer(new String(data, 0, pos));
 580         }
 581     }
 582 
 583     private static void checkHeaderFooter(String header,
 584             String footer) throws IOException {
 585         if (header.length() < 16 || !header.startsWith("-----BEGIN ") ||
 586                 !header.endsWith("-----")) {
 587             throw new IOException("Illegal header: " + header);
 588         }
 589         if (footer.length() < 14 || !footer.startsWith("-----END ") ||
 590                 !footer.endsWith("-----")) {
 591             throw new IOException("Illegal footer: " + footer);
 592         }
 593         String headerType = header.substring(11, header.length()-5);
 594         String footerType = footer.substring(9, footer.length()-5);
 595         if (!headerType.equals(footerType)) {
 596             throw new IOException("Header and footer do not match: " +
 597                     header + " " + footer);
 598         }
 599     }
 600 
 601     /**
 602      * Read one BER data block. This method is aware of indefinite-length BER
 603      * encoding and will read all of the sub-sections in a recursive way
 604      *
 605      * @param is    Read from this InputStream
 606      * @param bout  Write into this OutputStream
 607      * @param tag   Tag already read (-1 mean not read)
 608      * @returns     The current tag, used to check EOC in indefinite-length BER
 609      * @throws IOException Any parsing error
 610      */
 611     private static int readBERInternal(InputStream is,
 612             ByteArrayOutputStream bout, int tag) throws IOException {
 613 
 614         if (tag == -1) {        // Not read before the call, read now
 615             tag = is.read();
 616             if (tag == -1) {
 617                 throw new IOException("BER/DER tag info absent");
 618             }
 619             if ((tag & 0x1f) == 0x1f) {
 620                 throw new IOException("Multi octets tag not supported");
 621             }
 622             bout.write(tag);
 623         }
 624 
 625         int n = is.read();
 626         if (n == -1) {
 627             throw new IOException("BER/DER length info ansent");
 628         }
 629         bout.write(n);
 630 
 631         int length;
 632 
 633         if (n == 0x80) {        // Indefinite-length encoding
 634             if ((tag & 0x20) != 0x20) {
 635                 throw new IOException(
 636                         "Non constructed encoding must have definite length");
 637             }
 638             while (true) {
 639                 int subTag = readBERInternal(is, bout, -1);
 640                 if (subTag == 0) {   // EOC, end of indefinite-length section
 641                     break;
 642                 }
 643             }
 644         } else {
 645             if (n < 0x80) {
 646                 length = n;
 647             } else if (n == 0x81) {
 648                 length = is.read();
 649                 if (length == -1) {
 650                     throw new IOException("Incomplete BER/DER length info");
 651                 }
 652                 bout.write(length);
 653             } else if (n == 0x82) {
 654                 int highByte = is.read();
 655                 int lowByte = is.read();
 656                 if (lowByte == -1) {
 657                     throw new IOException("Incomplete BER/DER length info");
 658                 }
 659                 bout.write(highByte);
 660                 bout.write(lowByte);
 661                 length = (highByte << 8) | lowByte;
 662             } else if (n == 0x83) {
 663                 int highByte = is.read();
 664                 int midByte = is.read();
 665                 int lowByte = is.read();
 666                 if (lowByte == -1) {
 667                     throw new IOException("Incomplete BER/DER length info");
 668                 }
 669                 bout.write(highByte);
 670                 bout.write(midByte);
 671                 bout.write(lowByte);
 672                 length = (highByte << 16) | (midByte << 8) | lowByte;
 673             } else if (n == 0x84) {
 674                 int highByte = is.read();
 675                 int nextByte = is.read();
 676                 int midByte = is.read();
 677                 int lowByte = is.read();
 678                 if (lowByte == -1) {
 679                     throw new IOException("Incomplete BER/DER length info");
 680                 }
 681                 if (highByte > 127) {
 682                     throw new IOException("Invalid BER/DER data (a little huge?)");
 683                 }
 684                 bout.write(highByte);
 685                 bout.write(nextByte);
 686                 bout.write(midByte);
 687                 bout.write(lowByte);
 688                 length = (highByte << 24 ) | (nextByte << 16) |
 689                         (midByte << 8) | lowByte;
 690             } else { // ignore longer length forms
 691                 throw new IOException("Invalid BER/DER data (too huge?)");
 692             }
 693             if (readFully(is, bout, length) != length) {
 694                 throw new IOException("Incomplete BER/DER data");
 695             }
 696         }
 697         return tag;
 698     }
 699 }