1package org.bouncycastle.cms;
2
3import java.io.IOException;
4import java.io.OutputStream;
5import java.security.NoSuchAlgorithmException;
6import java.security.NoSuchProviderException;
7import java.security.Provider;
8import java.security.PublicKey;
9import java.security.cert.CertificateExpiredException;
10import java.security.cert.CertificateNotYetValidException;
11import java.security.cert.X509Certificate;
12import java.util.ArrayList;
13import java.util.Enumeration;
14import java.util.Iterator;
15import java.util.List;
16
17import org.bouncycastle.asn1.ASN1Encodable;
18import org.bouncycastle.asn1.ASN1EncodableVector;
19import org.bouncycastle.asn1.ASN1Encoding;
20import org.bouncycastle.asn1.ASN1ObjectIdentifier;
21import org.bouncycastle.asn1.ASN1OctetString;
22import org.bouncycastle.asn1.ASN1Primitive;
23import org.bouncycastle.asn1.ASN1Set;
24import org.bouncycastle.asn1.DERObjectIdentifier;
25import org.bouncycastle.asn1.DERSet;
26import org.bouncycastle.asn1.cms.Attribute;
27import org.bouncycastle.asn1.cms.AttributeTable;
28import org.bouncycastle.asn1.cms.CMSAttributes;
29import org.bouncycastle.asn1.cms.IssuerAndSerialNumber;
30import org.bouncycastle.asn1.cms.SignerIdentifier;
31import org.bouncycastle.asn1.cms.SignerInfo;
32import org.bouncycastle.asn1.cms.Time;
33import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
34import org.bouncycastle.asn1.x509.DigestInfo;
35import org.bouncycastle.cert.X509CertificateHolder;
36import org.bouncycastle.cms.jcajce.JcaSignerInfoVerifierBuilder;
37import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder;
38import org.bouncycastle.operator.ContentVerifier;
39import org.bouncycastle.operator.DigestCalculator;
40import org.bouncycastle.operator.OperatorCreationException;
41import org.bouncycastle.operator.RawContentVerifier;
42import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
43import org.bouncycastle.util.Arrays;
44
45/**
46 * an expanded SignerInfo block from a CMS Signed message
47 */
48public class SignerInformation
49{
50    private SignerId                sid;
51    private SignerInfo              info;
52    private AlgorithmIdentifier     digestAlgorithm;
53    private AlgorithmIdentifier     encryptionAlgorithm;
54    private final ASN1Set           signedAttributeSet;
55    private final ASN1Set           unsignedAttributeSet;
56    private CMSProcessable          content;
57    private byte[]                  signature;
58    private ASN1ObjectIdentifier    contentType;
59    private byte[]                  resultDigest;
60
61    // Derived
62    private AttributeTable          signedAttributeValues;
63    private AttributeTable          unsignedAttributeValues;
64    private boolean                 isCounterSignature;
65
66    SignerInformation(
67        SignerInfo          info,
68        ASN1ObjectIdentifier contentType,
69        CMSProcessable      content,
70        byte[]              resultDigest)
71    {
72        this.info = info;
73        this.contentType = contentType;
74        this.isCounterSignature = contentType == null;
75
76        SignerIdentifier   s = info.getSID();
77
78        if (s.isTagged())
79        {
80            ASN1OctetString octs = ASN1OctetString.getInstance(s.getId());
81
82            sid = new SignerId(octs.getOctets());
83        }
84        else
85        {
86            IssuerAndSerialNumber   iAnds = IssuerAndSerialNumber.getInstance(s.getId());
87
88            sid = new SignerId(iAnds.getName(), iAnds.getSerialNumber().getValue());
89        }
90
91        this.digestAlgorithm = info.getDigestAlgorithm();
92        this.signedAttributeSet = info.getAuthenticatedAttributes();
93        this.unsignedAttributeSet = info.getUnauthenticatedAttributes();
94        this.encryptionAlgorithm = info.getDigestEncryptionAlgorithm();
95        this.signature = info.getEncryptedDigest().getOctets();
96
97        this.content = content;
98        this.resultDigest = resultDigest;
99    }
100
101    public boolean isCounterSignature()
102    {
103        return isCounterSignature;
104    }
105
106    public ASN1ObjectIdentifier getContentType()
107    {
108        return this.contentType;
109    }
110
111    private byte[] encodeObj(
112        ASN1Encodable    obj)
113        throws IOException
114    {
115        if (obj != null)
116        {
117            return obj.toASN1Primitive().getEncoded();
118        }
119
120        return null;
121    }
122
123    public SignerId getSID()
124    {
125        return sid;
126    }
127
128    /**
129     * return the version number for this objects underlying SignerInfo structure.
130     */
131    public int getVersion()
132    {
133        return info.getVersion().getValue().intValue();
134    }
135
136    public AlgorithmIdentifier getDigestAlgorithmID()
137    {
138        return digestAlgorithm;
139    }
140
141    /**
142     * return the object identifier for the signature.
143     */
144    public String getDigestAlgOID()
145    {
146        return digestAlgorithm.getObjectId().getId();
147    }
148
149    /**
150     * return the signature parameters, or null if there aren't any.
151     */
152    public byte[] getDigestAlgParams()
153    {
154        try
155        {
156            return encodeObj(digestAlgorithm.getParameters());
157        }
158        catch (Exception e)
159        {
160            throw new RuntimeException("exception getting digest parameters " + e);
161        }
162    }
163
164    /**
165     * return the content digest that was calculated during verification.
166     */
167    public byte[] getContentDigest()
168    {
169        if (resultDigest == null)
170        {
171            throw new IllegalStateException("method can only be called after verify.");
172        }
173
174        return (byte[])resultDigest.clone();
175    }
176
177    /**
178     * return the object identifier for the signature.
179     */
180    public String getEncryptionAlgOID()
181    {
182        return encryptionAlgorithm.getObjectId().getId();
183    }
184
185    /**
186     * return the signature/encryption algorithm parameters, or null if
187     * there aren't any.
188     */
189    public byte[] getEncryptionAlgParams()
190    {
191        try
192        {
193            return encodeObj(encryptionAlgorithm.getParameters());
194        }
195        catch (Exception e)
196        {
197            throw new RuntimeException("exception getting encryption parameters " + e);
198        }
199    }
200
201    /**
202     * return a table of the signed attributes - indexed by
203     * the OID of the attribute.
204     */
205    public AttributeTable getSignedAttributes()
206    {
207        if (signedAttributeSet != null && signedAttributeValues == null)
208        {
209            signedAttributeValues = new AttributeTable(signedAttributeSet);
210        }
211
212        return signedAttributeValues;
213    }
214
215    /**
216     * return a table of the unsigned attributes indexed by
217     * the OID of the attribute.
218     */
219    public AttributeTable getUnsignedAttributes()
220    {
221        if (unsignedAttributeSet != null && unsignedAttributeValues == null)
222        {
223            unsignedAttributeValues = new AttributeTable(unsignedAttributeSet);
224        }
225
226        return unsignedAttributeValues;
227    }
228
229    /**
230     * return the encoded signature
231     */
232    public byte[] getSignature()
233    {
234        return (byte[])signature.clone();
235    }
236
237    /**
238     * Return a SignerInformationStore containing the counter signatures attached to this
239     * signer. If no counter signatures are present an empty store is returned.
240     */
241    public SignerInformationStore getCounterSignatures()
242    {
243        // TODO There are several checks implied by the RFC3852 comments that are missing
244
245        /*
246        The countersignature attribute MUST be an unsigned attribute; it MUST
247        NOT be a signed attribute, an authenticated attribute, an
248        unauthenticated attribute, or an unprotected attribute.
249        */
250        AttributeTable unsignedAttributeTable = getUnsignedAttributes();
251        if (unsignedAttributeTable == null)
252        {
253            return new SignerInformationStore(new ArrayList(0));
254        }
255
256        List counterSignatures = new ArrayList();
257
258        /*
259        The UnsignedAttributes syntax is defined as a SET OF Attributes.  The
260        UnsignedAttributes in a signerInfo may include multiple instances of
261        the countersignature attribute.
262        */
263        ASN1EncodableVector allCSAttrs = unsignedAttributeTable.getAll(CMSAttributes.counterSignature);
264
265        for (int i = 0; i < allCSAttrs.size(); ++i)
266        {
267            Attribute counterSignatureAttribute = (Attribute)allCSAttrs.get(i);
268
269            /*
270            A countersignature attribute can have multiple attribute values.  The
271            syntax is defined as a SET OF AttributeValue, and there MUST be one
272            or more instances of AttributeValue present.
273            */
274            ASN1Set values = counterSignatureAttribute.getAttrValues();
275            if (values.size() < 1)
276            {
277                // TODO Throw an appropriate exception?
278            }
279
280            for (Enumeration en = values.getObjects(); en.hasMoreElements();)
281            {
282                /*
283                Countersignature values have the same meaning as SignerInfo values
284                for ordinary signatures, except that:
285
286                   1. The signedAttributes field MUST NOT contain a content-type
287                      attribute; there is no content type for countersignatures.
288
289                   2. The signedAttributes field MUST contain a message-digest
290                      attribute if it contains any other attributes.
291
292                   3. The input to the message-digesting process is the contents
293                      octets of the DER encoding of the signatureValue field of the
294                      SignerInfo value with which the attribute is associated.
295                */
296                SignerInfo si = SignerInfo.getInstance(en.nextElement());
297
298                counterSignatures.add(new SignerInformation(si, null, new CMSProcessableByteArray(getSignature()), null));
299            }
300        }
301
302        return new SignerInformationStore(counterSignatures);
303    }
304
305    /**
306     * return the DER encoding of the signed attributes.
307     * @throws IOException if an encoding error occurs.
308     */
309    public byte[] getEncodedSignedAttributes()
310        throws IOException
311    {
312        if (signedAttributeSet != null)
313        {
314            return signedAttributeSet.getEncoded();
315        }
316
317        return null;
318    }
319
320    /**
321     * @deprecated
322     */
323    private boolean doVerify(
324        PublicKey       key,
325        Provider        sigProvider)
326        throws CMSException, NoSuchAlgorithmException
327    {
328        try
329        {
330            SignerInformationVerifier verifier;
331
332            if (sigProvider != null)
333            {
334                if (!sigProvider.getName().equalsIgnoreCase("BC"))
335                {
336                    verifier = new JcaSignerInfoVerifierBuilder(new JcaDigestCalculatorProviderBuilder().build()).setProvider(sigProvider).build(key);
337                }
338                else
339                {
340                    verifier = new JcaSimpleSignerInfoVerifierBuilder().setProvider(sigProvider).build(key);
341                }
342            }
343            else
344            {
345                verifier = new JcaSimpleSignerInfoVerifierBuilder().build(key);
346            }
347
348            return doVerify(verifier);
349        }
350        catch (OperatorCreationException e)
351        {
352            throw new CMSException("unable to create verifier: " + e.getMessage(), e);
353        }
354    }
355
356    private boolean doVerify(
357        SignerInformationVerifier verifier)
358        throws CMSException
359    {
360        String          encName = CMSSignedHelper.INSTANCE.getEncryptionAlgName(this.getEncryptionAlgOID());
361
362        try
363        {
364            if (resultDigest == null)
365            {
366                DigestCalculator calc = verifier.getDigestCalculator(this.getDigestAlgorithmID());
367                if (content != null)
368                {
369                    OutputStream      digOut = calc.getOutputStream();
370
371                    content.write(digOut);
372
373                    digOut.close();
374                }
375                else if (signedAttributeSet == null)
376                {
377                    // TODO Get rid of this exception and just treat content==null as empty not missing?
378                    throw new CMSException("data not encapsulated in signature - use detached constructor.");
379                }
380
381                resultDigest = calc.getDigest();
382            }
383        }
384        catch (IOException e)
385        {
386            throw new CMSException("can't process mime object to create signature.", e);
387        }
388        catch (OperatorCreationException e)
389        {
390            throw new CMSException("can't create digest calculator: " + e.getMessage(), e);
391        }
392
393        // RFC 3852 11.1 Check the content-type attribute is correct
394        {
395            ASN1Primitive validContentType = getSingleValuedSignedAttribute(
396                CMSAttributes.contentType, "content-type");
397            if (validContentType == null)
398            {
399                if (!isCounterSignature && signedAttributeSet != null)
400                {
401                    throw new CMSException("The content-type attribute type MUST be present whenever signed attributes are present in signed-data");
402                }
403            }
404            else
405            {
406                if (isCounterSignature)
407                {
408                    throw new CMSException("[For counter signatures,] the signedAttributes field MUST NOT contain a content-type attribute");
409                }
410
411                if (!(validContentType instanceof DERObjectIdentifier))
412                {
413                    throw new CMSException("content-type attribute value not of ASN.1 type 'OBJECT IDENTIFIER'");
414                }
415
416                DERObjectIdentifier signedContentType = (DERObjectIdentifier)validContentType;
417
418                if (!signedContentType.equals(contentType))
419                {
420                    throw new CMSException("content-type attribute value does not match eContentType");
421                }
422            }
423        }
424
425        // RFC 3852 11.2 Check the message-digest attribute is correct
426        {
427            ASN1Primitive validMessageDigest = getSingleValuedSignedAttribute(
428                CMSAttributes.messageDigest, "message-digest");
429            if (validMessageDigest == null)
430            {
431                if (signedAttributeSet != null)
432                {
433                    throw new CMSException("the message-digest signed attribute type MUST be present when there are any signed attributes present");
434                }
435            }
436            else
437            {
438                if (!(validMessageDigest instanceof ASN1OctetString))
439                {
440                    throw new CMSException("message-digest attribute value not of ASN.1 type 'OCTET STRING'");
441                }
442
443                ASN1OctetString signedMessageDigest = (ASN1OctetString)validMessageDigest;
444
445                if (!Arrays.constantTimeAreEqual(resultDigest, signedMessageDigest.getOctets()))
446                {
447                    throw new CMSSignerDigestMismatchException("message-digest attribute value does not match calculated value");
448                }
449            }
450        }
451
452        // RFC 3852 11.4 Validate countersignature attribute(s)
453        {
454            AttributeTable signedAttrTable = this.getSignedAttributes();
455            if (signedAttrTable != null
456                && signedAttrTable.getAll(CMSAttributes.counterSignature).size() > 0)
457            {
458                throw new CMSException("A countersignature attribute MUST NOT be a signed attribute");
459            }
460
461            AttributeTable unsignedAttrTable = this.getUnsignedAttributes();
462            if (unsignedAttrTable != null)
463            {
464                ASN1EncodableVector csAttrs = unsignedAttrTable.getAll(CMSAttributes.counterSignature);
465                for (int i = 0; i < csAttrs.size(); ++i)
466                {
467                    Attribute csAttr = (Attribute)csAttrs.get(i);
468                    if (csAttr.getAttrValues().size() < 1)
469                    {
470                        throw new CMSException("A countersignature attribute MUST contain at least one AttributeValue");
471                    }
472
473                    // Note: We don't recursively validate the countersignature value
474                }
475            }
476        }
477
478        try
479        {
480            ContentVerifier contentVerifier = verifier.getContentVerifier(encryptionAlgorithm, info.getDigestAlgorithm());
481            OutputStream sigOut = contentVerifier.getOutputStream();
482
483            if (signedAttributeSet == null)
484            {
485                if (resultDigest != null)
486                {
487                    if (contentVerifier instanceof RawContentVerifier)
488                    {
489                        RawContentVerifier rawVerifier = (RawContentVerifier)contentVerifier;
490
491                        if (encName.equals("RSA"))
492                        {
493                            DigestInfo digInfo = new DigestInfo(digestAlgorithm, resultDigest);
494
495                            return rawVerifier.verify(digInfo.getEncoded(ASN1Encoding.DER), this.getSignature());
496                        }
497
498                        return rawVerifier.verify(resultDigest, this.getSignature());
499                    }
500
501                    throw new CMSException("verifier unable to process raw signature");
502                }
503                else if (content != null)
504                {
505                    // TODO Use raw signature of the hash value instead
506                    content.write(sigOut);
507                }
508            }
509            else
510            {
511                sigOut.write(this.getEncodedSignedAttributes());
512            }
513
514            sigOut.close();
515
516            return contentVerifier.verify(this.getSignature());
517        }
518        catch (IOException e)
519        {
520            throw new CMSException("can't process mime object to create signature.", e);
521        }
522        catch (OperatorCreationException e)
523        {
524            throw new CMSException("can't create content verifier: " + e.getMessage(), e);
525        }
526    }
527
528    /**
529     * verify that the given public key successfully handles and confirms the
530     * signature associated with this signer.
531     * @deprecated use verify(ContentVerifierProvider)
532     */
533    public boolean verify(
534        PublicKey   key,
535        String      sigProvider)
536        throws NoSuchAlgorithmException, NoSuchProviderException, CMSException
537    {
538        return verify(key, CMSUtils.getProvider(sigProvider));
539    }
540
541    /**
542     * verify that the given public key successfully handles and confirms the
543     * signature associated with this signer
544     * @deprecated use verify(ContentVerifierProvider)
545     */
546    public boolean verify(
547        PublicKey   key,
548        Provider    sigProvider)
549        throws NoSuchAlgorithmException, NoSuchProviderException, CMSException
550    {
551        // Optional, but still need to validate if present
552        getSigningTime();
553
554        return doVerify(key, sigProvider);
555    }
556
557    /**
558     * verify that the given certificate successfully handles and confirms
559     * the signature associated with this signer and, if a signingTime
560     * attribute is available, that the certificate was valid at the time the
561     * signature was generated.
562     * @deprecated use verify(ContentVerifierProvider)
563     */
564    public boolean verify(
565        X509Certificate cert,
566        String          sigProvider)
567        throws NoSuchAlgorithmException, NoSuchProviderException,
568            CertificateExpiredException, CertificateNotYetValidException,
569            CMSException
570    {
571        return verify(cert, CMSUtils.getProvider(sigProvider));
572    }
573
574    /**
575     * verify that the given certificate successfully handles and confirms
576     * the signature associated with this signer and, if a signingTime
577     * attribute is available, that the certificate was valid at the time the
578     * signature was generated.
579     * @deprecated use verify(ContentVerifierProvider)
580     */
581    public boolean verify(
582        X509Certificate cert,
583        Provider        sigProvider)
584        throws NoSuchAlgorithmException,
585            CertificateExpiredException, CertificateNotYetValidException,
586            CMSException
587    {
588        Time signingTime = getSigningTime();
589        if (signingTime != null)
590        {
591            cert.checkValidity(signingTime.getDate());
592        }
593
594        return doVerify(cert.getPublicKey(), sigProvider);
595    }
596
597    /**
598     * Verify that the given verifier can successfully verify the signature on
599     * this SignerInformation object.
600     *
601     * @param verifier a suitably configured SignerInformationVerifier.
602     * @return true if the signer information is verified, false otherwise.
603     * @throws org.bouncycastle.cms.CMSVerifierCertificateNotValidException if the provider has an associated certificate and the certificate is not valid at the time given as the SignerInfo's signing time.
604     * @throws org.bouncycastle.cms.CMSException if the verifier is unable to create a ContentVerifiers or DigestCalculators.
605     */
606    public boolean verify(SignerInformationVerifier verifier)
607        throws CMSException
608    {
609        Time signingTime = getSigningTime();   // has to be validated if present.
610
611        if (verifier.hasAssociatedCertificate())
612        {
613            if (signingTime != null)
614            {
615                X509CertificateHolder dcv = verifier.getAssociatedCertificate();
616
617                if (!dcv.isValidOn(signingTime.getDate()))
618                {
619                    throw new CMSVerifierCertificateNotValidException("verifier not valid at signingTime");
620                }
621            }
622        }
623
624        return doVerify(verifier);
625    }
626
627    /**
628     * Return the base ASN.1 CMS structure that this object contains.
629     *
630     * @return an object containing a CMS SignerInfo structure.
631     * @deprecated use toASN1Structure()
632     */
633    public SignerInfo toSignerInfo()
634    {
635        return info;
636    }
637
638    /**
639     * Return the underlying ASN.1 object defining this SignerInformation object.
640     *
641     * @return a SignerInfo.
642     */
643    public SignerInfo toASN1Structure()
644    {
645        return info;
646    }
647
648    private ASN1Primitive getSingleValuedSignedAttribute(
649        ASN1ObjectIdentifier attrOID, String printableName)
650        throws CMSException
651    {
652        AttributeTable unsignedAttrTable = this.getUnsignedAttributes();
653        if (unsignedAttrTable != null
654            && unsignedAttrTable.getAll(attrOID).size() > 0)
655        {
656            throw new CMSException("The " + printableName
657                + " attribute MUST NOT be an unsigned attribute");
658        }
659
660        AttributeTable signedAttrTable = this.getSignedAttributes();
661        if (signedAttrTable == null)
662        {
663            return null;
664        }
665
666        ASN1EncodableVector v = signedAttrTable.getAll(attrOID);
667        switch (v.size())
668        {
669            case 0:
670                return null;
671            case 1:
672            {
673                Attribute t = (Attribute)v.get(0);
674                ASN1Set attrValues = t.getAttrValues();
675                if (attrValues.size() != 1)
676                {
677                    throw new CMSException("A " + printableName
678                        + " attribute MUST have a single attribute value");
679                }
680
681                return attrValues.getObjectAt(0).toASN1Primitive();
682            }
683            default:
684                throw new CMSException("The SignedAttributes in a signerInfo MUST NOT include multiple instances of the "
685                    + printableName + " attribute");
686        }
687    }
688
689    private Time getSigningTime() throws CMSException
690    {
691        ASN1Primitive validSigningTime = getSingleValuedSignedAttribute(
692            CMSAttributes.signingTime, "signing-time");
693
694        if (validSigningTime == null)
695        {
696            return null;
697        }
698
699        try
700        {
701            return Time.getInstance(validSigningTime);
702        }
703        catch (IllegalArgumentException e)
704        {
705            throw new CMSException("signing-time attribute value not a valid 'Time' structure");
706        }
707    }
708
709    /**
710     * Return a signer information object with the passed in unsigned
711     * attributes replacing the ones that are current associated with
712     * the object passed in.
713     *
714     * @param signerInformation the signerInfo to be used as the basis.
715     * @param unsignedAttributes the unsigned attributes to add.
716     * @return a copy of the original SignerInformationObject with the changed attributes.
717     */
718    public static SignerInformation replaceUnsignedAttributes(
719        SignerInformation   signerInformation,
720        AttributeTable      unsignedAttributes)
721    {
722        SignerInfo  sInfo = signerInformation.info;
723        ASN1Set     unsignedAttr = null;
724
725        if (unsignedAttributes != null)
726        {
727            unsignedAttr = new DERSet(unsignedAttributes.toASN1EncodableVector());
728        }
729
730        return new SignerInformation(
731                new SignerInfo(sInfo.getSID(), sInfo.getDigestAlgorithm(),
732                    sInfo.getAuthenticatedAttributes(), sInfo.getDigestEncryptionAlgorithm(), sInfo.getEncryptedDigest(), unsignedAttr),
733                    signerInformation.contentType, signerInformation.content, null);
734    }
735
736    /**
737     * Return a signer information object with passed in SignerInformationStore representing counter
738     * signatures attached as an unsigned attribute.
739     *
740     * @param signerInformation the signerInfo to be used as the basis.
741     * @param counterSigners signer info objects carrying counter signature.
742     * @return a copy of the original SignerInformationObject with the changed attributes.
743     */
744    public static SignerInformation addCounterSigners(
745        SignerInformation        signerInformation,
746        SignerInformationStore   counterSigners)
747    {
748        // TODO Perform checks from RFC 3852 11.4
749
750        SignerInfo          sInfo = signerInformation.info;
751        AttributeTable      unsignedAttr = signerInformation.getUnsignedAttributes();
752        ASN1EncodableVector v;
753
754        if (unsignedAttr != null)
755        {
756            v = unsignedAttr.toASN1EncodableVector();
757        }
758        else
759        {
760            v = new ASN1EncodableVector();
761        }
762
763        ASN1EncodableVector sigs = new ASN1EncodableVector();
764
765        for (Iterator it = counterSigners.getSigners().iterator(); it.hasNext();)
766        {
767            sigs.add(((SignerInformation)it.next()).toSignerInfo());
768        }
769
770        v.add(new Attribute(CMSAttributes.counterSignature, new DERSet(sigs)));
771
772        return new SignerInformation(
773                new SignerInfo(sInfo.getSID(), sInfo.getDigestAlgorithm(),
774                    sInfo.getAuthenticatedAttributes(), sInfo.getDigestEncryptionAlgorithm(), sInfo.getEncryptedDigest(), new DERSet(v)),
775                    signerInformation.contentType, signerInformation.content, null);
776    }
777}
778