View Javadoc

1   /**
2    * LICENCIA LGPL:
3    * 
4    * Esta librería es Software Libre; Usted puede redistribuirla y/o modificarla
5    * bajo los términos de la GNU Lesser General Public License (LGPL) tal y como 
6    * ha sido publicada por la Free Software Foundation; o bien la versión 2.1 de 
7    * la Licencia, o (a su elección) cualquier versión posterior.
8    * 
9    * Esta librería se distribuye con la esperanza de que sea útil, pero SIN 
10   * NINGUNA GARANTÍA; tampoco las implícitas garantías de MERCANTILIDAD o 
11   * ADECUACIÓN A UN PROPÓSITO PARTICULAR. Consulte la GNU Lesser General Public 
12   * License (LGPL) para más detalles
13   * 
14   * Usted debe recibir una copia de la GNU Lesser General Public License (LGPL) 
15   * junto con esta librería; si no es así, escriba a la Free Software Foundation 
16   * Inc. 51 Franklin Street, 5º Piso, Boston, MA 02110-1301, USA o consulte
17   * <http://www.gnu.org/licenses/>.
18   *
19   * Copyright 2011 Agencia de Tecnología y Certificación Electrónica
20   */
21  package es.accv.arangi.base.certificate.validation;
22  
23  import java.io.File;
24  import java.io.IOException;
25  import java.security.KeyStore;
26  import java.security.KeyStoreException;
27  import java.security.NoSuchAlgorithmException;
28  import java.security.cert.CertificateException;
29  import java.security.cert.X509Certificate;
30  import java.util.ArrayList;
31  import java.util.HashMap;
32  import java.util.Iterator;
33  import java.util.List;
34  import java.util.Map;
35  
36  import org.apache.log4j.Logger;
37  
38  import es.accv.arangi.base.ArangiObject;
39  import es.accv.arangi.base.algorithm.HashingAlgorithm;
40  import es.accv.arangi.base.certificate.Certificate;
41  import es.accv.arangi.base.device.KeyStoreManager;
42  import es.accv.arangi.base.exception.certificate.NormalizeCertificateException;
43  import es.accv.arangi.base.exception.certificate.ValidationXMLException;
44  
45  /**
46   * Clase que gestiona una lista de certificados de Autoridades de Validación.<br><br>
47   * 
48   * A la hora de validar un certificado esta clase posee las siguientes funcionalidades:
49   * <ul>
50   * 	<li>Permite restringir la validación únicamente a las Autoridades de Certificación que
51   * 	interese.</li>
52   * 	<li>Permite obtener la cadena de confianza del certificado.</li>
53   * 	<li>Permite definir las URLs de CRLs y OCSPs donde se realizará la validación</li>
54   * </ul><br>
55   * 
56   * Para definir las URLs de CRLs y OCSPs donde se realizarán las validaciones para certificados
57   * de unas determinadas Autoridades de Certificación se utiliza un fichero XML con un formato 
58   * determinado y que puede ser tratado con la clase {@link ValidationXML ValidationXML}.
59   * 
60   * @author <a href="mailto:jgutierrez@accv.es">José M Gutiérrez</a>
61   */
62  public class CAList extends ArangiObject {
63  
64  	/*
65  	 * Nombre del fichero validationXML
66  	 */
67  	private static final String VALIDATION_XML_FILE = "validation_data.xml";
68  
69  	/*
70  	 * Class logger
71  	 */
72  	private Logger logger = Logger.getLogger(CAList.class);
73  	
74  	/*
75  	 * List of CA certificates
76  	 */
77  	private HashMap<String, Map<String,X509Certificate>> hmCACertificates;
78  	
79  	/*
80  	 * Alternative list of CA certificates (by issuer CN)
81  	 */
82  	private HashMap<String, X509Certificate> hmAlternativeCACertificates;
83  	
84  	/*
85  	 * Validation xml object
86  	 */
87  	private ValidationXML validationXML;
88  	
89  	/**
90  	 * Constructor por defecto. Inicializa una lista de CAs vacía.
91  	 */
92  	public CAList () {
93  		logger.debug("[CAList()] :: Inicio");
94  		
95  		hmCACertificates = new HashMap<String, Map<String,X509Certificate>> ();
96  		hmAlternativeCACertificates = new HashMap<String, X509Certificate> ();
97  	}
98  	
99  	/**
100 	 * Constructor que inicializa el objeto con una lista de certificados 
101 	 * java.security.cert.X509Certificate.
102 	 * 
103 	 * @param caList Lista de java.security.cert.X509Certificate
104 	 */
105 	public CAList (List caList) throws NormalizeCertificateException {
106 		logger.debug("[CAList(caList)] :: " + (caList==null?null:caList.size()));
107 		
108 		hmCACertificates = new HashMap<String, Map<String,X509Certificate>> ();
109 		hmAlternativeCACertificates = new HashMap<String, X509Certificate> ();
110 		
111 		//-- iterate and get values for hashmap
112 		Iterator itCAs = caList.iterator();
113 		while (itCAs.hasNext()) {
114 			Object objectCertificate = itCAs.next();
115 			Certificate certificate;
116 			if (objectCertificate instanceof Certificate) {
117 				//-- La lista es de objetos Certificate
118 				certificate = (Certificate) objectCertificate;
119 			} else {
120 				//-- La lista es de objetos X509Certificate
121 				try {
122 					certificate = new Certificate((X509Certificate) objectCertificate);
123 				} catch (ClassCastException e) {
124 					logger.debug("[CAList(caList)] :: Some element in the list is not a valid X.509 certificate");
125 					throw new NormalizeCertificateException ("Algún elemento de la lista no es un objeto X509Certificate");
126 				} catch (NormalizeCertificateException e) {
127 					logger.debug("[CAList(caList)] :: Cannot normalize as a bouncy castle X.509 certificate an element in the list");
128 					throw e;
129 				}
130 			}
131 			if (certificate.getSubjectKeyIdentifier() != null) {
132 				try {
133 					addToCACertificates(certificate.getSubjectKeyIdentifier(), certificate);
134 				} catch (NoSuchAlgorithmException e) {
135 					logger.info("[CAList(caList)] :: No se puede obtener el algoritmo de hashing del certificado:\n" + certificate);
136 				}
137 			}
138 			hmAlternativeCACertificates.put(certificate.getCommonName(), certificate.toX509Certificate());
139 		}
140 	}
141 
142 	/**
143 	 * Constructor que inicializa la lista de CAs con los certificados que se encuentran
144 	 * en el directorio indicado en el parámetro. Si en el directorio también se halla
145 	 * un fichero validation_data.xml, la información que contiene se incluirá en este
146 	 * objeto.<br><br>
147 	 * Si el objeto pasado como parámetro es un fichero (no directorio) el objeto se
148 	 * incializará con este fichero, que debe ser un certificado. 
149 	 * 
150 	 * @param listSource Fichero (certificado) o carpeta qie contiene una lista de certificados.
151 	 */
152 	public CAList (File listSource) throws Exception {
153 		logger.debug("[CAList(listSource)] :: " + listSource);
154 		
155 		//-- Check that listSource is not null
156 		if (listSource == null) {
157 			logger.info ("[CAList(listSource)] :: List source from CA certificates is a null value");
158 			throw new Exception ("List source from CA certificates is a null value");
159 		}
160 		
161 		hmCACertificates = new HashMap<String, Map<String,X509Certificate>>();
162 		hmAlternativeCACertificates = new HashMap<String, X509Certificate>();
163 		if (listSource.isDirectory()) {
164 			//-- listSource is a directory -> try to load all files in the folder like a X509Certificate or a
165 			//-- validation xml
166 			for (int i=0;i<listSource.listFiles().length;i++) {
167 				
168 				//-- if is a directory continue
169 				if (listSource.listFiles()[i].isDirectory()) {
170 					continue;
171 				}
172 				
173 				//-- Check if file is a validation xml
174 				if (listSource.listFiles()[i].getName().equalsIgnoreCase(VALIDATION_XML_FILE)) {
175 					validationXML = new ValidationXML (listSource.listFiles()[i], this);
176 				} else {
177 				
178 					//-- Check if file is a X509Certificate
179 					try {
180 						Certificate certificate = new Certificate(listSource.listFiles()[i]);
181 						if (certificate.getSubjectKeyIdentifier() != null) {
182 							addToCACertificates(certificate.getSubjectKeyIdentifier(), certificate);
183 						}
184 						hmAlternativeCACertificates.put(certificate.getCommonName(), certificate.toX509Certificate());
185 						logger.debug ("[CAList(listSource)] :: File " + listSource.listFiles()[i].getAbsolutePath() + " imported as a CA certificate");
186 					} catch (Exception e) {
187 						logger.debug ("[CAList(listSource)] :: File " + listSource.listFiles()[i].getAbsolutePath() + " is not a X509Certificate", e);
188 					}
189 				}
190 			}
191 			
192 			//-- If no file are found to be a X509Certificate throws an exception
193 			if (hmCACertificates.isEmpty() && hmAlternativeCACertificates.isEmpty()) {
194 				logger.info ("[CAList(listSource)] :: No X509 certificate is found in folder " + listSource.getAbsolutePath());
195 				throw new Exception ("They are not X509 certificates in the folder " + listSource.getAbsolutePath());
196 			}
197 			
198 		} else {
199 			//-- listSource is a file -> the list will only have 1 element
200 			try {
201 				Certificate certificate = new Certificate(listSource);
202 				if (certificate.getSubjectKeyIdentifier() != null) {
203 					addToCACertificates(certificate.getSubjectKeyIdentifier(), certificate);
204 				}
205 				hmAlternativeCACertificates.put(certificate.getCommonName(), certificate.toX509Certificate());
206 			} catch (Exception e) {
207 				logger.info ("[CAList(listSource)] :: Error when getting the CA certificate from path " + listSource.getAbsolutePath(), e);
208 				throw new Exception ("Error when getting the CA certificate from path ", e);
209 			}
210 		}
211 		
212 	}
213 
214 	/**
215 	 * Metodo para obtener el objeto {@link ValidationXML ValidationXML}
216 	 * 
217 	 * @return Objeto {@link ValidationXML ValidationXML} o nulo si no se ha definido
218 	 */
219 	public ValidationXML getValidationXML () {
220 		return validationXML;
221 	}
222 	
223 	/**
224 	 * Metodo para cargar el objeto {@link ValidationXML ValidationXML} junto a
225 	 * la lista de CAs
226 	 * 
227 	 * @param validationXml Fichero de validación XML 
228 	 * @throws Exception 
229 	 */
230 	public void setValidationXML (File validationXml) throws Exception {
231 		this.validationXML = new ValidationXML (validationXml, this);
232 	}
233 	
234 	/**
235 	 * Metodo para cargar el objeto {@link ValidationXML ValidationXML} junto a
236 	 * la lista de CAs
237 	 * 
238 	 * @param validationXml Array de bytes con el fichero de validación XML  
239 	 * @throws ValidationXMLException Error parseando el fichero de validación
240 	 */
241 	public void setValidationXML (byte[] validationXml) throws ValidationXMLException {
242 		this.validationXML = new ValidationXML (validationXml, this);
243 	}
244 	
245 	/**
246 	 * Método que trata de encontrar en la lista de certificados de CA  uno que 
247 	 * sea el emisor del certificado pasado como parámetro.
248 	 * 
249 	 * @param x509Certificate Certificado del que se busca el emisor 
250 	 * @return Certificado emisor o nulo si éste no se encuentra en la lista
251 	 */
252 	public X509Certificate getCACertificate(X509Certificate x509Certificate) {
253 
254 		logger.debug("[CAList.getCACertificate]::Entrada");
255 		
256 		Certificate certificate;
257 		try {
258 			certificate = new Certificate (x509Certificate);
259 			logger.debug("[CAList.getCACertificate]::Certificado: " + certificate.getCommonName());
260 		} catch (NormalizeCertificateException e) {
261 			logger.info ("[CAList.getCACertificate]::El certificado no puede ser normalizado según el proveedor criptográfico de Arangí");
262 			return null;
263 		}
264 		
265 		//-- Comprobar si tiene el campo IKI
266 		if (certificate.getIssuerKeyIdentifier() != null) {
267 			Map<String,X509Certificate> map = (Map<String,X509Certificate>)hmCACertificates.get(certificate.getIssuerKeyIdentifier());
268 			if (map == null || map.isEmpty()) {
269 				return null;
270 			}
271 			if (map.containsKey(HashingAlgorithm.SHA512)) {
272 				return map.get(HashingAlgorithm.SHA512);
273 			}
274 			if (map.containsKey(HashingAlgorithm.SHA384)) {
275 				return map.get(HashingAlgorithm.SHA384);
276 			}
277 			if (map.containsKey(HashingAlgorithm.SHA256)) {
278 				return map.get(HashingAlgorithm.SHA256);
279 			}
280 			if (map.containsKey(HashingAlgorithm.SHA1)) {
281 				return map.get(HashingAlgorithm.SHA1);
282 			}
283 			return map.get(map.keySet().iterator().next());
284 		}
285 		
286 		//-- Probar con el CN del issuer
287 		logger.debug("[CAList.getCACertificate] :: El certificado no tiene SKI, probar por el CN del issuer");
288 		if (certificate.getIssuerCommonName() != null) {
289 			return (X509Certificate)hmAlternativeCACertificates.get(certificate.getIssuerCommonName());
290 		}
291 		
292 		logger.debug("[CAList.getCACertificate] :: Devolviendo null");
293 		return null;
294 		
295 	}
296 
297 	/**
298 	 * Método que trata de encontrar en la lista de certificados de CA  uno que 
299 	 * sea el emisor del certificado pasado como parámetro y tenga como algoritmo
300 	 * de hashing el indicado en el parámetro.
301 	 * 
302 	 * @param x509Certificate Certificado del que se busca el emisor 
303 	 * @param hashingAlgorithm Algoritmo de hashing del certificado emisor
304 	 * @return Certificado emisor o nulo si éste no se encuentra en la lista
305 	 */
306 	public X509Certificate getCACertificate(X509Certificate x509Certificate, String hashingAlgorithm) {
307 
308 		logger.debug("[CAList.getCACertificate]::Entrada");
309 		
310 		Certificate certificate;
311 		try {
312 			certificate = new Certificate (x509Certificate);
313 			logger.debug("[CAList.getCACertificate]::Certificado: " + certificate.getCommonName());
314 		} catch (NormalizeCertificateException e) {
315 			logger.info ("[CAList.getCACertificate]::El certificado no puede ser normalizado según el proveedor criptográfico de Arangí");
316 			return null;
317 		}
318 		
319 		//-- Comprobar si tiene el campo IKI
320 		if (certificate.getIssuerKeyIdentifier() != null) {
321 			Map<String,X509Certificate> map = (Map<String,X509Certificate>)hmCACertificates.get(certificate.getIssuerKeyIdentifier());
322 			if (map == null) {
323 				return null;
324 			}
325 			return map.get(hashingAlgorithm);
326 		}
327 		
328 		//-- Probar con el CN del issuer
329 		logger.debug("[CAList.getCACertificate] :: El certificado no tiene SKI, probar por el CN del issuer");
330 		if (certificate.getIssuerCommonName() != null) {
331 			X509Certificate x509Cert = (X509Certificate)hmAlternativeCACertificates.get(certificate.getIssuerCommonName());
332 			if (x509Cert == null) {
333 				return null;
334 			}
335 			try {
336 				certificate = new Certificate (x509Cert);
337 				if (certificate.getDigestAlgorithm().equalsIgnoreCase(hashingAlgorithm)) {
338 					return x509Cert;
339 				}
340 			} catch (NormalizeCertificateException e) {
341 				logger.info ("[CAList.getCACertificate]::El certificado no puede ser normalizado según el proveedor criptográfico de Arangí");
342 				return null;
343 			} catch (NoSuchAlgorithmException e) {
344 				logger.info ("[CAList.getCACertificate]::No se puede obtener el algoritmo de hashing del certificado:\n" + certificate);
345 				return null;
346 			}
347 		}
348 		
349 		logger.debug("[CAList.getCACertificate] :: Devolviendo null");
350 		return null;
351 		
352 	}
353 	
354 	public List<List<X509Certificate>> getCertificatesChain(X509Certificate x509Certificate) {
355 
356 		logger.debug("[CAList.getCertificatesChain]::Entrada");
357 		
358 		List<List<X509Certificate>> resultado = new ArrayList<List<X509Certificate>>();
359 		while (true) {
360 			Certificate certificate;
361 			try {
362 				certificate = new Certificate (x509Certificate);
363 				logger.debug("[CAList.getCertificatesChain]::Certificado: " + certificate.getCommonName());
364 				if (certificate.isSelfSigned()) {
365 					break;
366 				}
367 			} catch (NormalizeCertificateException e) {
368 				logger.info ("[CAList.getCertificatesChain]::El certificado no puede ser normalizado según el proveedor criptográfico de Arangí");
369 				return null;
370 			}
371 			
372 			//-- Comprobar si tiene el campo IKI
373 			if (certificate.getIssuerKeyIdentifier() != null) {
374 				Map<String,X509Certificate> map = (Map<String,X509Certificate>)hmCACertificates.get(certificate.getIssuerKeyIdentifier());
375 				if (map == null) {
376 					break;
377 				}
378 				List<X509Certificate> lista = new ArrayList<X509Certificate>();
379 				for (String key : map.keySet()) {
380 					lista.add(map.get(key));
381 					x509Certificate = map.get(key);
382 				}
383 				resultado.add(lista);
384 			} else {
385 				//-- Probar con el CN del issuer
386 				logger.debug("[CAList.getCertificatesChain] :: El certificado no tiene SKI, probar por el CN del issuer");
387 				if (certificate.getIssuerCommonName() != null) {
388 					X509Certificate x509Cert = (X509Certificate)hmAlternativeCACertificates.get(certificate.getIssuerCommonName());
389 					if (x509Cert == null) {
390 						break;
391 					}
392 					List<X509Certificate> lista = new ArrayList<X509Certificate>();
393 					lista.add(x509Cert);
394 					resultado.add(lista);
395 					
396 					x509Certificate = x509Cert;
397 				}
398 			}
399 			
400 		}
401 		
402 		return resultado;
403 
404 	}
405 
406 	/**
407 	 * Método que determina si la lista se encuentra vacía.
408 	 * 
409 	 * @return Cierto si la lista está vacía.
410 	 */
411 	public boolean isEmpty() {
412 		return hmCACertificates.isEmpty() && hmAlternativeCACertificates.isEmpty();
413 	}
414 
415 	/**
416 	 * Comprueba si el emisor del certificado pasado como parámetro pertenece
417 	 * a la lista de certificados de CA.
418 	 * 
419 	 * @param x509Certificate Certificado
420 	 * @return Cierto si el emisor del certificado se encuentra en la lista.
421 	 */
422 	public boolean isOwned (X509Certificate x509Certificate) {
423 		logger.debug("[CAList.isOwned]::Entrada");
424 		
425 		Certificate certificate;
426 		try {
427 			certificate = new Certificate (x509Certificate);
428 			logger.debug("[CAList.isOwned]::Certificado: " + certificate.getCommonName());
429 		} catch (NormalizeCertificateException e) {
430 			logger.info ("[CAList.isOwned]::El certificado no puede ser normalizado según el proveedor criptográfico de Arangí");
431 			return false;
432 		}
433 		
434 		//-- Comprobar si tiene el campo IKI
435 		if (certificate.getIssuerKeyIdentifier() != null) {
436 			return hmCACertificates.containsKey(certificate.getIssuerKeyIdentifier());
437 		}
438 		
439 		//-- Probar con el CN del issuer
440 		logger.debug("[CAList.isOwned] :: El certificado no tiene SKI, probar por el CN del issuer");
441 		if (certificate.getIssuerCommonName() != null) {
442 			return hmAlternativeCACertificates.containsKey(certificate.getIssuerCommonName());
443 		}
444 		
445 		logger.debug("[CAList.isOwned] :: Devolviendo null");
446 		return false;
447 		
448 	}
449 
450 	/**
451 	 * Comprueba si el certificado pasado como parámetro pertenece a la lista
452 	 * de certificados de CA.
453 	 * 
454 	 * @param certificate Certificado
455 	 * @return Cierto si el certificado pertenece a la lista
456 	 */
457 	public boolean contains (X509Certificate certificate) { 
458 		Certificate cert;
459 		try {
460 			cert = new Certificate (certificate);
461 		} catch (NormalizeCertificateException e) {
462 			return false;
463 		}
464 		if (cert.getSubjectKeyIdentifier() != null) {
465 			return hmCACertificates.containsKey(cert.getSubjectKeyIdentifier());
466 		} else {
467 			return hmAlternativeCACertificates.containsKey(cert.getCommonName());
468 		}
469 	}
470 
471 	/**
472 	 * Comprueba si el Subject Key Identifier pasado como parámetro pertenece a uno de
473 	 * los certificados de CA de la lista.
474 	 * 
475 	 * @param base64SKI SKI del certificado de una CA en formato base64, tal y como se
476 	 * obtiene del método getIssuerKeyIdentifier de la
477 	 * clase {@link es.accv.arangi.base.certificate.Certificate Certificate}.
478 	 *  
479 	 * @return Cierto si Subject Key Identifier es de un certificado de la lista
480 	 */
481 	public boolean containsKey (String base64SKI) { 
482 		return hmCACertificates.containsKey(base64SKI);
483 	}
484 
485 	/**
486 	 * Obtiene la lista de certificados de CA. La lista no tiene ningún orden.
487 	 * 
488 	 * @return Lista de certificados de CA en formato java.security.X509Certificate
489 	 */
490 	public List<X509Certificate> getCACertificates () {
491 		List<X509Certificate> result = new ArrayList<X509Certificate> ();
492 		for (Map<String,X509Certificate> map : hmCACertificates.values()) {
493 			for (String key : map.keySet()) {
494 				X509Certificate caCertificate = map.get(key);
495 				result.add(caCertificate);
496 			}
497 		}
498 		for (X509Certificate caCertificate : hmAlternativeCACertificates.values()) {
499 			if (!result.contains(caCertificate)) {
500 				result.add(caCertificate);
501 			}
502 		}
503 		
504 		return result;
505 	}
506 	
507 	/**
508 	 * Añade un elemento a la lista de certificados de CA
509 	 * 
510 	 * @param caCertificate Certificado de CA
511 	 */
512 	public void addCACertificate (X509Certificate caCertificate) {
513 		Certificate cert = null;
514 		try {
515 			cert = new Certificate (caCertificate);
516 		} catch (NormalizeCertificateException e) {
517 		}
518 		if (cert != null) {
519 			if (cert.getSubjectKeyIdentifier() != null) {
520 				try {
521 					addToCACertificates(cert.getSubjectKeyIdentifier(), cert);
522 				} catch (NoSuchAlgorithmException e) {
523 					logger.info("[CAList(addCACertificate)] :: No se puede obtener el algoritmo de hashing del certificado:\n" + cert);
524 				}
525 			}
526 			hmAlternativeCACertificates.put(cert.getCommonName(), caCertificate);
527 		}
528 	}
529 	
530 	/**
531 	 * Obtiene un keystore con los certificados contenidos en este objeto. El
532 	 * alias de cada uno de estos certificados es el SKI del certificado.
533 	 * 
534 	 * @return Keystore
535 	 * @throws KeyStoreException Error creando el keystore o añadiendo alguno de
536 	 * 	los elementos
537 	 */
538 	public KeyStore toKeyStore () throws KeyStoreException {
539 		
540 		//-- Crear keystore
541 		KeyStore ks = KeyStore.getInstance(KeyStoreManager.STORE_TYPE_JKS);
542 		try {
543 			ks.load(null, null);
544 		} catch (NoSuchAlgorithmException e) {
545 			// No se va a dar
546 		} catch (CertificateException e) {
547 			// No se va a dar
548 		} catch (IOException e) {
549 			// No se va a dar
550 		}
551 
552 		//-- Añadir los certificados del objeto al keystore
553 		List lCACertificates = getCACertificates();
554 		int i = 0;
555 		for (Iterator iterator = lCACertificates.iterator(); iterator.hasNext();) {
556 			X509Certificate caCertificate = (X509Certificate) iterator.next();
557 			ks.setCertificateEntry("alias" + i, caCertificate);
558 			i++;
559 		}
560 		
561 		return ks;
562 	}
563 	
564 	//-- Métodos privados
565 	
566 	private void addToCACertificates(String subjectKeyIdentifier,
567 			Certificate certificate) throws NoSuchAlgorithmException {
568 		Map<String,X509Certificate> mapCaCertificates = hmCACertificates.get(subjectKeyIdentifier);
569 		if (mapCaCertificates == null) {
570 			mapCaCertificates = new HashMap<String, X509Certificate>();
571 		}
572 		mapCaCertificates.put(certificate.getDigestAlgorithm(), certificate.toX509Certificate());
573 		hmCACertificates.put(subjectKeyIdentifier, mapCaCertificates);
574 	}
575 
576 
577 }