1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29 package dk.sosi.seal.model;
30
31 import dk.sosi.seal.SOSIFactory;
32 import dk.sosi.seal.model.constants.*;
33 import dk.sosi.seal.modelbuilders.SignatureInvalidModelBuildException;
34 import dk.sosi.seal.pki.*;
35 import dk.sosi.seal.transform.internal.STRTransform;
36 import dk.sosi.seal.vault.CredentialPair;
37 import dk.sosi.seal.vault.CredentialVault;
38 import dk.sosi.seal.vault.CredentialVaultException;
39 import dk.sosi.seal.xml.CertificateParser;
40 import dk.sosi.seal.xml.XmlUtil;
41 import org.apache.ws.security.WSDocInfo;
42 import org.apache.ws.security.WSDocInfoStore;
43 import org.apache.xml.security.c14n.CanonicalizationException;
44 import org.apache.xml.security.c14n.Canonicalizer;
45 import org.apache.xml.security.c14n.InvalidCanonicalizerException;
46 import org.apache.xml.security.exceptions.AlgorithmAlreadyRegisteredException;
47 import org.apache.xml.security.exceptions.XMLSecurityException;
48 import org.apache.xml.security.keys.KeyInfo;
49 import org.apache.xml.security.keys.keyresolver.KeyResolverException;
50 import org.apache.xml.security.signature.XMLSignature;
51 import org.apache.xml.security.signature.XMLSignatureException;
52 import org.apache.xml.security.transforms.Transform;
53 import org.apache.xml.security.transforms.TransformationException;
54 import org.apache.xml.security.transforms.Transforms;
55 import org.w3c.dom.*;
56
57 import java.io.BufferedOutputStream;
58 import java.io.ByteArrayOutputStream;
59 import java.security.KeyStore;
60 import java.security.Provider;
61 import java.security.Security;
62 import java.security.cert.CertificateEncodingException;
63 import java.security.cert.X509Certificate;
64 import java.util.Iterator;
65 import java.util.LinkedList;
66 import java.util.List;
67 import java.util.Properties;
68 import java.util.logging.Level;
69 import java.util.logging.Logger;
70
71
72
73
74
75
76
77
78 public class SignatureUtil {
79
80
81 static final boolean bcAddedInJavaSecurity = Security.getProvider(SOSIFactory.PROPERTYVALUE_SOSI_CRYPTOPROVIDER_BOUNCYCASTLE) != null;
82
83 static {
84
85 org.apache.xml.security.Init.init();
86 if (! Boolean.getBoolean(SOSIFactory.PROPERTYNAME_SOSI_DO_NOT_REGISTER_STR_TRANSFORM)) {
87 try {
88 Transform.register(STRTransform.implementedTransformURI, STRTransform.class.getName());
89 } catch (AlgorithmAlreadyRegisteredException e) {
90 throw new RuntimeException("Unable to register STR-Transform " + STRTransform.class.getName(), e);
91 }
92 }
93
94 Logger.getLogger("org.apache.xml.security.signature.Reference").setLevel(Level.OFF);
95 }
96
97
98 private static class CredentialPairAdapterVault implements CredentialVault {
99
100 private final CredentialPair credentialPair;
101
102 public CredentialPairAdapterVault(CredentialPair credentialPair) {
103 this.credentialPair = credentialPair;
104 }
105
106 public boolean isTrustedCertificate(X509Certificate certificate) throws CredentialVaultException {
107 throw new UnsupportedOperationException();
108 }
109
110 public CredentialPair getSystemCredentialPair() throws CredentialVaultException {
111 return credentialPair;
112 }
113
114 public void setSystemCredentialPair(CredentialPair credentialPair) throws CredentialVaultException {
115 throw new UnsupportedOperationException();
116 }
117
118 public KeyStore getKeyStore() {
119 throw new UnsupportedOperationException();
120 }
121 }
122
123
124 private static class CertificateFederation extends Federation {
125
126 private final X509Certificate certificate;
127
128 public CertificateFederation(X509Certificate certificate) {
129 super(System.getProperties(), null);
130 this.certificate = certificate;
131 }
132
133 @Override
134 protected boolean subjectDistinguishedNameMatches(DistinguishedName subjectDistinguishedName) {
135 return false;
136 }
137
138 @Override
139 public X509Certificate getFederationCertificate(FederationCertificateReference reference) {
140 return certificate;
141 }
142 }
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162 @Deprecated
163 public static byte[] getSignedInfoBytes(String[] referenceUris, Document doc, String signatureParentID) {
164 SignatureConfiguration configuration = new SignatureConfiguration(referenceUris, signatureParentID, IDValues.id);
165 return getSignedInfoBytes(doc, configuration);
166 }
167
168 public static byte[] getSignedInfoBytes(Document doc, SignatureConfiguration configuration) {
169 XMLSignature xmlSignature = initXmlSignature(doc, configuration);
170
171 try {
172 xmlSignature.getSignedInfo().generateDigestValues();
173 } catch (XMLSignatureException e) {
174 throw new ModelException("Unable to digest values", e);
175 }
176
177 tidyXML(xmlSignature.getElement());
178
179 ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
180
181 try {
182 xmlSignature.getSignedInfo().signInOctectStream(new BufferedOutputStream(byteArrayOutputStream));
183 } catch (XMLSecurityException e) {
184 throw new ModelException("Unable to generate c14n of SignedInfo", e);
185 }
186
187 return byteArrayOutputStream.toByteArray();
188 }
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205 @Deprecated
206 public static void sign(String[] referenceUris, CredentialPair credentialPair, Document doc, String signatureSiblingLocationID) {
207 SignatureConfiguration signatureConfiguration = new SignatureConfiguration(referenceUris, signatureSiblingLocationID, IDValues.id);
208 CredentialVault vault = new CredentialPairAdapterVault(credentialPair);
209 sign(new CredentialVaultSignatureProvider(vault), doc, signatureConfiguration);
210 }
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226 @Deprecated
227 public static void sign(CredentialPair credentialPair, Document doc, SignatureConfiguration configuration) {
228 CredentialVault vault = new CredentialPairAdapterVault(credentialPair);
229 sign(new CredentialVaultSignatureProvider(vault), doc, configuration);
230 }
231
232 public static void sign(SignatureProvider provider, Document doc, SignatureConfiguration configuration) {
233 WSDocInfo wsDocInfo = new WSDocInfo(doc);
234 WSDocInfoStore.store(wsDocInfo);
235
236 try {
237
238 byte[] bytes = getSignedInfoBytes(doc, configuration);
239 SignatureProvider.SignatureResult result = provider.sign(bytes);
240 injectSignature(doc, result.getSignature(), configuration, result.getCertificate(), false);
241
242 } finally {
243 WSDocInfoStore.delete(wsDocInfo);
244 }
245 }
246
247
248
249
250
251
252
253
254
255 public static boolean validate(Node signatureToValidate, Federation federation, CredentialVault vault, boolean checkTrust) {
256 return internalValidate(signatureToValidate, federation, vault, checkTrust);
257 }
258
259
260
261
262
263
264
265
266
267
268 public static X509Certificate getCertificateFromSignature(Node signatureElement) throws ModelException {
269 NodeList nodeList = ((Element) signatureElement).getElementsByTagNameNS(NameSpaces.DSIG_SCHEMA, DSTags.X509CERTIFICATE);
270 Node x509Certificate = nodeList.getLength() > 0 ? nodeList.item(0) : null;
271
272 if (x509Certificate == null) {
273 throw new ModelException("No " + NameSpaces.DSIG_SCHEMA + ":" + DSTags.X509CERTIFICATE + " element in signature " + signatureElement);
274 }
275 String certValue = XmlUtil.getTextNodeValue(x509Certificate);
276
277 certValue = certValue.replaceAll("\\s", "");
278 byte[] base64decodedCertValue = XmlUtil.fromBase64(certValue);
279 return CertificateParser.asCertificate(base64decodedCertValue);
280
281 }
282
283
284
285
286
287
288
289
290
291
292
293 public static String getDigestOfCertificate(X509Certificate certificate) throws ModelException {
294
295 if (certificate == null)
296 return null;
297 byte[] certAsBytes;
298 try {
299 certAsBytes = certificate.getEncoded();
300 } catch (CertificateEncodingException e) {
301 throw new ModelException("Unable to convert certificate to byte array", e);
302 }
303
304 byte[] hash = XmlUtil.getSha1Digest(certAsBytes);
305
306 return XmlUtil.toBase64(hash);
307 }
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322 @Deprecated
323 public static void injectSignature(Document document, String signature, String signatureParentElementID, X509Certificate certificate) {
324 SignatureConfiguration configuration = new SignatureConfiguration((SignatureConfiguration.Reference[]) null, signatureParentElementID, null);
325 injectSignature(document, signature, configuration, certificate, true);
326 }
327
328 public static void injectSignature(Document document, String signature, SignatureConfiguration configuration, X509Certificate certificate, boolean validateSignature) {
329 if (certificate == null) {
330 throw new ModelException("X509Certifcate cannot be <null>");
331 }
332
333 Element signatureParentElement = (Element) XmlUtil.getElementByIdExtended(document, configuration.getSignatureParentID());
334
335 List<Element> signatureElements = XmlUtil.getChildElementsNS(signatureParentElement, NameSpaces.DSIG_SCHEMA, DSTags.SIGNATURE);
336 if (signatureElements.size() != 1) {
337 throw new ModelException("Expected 1 but found " + signatureElements.size() + " Signature elements");
338 }
339 Element elmSignature = signatureElements.get(0);
340
341 List<Element> signatureValueElements = XmlUtil.getChildElementsNS(elmSignature, NameSpaces.DSIG_SCHEMA, DSTags.SIGNATURE_VALUE);
342 if (signatureValueElements.size() != 1) {
343 throw new ModelException("Expected 1 but found " + signatureValueElements.size() + " SignatureValue elements");
344 }
345 signatureValueElements.get(0).appendChild(document.createTextNode(signature));
346
347 addKeyInfo(signatureParentElement, configuration, certificate);
348
349
350 if (validateSignature && !internalValidateIgnoreTrust(elmSignature, new CertificateFederation(certificate))) {
351 throw new ModelException("The signature does not validate with the supplied certificate. "
352 + "Either the signature or the certificate is wrong.");
353 }
354 }
355
356 private static void addKeyInfo(Element signatureParentElement, SignatureConfiguration configuration, X509Certificate certificate) {
357 Document doc = signatureParentElement.getOwnerDocument();
358
359 List<Element> signatureElements = XmlUtil.getChildElementsNS(signatureParentElement, NameSpaces.DSIG_SCHEMA, DSTags.SIGNATURE);
360 if (signatureElements.size() != 1) {
361 throw new ModelException("Expected 1 but found " + signatureElements.size() + " Signature elements");
362 }
363 Element elmSignature = signatureElements.get(0);
364
365
366 Element elmKeyInfo = doc.createElementNS(NameSpaces.DSIG_SCHEMA, DSTags.KEY_INFO_PREFIXED);
367 elmSignature.appendChild(elmKeyInfo);
368
369 if (configuration.getKeyInfoId() != null) {
370 elmKeyInfo.setAttribute(IDValues.Id, configuration.getKeyInfoId());
371 }
372
373 if (configuration.isAddCertificateAsReference()) {
374 Element elmKeyName = doc.createElementNS(NameSpaces.DSIG_SCHEMA, DSTags.KEY_NAME_PREFIXED);
375 elmKeyInfo.appendChild(elmKeyName);
376 elmKeyName.setTextContent(new FederationCertificateReference(certificate).toString());
377 } else {
378 Element elmX509Data = doc.createElementNS(NameSpaces.DSIG_SCHEMA, DSTags.X509DATA_PREFIXED);
379 elmKeyInfo.appendChild(elmX509Data);
380
381 Element elmX509Certificate = doc.createElementNS(NameSpaces.DSIG_SCHEMA, DSTags.X509CERTIFICATE_PREFIXED);
382 elmX509Data.appendChild(elmX509Certificate);
383
384 String encodedCert;
385 try {
386 encodedCert = XmlUtil.toBase64(certificate.getEncoded());
387 } catch (CertificateEncodingException e) {
388 throw new ModelException("Unable to encode certificate", e);
389 }
390 elmX509Certificate.appendChild(doc.createTextNode(encodedCert));
391 }
392 }
393
394 public static void validateAllSignatures(Message message, NodeList signatures, Federation federation, CredentialVault credentialVault,
395 boolean checkTrust) throws SignatureInvalidModelBuildException {
396
397 for (int i = 0; i < signatures.getLength(); i++) {
398 if (!SignatureUtil.validate(signatures.item(i), federation, credentialVault, checkTrust)) {
399
400 Properties props = (federation == null) ? System.getProperties() : federation.getProperties();
401 SOSIFactory.getAuditEventHandler(props).onInformationalAuditingEvent(
402 AuditEventHandler.EVENT_TYPE_ERROR_VALIDATING_SOSI_MESSAGE,
403 new Object[]{message}
404 );
405 throw new SignatureInvalidModelBuildException("Signature could not be validated", message.getMessageID(), message.getFlowID(), message.getDGWSVersion());
406 }
407 }
408 }
409
410
411
412
413
414
415
416
417
418
419
420 public static String getCryptoProvider(Properties properties, String key) {
421 String cryptoProvider = SOSIFactory.PROPERTYVALUE_SOSI_CRYPTOPROVIDER_BOUNCYCASTLE;
422 if (properties != null && properties.containsKey(key)){
423 cryptoProvider = properties.getProperty(key);
424 }
425 if(cryptoProvider.equals(SOSIFactory.PROPERTYVALUE_SOSI_CRYPTOPROVIDER_BOUNCYCASTLE)
426 && Security.getProvider(SOSIFactory.PROPERTYVALUE_SOSI_CRYPTOPROVIDER_BOUNCYCASTLE) == null) {
427
428 try {
429 Provider provider = (Provider) Class.forName("org.bouncycastle.jce.provider.BouncyCastleProvider").newInstance();
430 Security.addProvider(provider);
431 } catch (InstantiationException e) {
432 throw new ModelException("Provider could not be set", e);
433 } catch (IllegalAccessException e) {
434 throw new ModelException("Provider could not be set", e);
435 } catch (ClassNotFoundException e) {
436 throw new ModelException("Provider could not be set", e);
437 }
438 }
439 return cryptoProvider;
440 }
441
442
443
444
445
446
447
448 public static Properties setupCryptoProviderForJVM() {
449 Properties properties = new Properties();
450 if("IBM Corporation".equals(System.getProperty("java.vm.vendor"))) {
451 properties.put(SOSIFactory.PROPERTYNAME_SOSI_CRYPTOPROVIDER_PKCS12, "BC");
452 properties.put(SOSIFactory.PROPERTYNAME_SOSI_CRYPTOPROVIDER_X509, "BC");
453 properties.put(SOSIFactory.PROPERTYNAME_SOSI_CRYPTOPROVIDER_RSA, "IBMJCE");
454 properties.put(SOSIFactory.PROPERTYNAME_SOSI_CRYPTOPROVIDER_SHA1WITHRSA, "IBMJCE");
455 } else {
456 if ("1.4".equals(System.getProperty("java.specification.version"))) {
457 properties.put(SOSIFactory.PROPERTYNAME_SOSI_CRYPTOPROVIDER_PKCS12, "BC");
458 } else {
459 properties.put(SOSIFactory.PROPERTYNAME_SOSI_CRYPTOPROVIDER_PKCS12, "SunJSSE");
460 }
461 if ("1.6".equals(System.getProperty("java.specification.version"))) {
462 properties.put(SOSIFactory.PROPERTYNAME_SOSI_CRYPTOPROVIDER_X509, "BC");
463 } else if ("1.4".equals(System.getProperty("java.specification.version"))) {
464 properties.put(SOSIFactory.PROPERTYNAME_SOSI_CRYPTOPROVIDER_X509, "BC");
465 } else {
466 properties.put(SOSIFactory.PROPERTYNAME_SOSI_CRYPTOPROVIDER_X509, "SUN");
467 }
468 properties.put(SOSIFactory.PROPERTYNAME_SOSI_CRYPTOPROVIDER_RSA, "SunRsaSign");
469 properties.put(SOSIFactory.PROPERTYNAME_SOSI_CRYPTOPROVIDER_SHA1WITHRSA, "SunRsaSign");
470 }
471 return properties;
472 }
473
474
475
476
477
478
479 public static String getC14NString(Element domElement) throws CanonicalizationException, InvalidCanonicalizerException, XMLSecurityException {
480 ByteArrayOutputStream baos = new ByteArrayOutputStream();
481 Canonicalizer c14nizer = Canonicalizer.getInstance("http://www.w3.org/2001/10/xml-exc-c14n#");
482 c14nizer.setWriter(new BufferedOutputStream(baos));
483 c14nizer.canonicalizeSubtree(domElement);
484 return baos.toString();
485 }
486
487
488
489
490
491
492
493
494
495
496
497
498 private static XMLSignature initXmlSignature(Document doc, SignatureConfiguration config) {
499 String baseURI = doc.getDocumentElement().getNamespaceURI();
500
501 XMLSignature xmlSignature;
502 try {
503 xmlSignature = new XMLSignature(doc, baseURI, XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1, Canonicalizer.ALGO_ID_C14N_EXCL_OMIT_COMMENTS);
504 } catch (XMLSecurityException e) {
505 throw new ModelException("Unable to get XMLSignature object", e);
506 }
507
508 SignatureConfiguration.Reference[] references = config.getReferences();
509 for (int i = 0; i < references.length; i++) {
510 processReference(doc, xmlSignature, references[i]);
511 }
512
513 Element xmlSigElement = xmlSignature.getElement();
514 if (config.getIdAttributeName() != null) {
515 xmlSigElement.setAttribute(config.getIdAttributeName(), IDValues.OCES_SIGNATURE);
516 }
517
518 Element signatureParentLocation = (Element) XmlUtil.getElementByIdExtended(doc, config.getSignatureParentID());
519
520
521
522 if (signatureParentLocation == null) {
523 signatureParentLocation = doc.getDocumentElement();
524 }
525
526
527 if (config.getSignatureSiblingNode() != null) {
528 signatureParentLocation.insertBefore(xmlSigElement, config.getSignatureSiblingNode());
529 } else {
530 signatureParentLocation.appendChild(xmlSigElement);
531 }
532
533 return xmlSignature;
534 }
535
536 private static void processReference(Document doc, XMLSignature xmlSignature, SignatureConfiguration.Reference reference) {
537 String referenceURI = reference.getURI();
538 Transforms transforms = new Transforms(doc);
539 try {
540 switch (reference.getType()) {
541 case DIRECT_REFERENCE:
542 transforms.addTransform(Transforms.TRANSFORM_ENVELOPED_SIGNATURE);
543 transforms.addTransform(Transforms.TRANSFORM_C14N_EXCL_OMIT_COMMENTS);
544 break;
545 case SECURITY_TOKEN_REFERENCE:
546 transforms.addTransform(STRTransform.implementedTransformURI, createSTRParameters(doc));
547 break;
548 }
549 } catch (TransformationException e) {
550 throw new ModelException("Unable to add c14n omit comments gctransform to reference " + referenceURI, e);
551 }
552
553 try {
554 xmlSignature.addDocument("#" + referenceURI, transforms);
555 } catch (XMLSignatureException e) {
556 throw new ModelException("Unable to add transforms for reference " + referenceURI, e);
557 }
558 }
559
560 private static Element createSTRParameters(Document doc) {
561 Element parameters = doc.createElementNS(NameSpaces.WSSE_SCHEMA, WSSETags.TRANSFORMATION_PARAMETERS_PREFIXED);
562 Element canonicalization = doc.createElementNS(NameSpaces.DSIG_SCHEMA, DSTags.CANONICALIZATION_METHOD_PREFIXED);
563
564 canonicalization.setAttribute(DSAttributes.ALGORITHM, Canonicalizer.ALGO_ID_C14N_EXCL_OMIT_COMMENTS);
565 parameters.appendChild(canonicalization);
566 return parameters;
567 }
568
569
570
571
572
573
574
575 private static void tidyXML(Element element) {
576
577 NamedNodeMap attributes = element.getAttributes();
578 for (int i = 0; i < attributes.getLength(); i++) {
579 Attr item = (Attr) attributes.item(i);
580 if (item.getNodeName().startsWith("xmlns:"))
581 element.removeAttributeNode(item);
582 }
583
584 NodeList children = element.getChildNodes();
585 List<Text> lfElements = new LinkedList<Text>();
586 for (int i = 0; i < children.getLength(); i++) {
587 Node item = children.item(i);
588 if (item.getNodeType() == Node.ELEMENT_NODE) {
589 tidyXML((Element) item);
590 } else if (item.getNodeType() == Node.TEXT_NODE) {
591
592 Text textElement = ((Text) item);
593 String value = ((Text) item).getData();
594 if (value.indexOf('\n') >= 0) {
595 if (value.length() == 1) {
596
597 lfElements.add(textElement);
598 } else {
599
600 textElement.setData(value.replaceAll("\n", ""));
601 }
602 }
603 }
604 }
605
606
607 for (Iterator<Text> iter = lfElements.iterator(); iter.hasNext();) {
608 element.removeChild(iter.next());
609 }
610 }
611
612 private static boolean internalValidateIgnoreTrust(Node signatureToValidate, Federation federation) {
613 return internalValidate(signatureToValidate, federation, null, false);
614 }
615
616 private static boolean internalValidate(Node signatureToValidate, Federation federation, CredentialVault vault, boolean checkForTrustedCertificates) {
617 WSDocInfo wsDocInfo = new WSDocInfo(signatureToValidate.getOwnerDocument());
618 WSDocInfoStore.store(wsDocInfo);
619 try {
620
621 String baseUri = signatureToValidate.getOwnerDocument().getDocumentElement().getNamespaceURI();
622
623 if (baseUri == null) {
624 baseUri = "";
625 }
626
627 if (signatureToValidate.getNodeType() != Node.ELEMENT_NODE) {
628 throw new ModelException("The signature to validate must be a ds:Signature Element!");
629 }
630
631 XMLSignature xmlSignature;
632 try {
633 xmlSignature = new XMLSignature((Element) signatureToValidate, baseUri);
634 } catch (XMLSecurityException e) {
635 throw new ModelException("Unable to get XMLSignature element", e);
636 }
637
638 X509Certificate cert = resolveCertificate(xmlSignature, federation);
639
640
641
642
643 if (checkForTrustedCertificates) {
644 boolean trusted = false;
645 if (federation != null) {
646 trusted = federation.isValidSTSCertificate(cert);
647 } else if (vault != null) {
648 trusted = vault.isTrustedCertificate(cert);
649 }
650 if (!trusted) {
651 throw new ModelException("The certificate that signed the security token is not trusted!");
652 }
653
654 }
655
656
657
658 XmlUtil.ensureEnvelopedIds((Element) signatureToValidate);
659
660 try {
661 return xmlSignature.checkSignatureValue(cert);
662 } catch (XMLSignatureException e) {
663 throw new ModelException("Unable to validate the xmlSignature", e);
664 }
665 } finally {
666 WSDocInfoStore.delete(wsDocInfo);
667 }
668 }
669
670 private static X509Certificate resolveCertificate(XMLSignature xmlSignature, Federation federation) {
671 KeyInfo keyInfo = xmlSignature.getKeyInfo();
672 if (keyInfo.containsKeyName()) {
673 String keyName = resolveSingleKeyName(federation, keyInfo);
674 FederationCertificateReference federationCertificateReference = new FederationCertificateReference(keyName);
675 return federation.getFederationCertificate(federationCertificateReference);
676 } else {
677 try {
678 return keyInfo.getX509Certificate();
679 } catch (KeyResolverException e) {
680 throw new ModelException("Unable to get certificate from dom", e);
681 }
682 }
683 }
684
685 private static String resolveSingleKeyName(Federation federation, KeyInfo keyInfo) {
686 int keyNameCount = keyInfo.lengthKeyName();
687 if (keyNameCount > 1) {
688 throw new ModelException("Unable to handle more than one keyname");
689 }
690 if (keyNameCount > 0 && federation == null) {
691 throw new ModelException("Will need federation to lookup certificate by keyName");
692 }
693 try {
694 return keyInfo.itemKeyName(0).getKeyName();
695 } catch (XMLSecurityException e) {
696 throw new ModelException("Unable to lookup keyName", e);
697 }
698 }
699
700 }