001// Copyright (C) 2002 IAIK 002// https://sic.tech/ 003// 004// Copyright (C) 2003 - 2025 Stiftung Secure Information and 005// Communication Technologies SIC 006// https://sic.tech/ 007// 008// All rights reserved. 009// 010// This source is provided for inspection purposes and recompilation only, 011// unless specified differently in a contract with IAIK. This source has to 012// be kept in strict confidence and must not be disclosed to any third party 013// under any circumstances. Redistribution in source and binary forms, with 014// or without modification, are <not> permitted in any case! 015// 016// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 017// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 018// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 019// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 020// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 021// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 022// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 023// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 024// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 025// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 026// SUCH DAMAGE. 027// 028// $Header: /IAIK-CMS/current/src/demo/cms/signedData/PssSignedDataDemo.java 21 12.02.25 17:58 Dbratko $ 029// $Revision: 21 $ 030// 031 032package demo.cms.signedData; 033 034import java.io.ByteArrayInputStream; 035import java.io.ByteArrayOutputStream; 036import java.io.IOException; 037import java.io.InputStream; 038import java.security.AlgorithmParameters; 039import java.security.InvalidAlgorithmParameterException; 040import java.security.MessageDigest; 041import java.security.NoSuchAlgorithmException; 042import java.security.PrivateKey; 043import java.security.SignatureException; 044import java.security.cert.Certificate; 045import java.security.spec.InvalidParameterSpecException; 046 047import demo.DemoUtil; 048import demo.keystore.CMSKeyStore; 049import iaik.asn1.ObjectID; 050import iaik.asn1.structures.AlgorithmID; 051import iaik.asn1.structures.Attribute; 052import iaik.cms.CMSException; 053import iaik.cms.IssuerAndSerialNumber; 054import iaik.cms.SecurityProvider; 055import iaik.cms.SignedData; 056import iaik.cms.SignedDataStream; 057import iaik.cms.SignerInfo; 058import iaik.cms.SubjectKeyID; 059import iaik.cms.attributes.CMSContentType; 060import iaik.cms.attributes.SigningTime; 061import iaik.pkcs.pkcs1.MGF1ParameterSpec; 062import iaik.pkcs.pkcs1.MaskGenerationAlgorithm; 063import iaik.pkcs.pkcs1.RSAPssParameterSpec; 064import iaik.utils.Util; 065import iaik.x509.X509Certificate; 066import iaik.x509.X509ExtensionException; 067 068/** 069 * This class demonstrates the CMS SignedData implementation for 070 * the RSA-PSS (PKCS#1v2.1) algorithm. 071 */ 072public class PssSignedDataDemo { 073 074 // certificate of user 1 075 X509Certificate user1; 076 // private key of user 1 077 PrivateKey user1_pk; 078 // certificate of user 2 079 X509Certificate user2; 080 // private key of user 2 081 PrivateKey user2_pk; 082 083 // a certificate chain containing the user certs + CA 084 Certificate[] certificates; 085 Certificate[] certs; 086 Certificate[] user1Certs; 087 088 // just for attribute certificate testing 089 PrivateKey issuer1_pk; 090 091 /** 092 * Setups the demo certificate chains. 093 * 094 * Keys and certificate are retrieved from the demo KeyStore. 095 * 096 * @throws IOException if an file read error occurs 097 */ 098 public PssSignedDataDemo() throws IOException { 099 100 System.out.println(); 101 System.out.println("**********************************************************************************"); 102 System.out.println("* PssSignedDataDemo *"); 103 System.out.println("* (shows the usage of the CMS SignedData type with the RSA PSS signature scheme) *"); 104 System.out.println("**********************************************************************************"); 105 System.out.println(); 106 107 // add all certificates to the list 108 user1Certs = CMSKeyStore.getCertificateChain(CMSKeyStore.RSA, CMSKeyStore.SZ_2048_SIGN_1); 109 user1 = (X509Certificate)user1Certs[0]; 110 user1_pk = CMSKeyStore.getPrivateKey(CMSKeyStore.RSA, CMSKeyStore.SZ_2048_SIGN_1); 111 user2 = CMSKeyStore.getCertificateChain(CMSKeyStore.RSA, CMSKeyStore.SZ_2048_SIGN_1)[0]; 112 user2_pk = CMSKeyStore.getPrivateKey(CMSKeyStore.RSA, CMSKeyStore.SZ_2048_SIGN_1); 113 114 certs = user1Certs; 115 certificates = new Certificate[certs.length+1]; 116 System.arraycopy(certs, 0, certificates, 0, certs.length); 117 certificates[certs.length] = user2; 118 } 119 120 /** 121 * Creates a CMS <code>SignedData</code> object. 122 * <p> 123 * 124 * @param message the message to be signed, as byte representation 125 * @param mode the transmission mode, either IMPLICIT or EXPLICIT 126 * @return the BER encoding of the <code>SignedData</code> object just created 127 * @throws CMSException if the <code>SignedData</code> object cannot 128 * be created 129 * @throws IOException if some stream I/O error occurs 130 */ 131 public byte[] createSignedDataStream(byte[] message, int mode) throws CMSException, IOException { 132 133 System.out.print("Create a new message signed by user 1 :"); 134 135 // we are testing the stream interface 136 ByteArrayInputStream is = new ByteArrayInputStream(message); 137 // create a new SignedData object which includes the data 138 SignedDataStream signed_data = new SignedDataStream(is, mode); 139 140 // SignedData shall include the certificate chain for verifying 141 signed_data.setCertificates(certificates); 142 143 // cert at index 0 is the user certificate 144 IssuerAndSerialNumber issuer = new IssuerAndSerialNumber(user1); 145 146 // create a new SignerInfo for RSA-PSS with default parameters 147 SignerInfo signer_info = new SignerInfo(issuer, 148 (AlgorithmID)AlgorithmID.sha256.clone(), 149 (AlgorithmID)AlgorithmID.rsassaPss.clone(), 150 user1_pk); 151 // create some signed attributes 152 // the message digest attribute is automatically added 153 Attribute[] attributes = new Attribute[2]; 154 try { 155 // content type is data 156 CMSContentType contentType = new CMSContentType(ObjectID.cms_data); 157 attributes[0] = new Attribute(contentType); 158 // signing time is now 159 SigningTime signingTime = new SigningTime(); 160 attributes[1] = new Attribute(signingTime); 161 } catch (Exception ex) { 162 throw new CMSException("Error creating attribute: " + ex.toString()); 163 } 164 165 // set the attributes 166 signer_info.setSignedAttributes(attributes); 167 // finish the creation of SignerInfo by calling method addSigner 168 try { 169 signed_data.addSignerInfo(signer_info); 170 // another SignerInfo without signed attributes and RSA-PSS with user defined parameters 171 AlgorithmID hashID = (AlgorithmID)AlgorithmID.sha256.clone(); 172 AlgorithmID mgfID = (AlgorithmID)AlgorithmID.mgf1.clone(); 173 int saltLength = 32; 174 AlgorithmID rsaPssID = null; 175 try { 176 rsaPssID = createPssAlgorithmID(hashID, mgfID, saltLength); 177 } catch (Exception ex) { 178 throw new CMSException("Error creating PSS parameters: " + ex.toString()); 179 } 180 signer_info = new SignerInfo(new SubjectKeyID(user2), 181 hashID, 182 rsaPssID, 183 user2_pk); 184 185 // the message digest itself is protected 186 signed_data.addSignerInfo(signer_info); 187 188 } catch (NoSuchAlgorithmException ex) { 189 throw new CMSException("No implementation for signature algorithm: "+ex.getMessage()); 190 } catch (X509ExtensionException ex) { 191 throw new CMSException("Cannot create SubjectKeyID for user2 : " + ex.getMessage()); 192 } 193 194 // write the data through SignedData to any out-of-band place 195 if (mode == SignedDataStream.EXPLICIT) { 196 InputStream data_is = signed_data.getInputStream(); 197 byte[] buf = new byte[1024]; 198 int r; 199 while ((r = data_is.read(buf)) > 0) { 200 ; // skip data 201 } 202 } 203 204 // return the SignedData as DER encoded byte array with block size 2048 205 ByteArrayOutputStream os = new ByteArrayOutputStream(); 206 signed_data.writeTo(os, 2048); 207 return os.toByteArray(); 208 } 209 210 211 /** 212 * Parses a CMS <code>SignedData</code> object and verifies the signatures 213 * for all participated signers. 214 * 215 * @param signedData <code>SignedData</code> object as BER encoded byte array 216 * @param message the message which was transmitted out-of-band (if explicit signed), 217 * otherwise <code>null</code> (implicit signed) 218 * 219 * @return the inherent message as byte array 220 * @throws CMSException if any signature does not verify 221 * @throws IOException if some stream I/O error occurs 222 */ 223 public byte[] getSignedDataStream(byte[] signedData, byte[] message) throws CMSException, IOException { 224 225 // we are testing the stream interface 226 ByteArrayInputStream is = new ByteArrayInputStream(signedData); 227 // create the SignedData object 228 SignedDataStream signed_data = new SignedDataStream(is); 229 230 if (signed_data.getMode() == SignedDataStream.EXPLICIT) { 231 // explicitly signed; set the data stream for digesting the message 232 signed_data.setInputStream(new ByteArrayInputStream(message)); 233 } 234 235 // get an InputStream for reading the signed content 236 InputStream data = signed_data.getInputStream(); 237 ByteArrayOutputStream os = new ByteArrayOutputStream(); 238 Util.copyStream(data, os, null); 239 240 System.out.println("SignedData contains the following signer information:"); 241 SignerInfo[] signer_infos = signed_data.getSignerInfos(); 242 243 int numberOfSignerInfos = signer_infos.length; 244 if (numberOfSignerInfos == 0) { 245 String warning = "Warning: Unsigned message (no SignerInfo included)!"; 246 System.err.println(warning); 247 throw new CMSException(warning); 248 } else { 249 for (int i = 0; i < numberOfSignerInfos; i++) { 250 251 try { 252 // verify the signed data using the SignerInfo at index i 253 X509Certificate signer_cert = signed_data.verify(i); 254 // if the signature is OK the certificate of the signer is returned 255 System.out.println("Signature OK from signer: "+signer_cert.getSubjectDN()); 256 // get signed attributes 257 SigningTime signingTime = (SigningTime)signer_infos[i].getSignedAttributeValue(ObjectID.signingTime); 258 if (signingTime != null) { 259 System.out.println("This message has been signed at " + signingTime.get()); 260 } 261 CMSContentType contentType = (CMSContentType)signer_infos[i].getSignedAttributeValue(ObjectID.contentType); 262 if (contentType != null) { 263 System.out.println("The content has CMS content type " + contentType.get().getName()); 264 } 265 } catch (SignatureException ex) { 266 // if the signature is not OK a SignatureException is thrown 267 System.out.println("Signature ERROR from signer: "+signed_data.getCertificate(signer_infos[i].getSignerIdentifier()).getSubjectDN()); 268 throw new CMSException(ex.toString()); 269 } 270 } 271 272 // now check alternative signature verification 273 System.out.println("Now check the signature assuming that no certs have been included:"); 274 try { 275 SignerInfo signer_info = signed_data.verify(user1); 276 // if the signature is OK the certificate of the signer is returned 277 System.out.println("Signature OK from signer: "+user1.getSubjectDN()); 278 279 } catch (SignatureException ex) { 280 // if the signature is not OK a SignatureException is thrown 281 System.out.println("Signature ERROR from signer: "+user1.getSubjectDN()); 282 throw new CMSException(ex.toString()); 283 } 284 285 286 try { 287 SignerInfo signer_info = signed_data.verify(user2); 288 // if the signature is OK the certificate of the signer is returned 289 System.out.println("Signature OK from signer: "+signed_data.getCertificate(signer_info.getSignerIdentifier()).getSubjectDN()); 290 291 } catch (SignatureException ex) { 292 // if the signature is not OK a SignatureException is thrown 293 System.out.println("Signature ERROR from signer: "+user2.getSubjectDN()); 294 throw new CMSException(ex.toString()); 295 } 296 // in practice we also would validate the signer certificate(s) 297 } 298 299 return os.toByteArray(); 300 } 301 302 303 304 /** 305 * Creates a CMS <code>SignedData</code> object. 306 * <p> 307 * 308 * @param message the message to be signed, as byte representation 309 * @param mode the mode, either SignedData.IMPLICIT or SignedData.EXPLICIT 310 * @return the DER encoded <code>SignedData</code> object 311 * @throws CMSException if the <code>SignedData</code> object cannot 312 * be created 313 */ 314 public byte[] createSignedData(byte[] message, int mode) throws CMSException { 315 316 System.out.println("Create a new message signed by user 1 :"); 317 318 // create a new SignedData object which includes the data 319 SignedData signed_data = new SignedData(message, mode); 320 321 // SignedData shall include the certificate chain for verifying 322 signed_data.setCertificates(certificates); 323 324 // cert at index 0 is the user certificate 325 IssuerAndSerialNumber issuer = new IssuerAndSerialNumber(user1); 326 327 // create a new SignerInfo for RSA-PSS with default parameters 328 SignerInfo signer_info = new SignerInfo(issuer, 329 (AlgorithmID)AlgorithmID.sha256.clone(), 330 (AlgorithmID)AlgorithmID.rsassaPss.clone(), 331 user1_pk); 332 333 // create some signed attributes 334 // the message digest attribute is automatically added 335 Attribute[] attributes = new Attribute[2]; 336 try { 337 // content type is data 338 CMSContentType contentType = new CMSContentType(ObjectID.cms_data); 339 attributes[0] = new Attribute(contentType); 340 // signing time is now 341 SigningTime signingTime = new SigningTime(); 342 attributes[1] = new Attribute(signingTime); 343 } catch (Exception ex) { 344 throw new CMSException("Error creating attribute: " + ex.toString()); 345 } 346 347 // set the attributes 348 signer_info.setSignedAttributes(attributes); 349 // finish the creation of SignerInfo by calling method addSigner 350 try { 351 signed_data.addSignerInfo(signer_info); 352 353 // another SignerInfo without signed attributes and RSA-PSS with user defined parameters 354 AlgorithmID hashID = (AlgorithmID)AlgorithmID.sha256.clone(); 355 AlgorithmID mgfID = (AlgorithmID)AlgorithmID.mgf1.clone(); 356 int saltLength = 32; 357 AlgorithmID rsaPssID = null; 358 try { 359 rsaPssID = createPssAlgorithmID(hashID, mgfID, saltLength); 360 } catch (Exception ex) { 361 throw new CMSException("Error creating PSS parameters: " + ex.toString()); 362 } 363 signer_info = new SignerInfo(new SubjectKeyID(user2), 364 hashID, 365 rsaPssID, 366 user2_pk); 367 368 signed_data.addSignerInfo(signer_info); 369 370 } catch (NoSuchAlgorithmException ex) { 371 throw new CMSException("No implementation for signature algorithm: "+ex.getMessage()); 372 } catch (X509ExtensionException ex) { 373 throw new CMSException("Cannot create SubjectKeyID for user2 : " + ex.getMessage()); 374 } 375 return signed_data.getEncoded(); 376 } 377 378 379 /** 380 * Parses a CMS <code>SignedData</code> object and verifies the signatures 381 * for all participated signers. 382 * 383 * @param encoding the DER encoded <code>SignedData</code> object 384 * @param message the message which was transmitted out-of-band (if explicit signed), 385 * otherwise <code>null</code> (implicit signed) 386 * 387 * @return the inherent message as byte array 388 * @throws CMSException if any signature does not verify 389 * @throws IOException if some stream I/O error occurs 390 */ 391 public byte[] getSignedData(byte[] encoding, byte[] message) throws CMSException, IOException { 392 393 // create the SignedData object 394 SignedData signed_data = new SignedData(new ByteArrayInputStream(encoding)); 395 if (signed_data.getMode() == SignedData.EXPLICIT) { 396 // explicitly signed; set the data stream for digesting the message 397 signed_data.setContent(message); 398 } 399 400 System.out.println("SignedData contains the following signer information:"); 401 SignerInfo[] signer_infos = signed_data.getSignerInfos(); 402 403 int numberOfSignerInfos = signer_infos.length; 404 if (numberOfSignerInfos == 0) { 405 String warning = "Warning: Unsigned message (no SignerInfo included)!"; 406 System.err.println(warning); 407 throw new CMSException(warning); 408 } else { 409 for (int i = 0; i < numberOfSignerInfos; i++) { 410 try { 411 // verify the signed data using the SignerInfo at index i 412 X509Certificate signer_cert = signed_data.verify(i); 413 // if the signature is OK the certificate of the signer is returned 414 System.out.println("Signature OK from signer: "+signer_cert.getSubjectDN()); 415 // get signed attributes 416 SigningTime signingTime = (SigningTime)signer_infos[i].getSignedAttributeValue(ObjectID.signingTime); 417 if (signingTime != null) { 418 System.out.println("This message has been signed at " + signingTime.get()); 419 } 420 CMSContentType contentType = (CMSContentType)signer_infos[i].getSignedAttributeValue(ObjectID.contentType); 421 if (contentType != null) { 422 System.out.println("The content has CMS content type " + contentType.get().getName()); 423 } 424 } catch (SignatureException ex) { 425 // if the signature is not OK a SignatureException is thrown 426 System.out.println("Signature ERROR from signer: "+signed_data.getCertificate(signer_infos[i].getSignerIdentifier()).getSubjectDN()); 427 throw new CMSException(ex.toString()); 428 } 429 } 430 431 // now check alternative signature verification 432 System.out.println("Now check the signature assuming that no certs have been included:"); 433 try { 434 SignerInfo signer_info = signed_data.verify(user1); 435 // if the signature is OK the certificate of the signer is returned 436 System.out.println("Signature OK from signer: "+signed_data.getCertificate(signer_info.getSignerIdentifier()).getSubjectDN()); 437 438 } catch (SignatureException ex) { 439 // if the signature is not OK a SignatureException is thrown 440 System.out.println("Signature ERROR from signer: "+user1.getSubjectDN()); 441 throw new CMSException(ex.toString()); 442 } 443 try { 444 SignerInfo signer_info = signed_data.verify(user2); 445 // if the signature is OK the certificate of the signer is returned 446 System.out.println("Signature OK from signer: "+signed_data.getCertificate(signer_info.getSignerIdentifier()).getSubjectDN()); 447 448 } catch (SignatureException ex) { 449 // if the signature is not OK a SignatureException is thrown 450 System.out.println("Signature ERROR from signer: "+user2.getSubjectDN()); 451 throw new CMSException(ex.toString()); 452 } 453 // in practice we also would validate the signer certificate(s) 454 } 455 return signed_data.getContent(); 456 } 457 458 /** 459 * Creates an RSA-PSS AlgorithmID with the supplied parameters (hash algorithm id, 460 * mask generation function, salt length). 461 * 462 * @param hashID the hash algorithm to be used 463 * @param mgfID the mask generation function to be used 464 * @param saltLength the salt length to be used 465 * 466 * @return the RSA-PSS algorithm id with the given parameters 467 * 468 * @throws InvalidAlgorithmParameterException if the parameters cannot be created/set 469 * @throws NoSuchAlgorithmException if there is no AlgorithmParameters implementation 470 * for RSA-PSS 471 */ 472 public static AlgorithmID createPssAlgorithmID(AlgorithmID hashID, AlgorithmID mgfID, int saltLength) 473 throws InvalidAlgorithmParameterException, NoSuchAlgorithmException { 474 475 SecurityProvider provider = SecurityProvider.getSecurityProvider(); 476 AlgorithmID rsaPssID = (AlgorithmID)AlgorithmID.rsassaPss.clone(); 477 mgfID.setParameter(hashID.toASN1Object()); 478 // create a RSAPssParameterSpec 479 RSAPssParameterSpec pssParamSpec = new RSAPssParameterSpec(hashID, mgfID, saltLength); 480 // optionally set hash and mgf engines 481 MessageDigest hashEngine = provider.getMessageDigest(hashID); 482 pssParamSpec.setHashEngine(hashEngine); 483 MaskGenerationAlgorithm mgfEngine = provider.getMaskGenerationAlgorithm(mgfID); 484 MGF1ParameterSpec mgf1Spec = new MGF1ParameterSpec(hashID); 485 mgf1Spec.setHashEngine(hashEngine); 486 mgfEngine.setParameters(mgf1Spec); 487 pssParamSpec.setMGFEngine(mgfEngine); 488 489 AlgorithmParameters pssParams = null; 490 try { 491 pssParams = provider.getAlgorithmParameters(SecurityProvider.IMPLEMENTATION_NAME_RSA_PSS); 492 pssParams.init(pssParamSpec); 493 } catch (InvalidParameterSpecException ex) { 494 throw new InvalidAlgorithmParameterException("Cannot init PSS params: " + ex.getMessage()); 495 } 496 497 rsaPssID.setAlgorithmParameters(pssParams); 498 return rsaPssID; 499 } 500 501 /** 502 * Tests the CMS SignedData implementation for 503 * the RSA-PSS (PKCS#1v2.1) algorithm. 504 */ 505 public void start() { 506 // the test message 507 String m = "This is the test message."; 508 System.out.println("Test message: \""+m+"\""); 509 System.out.println(); 510 byte[] message = m.getBytes(); 511 512 try { 513 byte[] encoding; 514 byte[] received_message = null; 515 System.out.println("Stream implementation demos (RSA-PPS signing)"); 516 System.out.println("============================================="); 517 // 518 // test CMS Implicit SignedDataStream 519 // 520 System.out.println("\nImplicit SignedDataStream demo [create]:\n"); 521 encoding = createSignedDataStream(message, SignedDataStream.IMPLICIT); 522 // transmit data 523 System.out.println("\nImplicit SignedDataStream demo [parse]:\n"); 524 received_message = getSignedDataStream(encoding, null); 525 System.out.print("\nSigned content: "); 526 System.out.println(new String(received_message)); 527 528 // 529 // test CMS Explicit SignedDataStream 530 // 531 System.out.println("\nExplicit SignedDataStream demo [create]:\n"); 532 encoding = createSignedDataStream(message, SignedDataStream.EXPLICIT); 533 // transmit data 534 System.out.println("\nExplicit SignedDataStream demo [parse]:\n"); 535 received_message = getSignedDataStream(encoding, message); 536 System.out.print("\nSigned content: "); 537 System.out.println(new String(received_message)); 538 539 // the non-stream implementation 540 System.out.println("\nNon-stream implementation demos (RSA-PPS signing)"); 541 System.out.println("==================================================="); 542 543 // 544 // test CMS Implicit SignedData 545 // 546 System.out.println("\nImplicit CMS SignedData demo [create]:\n"); 547 encoding = createSignedData(message, SignedData.IMPLICIT); 548 // transmit data 549 System.out.println("\nImplicit CMS SignedData demo [parse]:\n"); 550 received_message = getSignedData(encoding, null); 551 System.out.print("\nSigned content: "); 552 System.out.println(new String(received_message)); 553 554 // 555 // test CMS Explicit SignedData 556 // 557 System.out.println("\nExplicit CMS SignedData demo [create]:\n"); 558 encoding = createSignedData(message, SignedData.EXPLICIT); 559 // transmit data 560 System.out.println("\nExplicit CMS SignedData demo [parse]:\n"); 561 received_message = getSignedData(encoding, message); 562 System.out.print("\nSigned content: "); 563 System.out.println(new String(received_message)); 564 565 } catch (Exception ex) { 566 ex.printStackTrace(); 567 throw new RuntimeException(ex.toString()); 568 } 569 } 570 571 /** 572 * The main method. 573 */ 574 public static void main(String argv[]) throws Exception { 575 576 DemoUtil.initDemos(); 577 (new PssSignedDataDemo()).start(); 578 System.out.println("\nReady!"); 579 DemoUtil.waitKey(); 580 } 581}