¿SecureString es práctico en una aplicación C #?


224

Siéntete libre de corregirme si mis suposiciones están mal aquí, pero déjame explicarte por qué pregunto.

Tomado de MSDN, a SecureString:

Representa texto que debe mantenerse confidencial. El texto está encriptado para mayor privacidad cuando se usa, y se elimina de la memoria de la computadora cuando ya no es necesario.

Entiendo esto, tiene mucho sentido almacenar una contraseña u otra información privada en un SecureStringover a System.String, porque puedes controlar cómo y cuándo se almacena realmente en la memoria, porque a System.String:

es inmutable y, cuando ya no se necesita, no se puede programar mediante programación para la recolección de basura; es decir, la instancia es de solo lectura después de su creación y no es posible predecir cuándo se eliminará de la memoria de la computadora. En consecuencia, si un objeto String contiene información confidencial, como una contraseña, un número de tarjeta de crédito o datos personales, existe el riesgo de que la información se revele después de ser utilizada porque su aplicación no puede eliminar los datos de la memoria de la computadora.

Sin embargo, en el caso de una aplicación GUI (por ejemplo, un cliente ssh), el SecureString debe construirse a partir de a System.String . Todos los controles de texto usan una cadena como su tipo de datos subyacente .

Entonces, esto significa que cada vez que el usuario presiona una tecla, la cadena anterior que estaba allí se descarta y se crea una nueva cadena para representar cuál es el valor dentro del cuadro de texto, incluso si se usa una máscara de contraseña. Y no podemos controlar cuándo o si alguno de esos valores se descarta de la memoria .

Ahora es el momento de iniciar sesión en el servidor. ¿Adivina qué? Debe pasar una cadena por la conexión para la autenticación . Así que vamos a convertir nuestro SecureStringen un System.String... y ahora tenemos una cadena en el montón sin forma de forzarlo a pasar por la recolección de basura (o escribir 0 en su búfer).

Mi punto es : no importa lo que haces, en algún lugar a lo largo de la línea, que SecureStringse va a convertir en una System.String, lo que significa que al menos existe en el montón en algún momento (sin ninguna garantía de recolección de basura).

Mi punto no es : si hay formas de eludir el envío de una cadena a una conexión ssh, o evitar que un control almacene una cadena (haga un control personalizado). Para esta pregunta, puede reemplazar "conexión ssh" con "formulario de inicio de sesión", "formulario de registro", "formulario de pago", "formulario de alimentos que alimentaría a su cachorro pero no a sus hijos", etc.

  • Entonces, ¿en qué momento el uso de a se SecureStringvuelve realmente práctico?
  • ¿Alguna vez vale la pena el tiempo de desarrollo adicional para erradicar por completo el uso de un System.Stringobjeto?
  • ¿El objetivo SecureStringes simplemente reducir la cantidad de tiempo que a System.Stringestá en el montón (reduciendo el riesgo de pasar a un archivo de intercambio físico)?
  • Si un atacante ya tiene los medios para una inspección de montón, lo más probable es que (A) ya tenga los medios para leer las pulsaciones de teclas, o (B) ya tenga físicamente la máquina ... Así que usar un SecureStringimpedimento para que llegue datos de todos modos?
  • ¿Es esto simplemente "seguridad a través de la oscuridad"?

Lo siento si estoy planteando las preguntas demasiado, la curiosidad me ganó. No dude en responder cualquiera o todas mis preguntas (o dígame que mis suposiciones están completamente equivocadas). :)


25
Tenga en cuenta que SecureStringno es realmente una cadena segura. Es simplemente una forma de disminuir el intervalo de tiempo en el que alguien puede inspeccionar su memoria y obtener con éxito los datos confidenciales. Esto no es a prueba de balas y no estaba destinado a serlo. Pero los puntos que estás planteando son muy válidos. Relacionado: stackoverflow.com/questions/14449579/…
Theodoros Chatzigiannakis

1
@TheodorosChatzigiannakis, sí, eso es más o menos lo que pensé. Acabo de pasar todo el día hoy hurgando en mi cerebro tratando de encontrar una forma segura de almacenar una contraseña para la vida útil de la aplicación, y me hizo preguntarme, ¿vale la pena?
Steven Jeffries

1
Probablemente no valga la pena. Pero, de nuevo, podría estar defendiéndose contra un escenario extremo. Extremo, no en el sentido de que es poco probable que alguien obtenga este tipo de acceso a su computadora, sino en el sentido de que si alguien obtiene este tipo de acceso, la computadora se considera (para todos los efectos) comprometida y yo no ' Creo que hay algún lenguaje o técnica que pueda usar para defenderse completamente de esto.
Theodoros Chatzigiannakis

8
Protege contra una contraseña visible durante años , mucho después de que todos dejaron de pensar que podría haber un problema. En un disco duro descartado, almacenado en el archivo de paginación. Limpiar la memoria para que las probabilidades de esto sean bajas es importante, no se puede hacer eso con System.String ya que es inmutable. Requiere una infraestructura a la que pocos programas tienen acceso actualmente, la interoperabilidad con el código nativo, por lo que los métodos Marshal.SecureStringXxx () son útiles y se está volviendo raro. O una forma decente de pedirle al usuario que lo haga :)
Hans Passant

1
SecureString es una técnica de seguridad en profundidad. La compensación entre el costo de desarrollo y la ganancia de seguridad no es favorable en la mayoría de las situaciones. Hay muy pocos casos de buen uso para ello.
Usr

Respuestas:


237

En realidad, hay usos muy prácticos de SecureString.

¿Sabes cuántas veces he visto tales escenarios? (la respuesta es: ¡muchas!):

  • Una contraseña aparece en un archivo de registro accidentalmente.
  • Se muestra una contraseña en algún lugar: una vez que una GUI mostró una línea de comando de aplicación que se estaba ejecutando, y la línea de comando consistía en una contraseña. Vaya .
  • Usando el generador de perfiles de memoria para el software de perfil con su colega. Colega ve su contraseña en la memoria. ¿Suena irreal? De ningún modo.
  • Una vez utilicé un RedGatesoftware que podía capturar el "valor" de las variables locales en caso de excepciones, increíblemente útil. Sin embargo, puedo imaginar que registrará "contraseñas de cadena" accidentalmente.
  • Un volcado de memoria que incluye contraseña de cadena.

¿Sabes cómo evitar todos estos problemas? SecureString. En general, se asegura de que no cometas errores tontos como tales. ¿Cómo lo evita? Al asegurarse de que la contraseña esté encriptada en la memoria no administrada y que solo se pueda acceder al valor real cuando esté 90% seguro de lo que está haciendo.

En el sentido, SecureStringfunciona con bastante facilidad:

1) Todo está encriptado

2) llamadas de usuario AppendChar

3) Descifra todo en MEMORIA NO MANEJADA y agrega el personaje

4) Cifrar todo de nuevo en MEMORIA NO MANEJADA.

¿Qué pasa si el usuario tiene acceso a su computadora? ¿Podría un virus tener acceso a todo el SecureStrings? Si. Todo lo que necesita hacer es conectarse RtlEncryptMemorycuando se descifre la memoria, obtendrá la ubicación de la dirección de memoria no cifrada y la leerá. Voila! De hecho, podría crear un virus que constantemente analizará el uso SecureStringy registrará todas las actividades con él. No estoy diciendo que será una tarea fácil, pero se puede hacer. Como puede ver, la "potencia" de SecureStringdesaparece por completo una vez que hay un usuario / virus en su sistema.

Tienes algunos puntos en tu publicación. Claro, si usa algunos de los controles de la interfaz de usuario que contienen una "contraseña de cadena" internamente, usar actual SecureStringno es tan útil. Aunque, aún así, puede proteger contra alguna estupidez que he enumerado anteriormente.

Además, como otros han señalado, WPF admite PasswordBox, que utiliza SecureStringinternamente a través de su propiedad SecurePassword .

La conclusión es; si tiene datos confidenciales (contraseñas, tarjetas de crédito, ..), use SecureString. Esto es lo que C # Framework está siguiendo. Por ejemplo, la NetworkCredentialclase almacena la contraseña como SecureString. Si observa esto , puede ver más de 80 usos diferentes en .NET framework de SecureString.

Hay muchos casos en los que tiene que convertir SecureStringa cadena, porque algunas API lo esperan.

El problema habitual es:

  1. La API es GENÉRICA. No sabe que hay datos confidenciales.
  2. La API sabe que se trata de datos confidenciales y usa "cadena", eso es simplemente un mal diseño.

Has planteado un buen punto: ¿qué sucede cuando SecureStringse convierte a string? Esto solo puede suceder debido al primer punto. Por ejemplo, la API no sabe que son datos confidenciales. Personalmente no he visto que eso suceda. Sacar una cadena de SecureString no es tan simple.

No es simple por una simple razón ; nunca tuvo la intención de permitir que el usuario convirtiera SecureString en cadena, como usted dijo: GC se activará. Si se ve haciendo eso, debe retroceder y preguntarse: ¿por qué estoy haciendo esto, o realmente necesito? ¿esto por que?

Hay un caso interesante que vi. A saber, la función WinApi LogonUser toma LPTSTR como contraseña, lo que significa que debe llamar SecureStringToGlobalAllocUnicode. Básicamente, eso le proporciona una contraseña no cifrada que vive en la memoria no administrada. Debe deshacerse de eso tan pronto como termine:

// Marshal the SecureString to unmanaged memory.
IntPtr rawPassword = Marshal.SecureStringToGlobalAllocUnicode(password);
try
{
   //...snip...
}
finally 
{
   // Zero-out and free the unmanaged string reference.
   Marshal.ZeroFreeGlobalAllocUnicode(rawPassword);
}

Siempre puede extender la SecureStringclase con un método de extensión, como ToEncryptedString(__SERVER__PUBLIC_KEY), que le proporciona una stringinstancia de SecureStringque está cifrada con la clave pública del servidor. Solo el servidor puede descifrarlo. Problema resuelto: Garbage Collection nunca verá la cadena "original", ya que nunca la expondrá en la memoria administrada. Esto es exactamente lo que se está haciendo en PSRemotingCryptoHelper( EncryptSecureStringCore(SecureString secureString)).

Y como algo muy relacionado: Mono SecureString no cifra en absoluto . La implementación se ha comentado porque ... espere ... "De alguna manera causa la rotura de la prueba de la unidad" , lo que me lleva a mi último punto:

SecureStringno es compatible en todas partes. Si la plataforma / arquitectura no es compatible SecureString, obtendrá una excepción. Hay una lista de plataformas compatibles con la documentación.


Creo que SecureStringsería mucho más práctico si hubiera un mecanismo para acceder a los caracteres individuales de los mismos. La eficiencia para tal mecanismo podría mejorarse teniendo un tipo de estructura SecureStringTempBuffersin ningún miembro público, que podría pasarse refal SecureStringmétodo de "leer un carácter" [si el cifrado / descifrado se procesa en fragmentos de 8 caracteres, dicha estructura podría contener datos para 8 caracteres y la ubicación del primero de esos caracteres dentro de SecureString].
supercat

2
Marco esto en cada auditoría de código fuente que realizo. También debe repetirse que si lo usa SecureString, debe usarse en toda la pila.
Casey

1
Implementé la extensión .ToEncryptedString () pero usando el certificado. ¿Te importaría echar un vistazo y avisarme si estoy haciendo todo esto mal? Espero que sea seguro engouhg gist.github.com/sturlath/99cfbfff26e0ebd12ea73316d0843330
Sturla

@Sturla - ¿Qué hay EngineSettingsen su método de extensión?
InteXX


15

Hay pocos problemas en sus suposiciones.

En primer lugar, la clase SecureString no tiene un constructor de cadenas. Para crear uno, asigna un objeto y luego agrega los caracteres.

En el caso de una GUI o una consola, puede pasar fácilmente cada tecla presionada a una cadena segura.

La clase está diseñada de manera que no puede, por error, acceder al valor almacenado. Esto significa que no puede obtenerla stringcomo contraseña directamente de ella.

Entonces, para usarlo, por ejemplo, para autenticarse a través de la web, deberá usar clases adecuadas que también sean seguras.

En el marco .NET tiene algunas clases que pueden usar SecureString

  • El control PasswordBox de WPF mantiene la contraseña como SecureString internamente.
  • La propiedad de contraseña de System.Diagnostics.ProcessInfo es un SecureString.
  • El constructor para X509Certificate2 toma un SecureString para la contraseña.

(más)

Para concluir, la clase SecureString puede ser útil, pero requiere más atención por parte del desarrollador.

Todo esto, con ejemplos, está bien descrito en la documentación de SecureString de MSDN


10
No entiendo bien el penúltimo párrafo de su respuesta. ¿Cómo transferiría un a SecureStringtravés de la red sin serializarlo en un string? Creo que el punto de OP de la pregunta es que llega un momento en el que quieres usar realmente el valor que se mantiene de forma segura en un SecureString, lo que significa que tienes que sacarlo de allí, y ya no es seguro. En toda la biblioteca de clases de Framework, prácticamente no hay métodos que acepten una SecureStringentrada directamente (por lo que puedo ver), entonces, ¿cuál es el punto de mantener un valor dentro de a SecureStringen primer lugar?
stakx - ya no contribuye el

@ DamianLeszczyński-Vash, sé que SecureString no tiene un constructor de cadenas, pero mi punto es que (A) crea un SecureString y agrega cada carácter de una cadena, o (B) en algún momento necesita usar el valor almacenado en SecureString como una cadena para pasarlo a alguna API. Dicho esto, traes algunos buenos puntos, y puedo ver cómo en algunos casos muy específicos puede ser útil.
Steven Jeffries

1
@stakx Lo enviaría a través de la red obteniendo char[]y enviando cada uno a chartravés de un zócalo, sin crear objetos de recolección de basura en el proceso.
user253751

Char [] es coleccionable y mutable, por lo que puede sobrescribirse.
Peter Ritchie

Por supuesto, también es fácil olvidar sobrescribir esa matriz. De cualquier manera, cualquier API a la que le pase los caracteres también puede hacer una copia.
cHao

10

Un SecureString es útil si:

  • Lo construye carácter por carácter (por ejemplo, desde la entrada de la consola) o lo obtiene de una API no administrada

  • Lo usa pasándolo a una API no administrada (SecureStringToBSTR).

Si alguna vez lo convierte en una cadena administrada, ha vencido su propósito.

ACTUALIZAR en respuesta al comentario

... o BSTR como usted menciona, que no parece más seguro

Después de que se haya convertido a un BSTR, el componente no administrado que consume el BSTR puede poner a cero la memoria. La memoria no administrada es más segura en el sentido de que se puede restablecer de esta manera.

Sin embargo, hay muy pocas API en .NET Framework que admitan SecureString, por lo que tiene razón al decir que hoy tiene un valor muy limitado.

El caso de uso principal que vería es en una aplicación cliente que requiere que el usuario ingrese un código o contraseña altamente confidenciales. La entrada del usuario podría usarse carácter por carácter para construir un SecureString, luego podría pasar a una API no administrada, que pone a cero el BSTR que recibe después de usarlo. Cualquier volcado de memoria posterior no contendrá la cadena sensible.

En una aplicación de servidor es difícil ver dónde sería útil.

ACTUALIZACIÓN 2

Un ejemplo de una API .NET que acepta un SecureString es este constructor para la clase X509Certificate . Si espelta con ILSpy o similar, verá que SecureString se convierte internamente en un búfer no administrado ( Marshal.SecureStringToGlobalAllocUnicode), que luego se pone a cero cuando termina con ( Marshal.ZeroFreeGlobalAllocUnicode).


1
+1, pero ¿no siempre terminarás "derrotando su propósito"? Dado que casi ningún método en Framework Class Library acepta una SecureStringentrada, ¿cuál sería su uso? Siempre tendrías que volver a convertir stringtarde o temprano (o BSTRcomo mencionas, lo que no parece más seguro). Entonces, ¿por qué molestarse SecureStringen absoluto?
stakx - ya no contribuye el

5

Microsoft no recomienda usar SecureStringpara códigos más nuevos.

De la documentación de la clase SecureString :

Importante

No recomendamos que use la SecureStringclase para un nuevo desarrollo. Para obtener más información, consulte SecureStringno debe usarse

Que recomienda:

No lo use SecureStringpara código nuevo. Al portar código a .NET Core, tenga en cuenta que el contenido de la matriz no está cifrado en la memoria.

El enfoque general de tratar con credenciales es evitarlas y confiar en otros medios para autenticarse, como certificados o autenticación de Windows. en GitHub.


4

Como ha identificado correctamente, SecureStringofrece una ventaja específica sobre string: el borrado determinista. Hay dos problemas con este hecho:

  1. Como otros han mencionado y como usted mismo ha notado, esto no es suficiente por sí solo. Debe asegurarse de que cada paso del proceso (incluida la recuperación de la entrada, la construcción de la cadena, el uso, la eliminación, el transporte, etc.) ocurra sin anular el propósito del uso SecureString. Esto significa que debe tener cuidado de no crear nunca un stringbúfer inmutable administrado por GC o cualquier otro búfer que almacene la información confidencial (o tendrá que hacer un seguimiento de eso también). En la práctica, esto no siempre es fácil de lograr, porque muchas API solo ofrecen una forma de trabajar string, no SecureString. E incluso si logras hacer todo bien ...
  2. SecureStringprotege contra tipos de ataque muy específicos (y para algunos de ellos, ni siquiera es tan confiable). Por ejemplo, SecureStringle permite reducir la ventana de tiempo en la que un atacante puede volcar la memoria de su proceso y extraer con éxito la información confidencial (nuevamente, como señaló correctamente), pero con la esperanza de que la ventana sea demasiado pequeña para que el atacante pueda tomar una instantánea de su memoria no se considera seguridad en absoluto.

Entonces, ¿cuándo deberías usarlo? Solo cuando trabajas con algo que puede permitirte trabajar SecureStringpara todas tus necesidades e incluso entonces debes tener en cuenta que esto es seguro solo en circunstancias específicas.


2
Está asumiendo que un atacante puede capturar la memoria de un proceso en un momento determinado. Ese no es necesariamente el caso. Considere un volcado por caída. Si el bloqueo ocurrió después de que la aplicación se realizó con los datos confidenciales, el volcado del bloqueo no debe contener los datos confidenciales.

4

El texto a continuación se copia del analizador de código estático HP Fortify

Resumen: El método PassString () en PassGenerator.cs almacena datos confidenciales de manera insegura (es decir, en cadena), lo que permite extraer los datos mediante la inspección del montón.

Explicación: Los datos confidenciales (como contraseñas, números de seguridad social, números de tarjetas de crédito, etc.) almacenados en la memoria pueden filtrarse si se almacenan en un objeto String administrado. Los objetos de cadena no están anclados, por lo que el recolector de basura puede reubicar estos objetos a voluntad y dejar varias copias en la memoria. Estos objetos no están encriptados de manera predeterminada, por lo que cualquiera que pueda leer la memoria del proceso podrá ver el contenido. Además, si la memoria del proceso se intercambia en el disco, los contenidos no cifrados de la cadena se escribirán en un archivo de intercambio. Por último, dado que los objetos String son inmutables, el recolector de basura CLR solo puede eliminar el valor de un String de la memoria. No es necesario que el recolector de basura se ejecute a menos que el CLR tenga poca memoria, por lo que no hay garantía de cuándo tendrá lugar la recolección de basura.

Recomendaciones: en lugar de almacenar datos confidenciales en objetos como cadenas, almacénelos en un objeto SecureString. Cada objeto almacena su contenido en un formato cifrado en la memoria en todo momento.


3

Me gustaría abordar este punto:

Si un atacante ya tiene los medios para una inspección montón, entonces lo más probable es o bien (A) ya cuentan con los medios para leer las pulsaciones del teclado, o (B) ya tener físicamente la máquina ... Así sería el uso de un SecureStringles impiden llegar a la datos de todos modos?

Un atacante puede no tener acceso completo a la computadora y la aplicación, pero puede tener medios para acceder a algunas partes de la memoria del proceso. Generalmente es causado por errores como desbordamientos del búfer cuando la entrada especialmente construida puede hacer que la aplicación exponga o sobrescriba alguna parte de la memoria.

HeartBleed perdiendo memoria

Tome Heartbleed por ejemplo. Las solicitudes especialmente construidas pueden hacer que el código exponga partes aleatorias de la memoria del proceso al atacante. Un atacante puede extraer certificados SSL de la memoria, sin embargo, lo único que necesita es usar una solicitud con formato incorrecto.

En el mundo del código administrado, los desbordamientos del búfer se convierten en un problema con mucha menos frecuencia. Y en el caso de WinForms, los datos ya están almacenados de manera insegura y no puede hacer nada al respecto. Esto hace que la protección sea SecureStringprácticamente inútil.

Sin embargo, la GUI se puede programar para usar SecureString, y en tal caso puede valer la pena reducir la ventana de disponibilidad de contraseña en la memoria. Por ejemplo, PasswordBox.SecurePassword de WPF es de tipo SecureString.


Hmm, definitivamente es interesante saberlo. Honestamente, en realidad no estoy tan preocupado por el tipo de ataques necesarios para obtener un valor de System.String de mi memoria, solo pregunto por pura curiosidad. Sin embargo, gracias por la información! :)
Steven Jeffries

2

Hace algún tiempo tuve que crear una interfaz ac # contra una pasarela de pago de tarjeta de crédito Java y uno necesitaba un cifrado de clave de comunicaciones seguro compatible. Como la implementación de Java fue bastante específica y tuve que trabajar con los datos protegidos de una manera determinada.

Encontré que este diseño es bastante fácil de usar y más fácil que trabajar con SecureString ... para aquellos que les gusta usar ... siéntanse libres, sin restricciones legales :-). Tenga en cuenta que estas clases son internas, es posible que deba hacerlas públicas.

namespace Cardinity.Infrastructure
{
    using System.Security.Cryptography;
    using System;
    enum EncryptionMethods
    {
        None=0,
        HMACSHA1,
        HMACSHA256,
        HMACSHA384,
        HMACSHA512,
        HMACMD5
    }


internal class Protected
{
    private  Byte[] salt = Guid.NewGuid().ToByteArray();

    protected byte[] Protect(byte[] data)
    {
        try
        {
            return ProtectedData.Protect(data, salt, DataProtectionScope.CurrentUser);
        }
        catch (CryptographicException)//no reason for hackers to know it failed
        {
#if DEBUG
            throw;
#else
            return null;
#endif
        }
    }

    protected byte[] Unprotect(byte[] data)
    {
        try
        {
            return ProtectedData.Unprotect(data, salt, DataProtectionScope.CurrentUser);
        }
        catch (CryptographicException)//no reason for hackers to know it failed
        {
#if DEBUG
            throw;
#else
            return null;
#endif
        }
    }
}


    internal class SecretKeySpec:Protected,IDisposable
    {
        readonly EncryptionMethods _method;

        private byte[] _secretKey;
        public SecretKeySpec(byte[] secretKey, EncryptionMethods encryptionMethod)
        {
            _secretKey = Protect(secretKey);
            _method = encryptionMethod;
        }

        public EncryptionMethods Method => _method;
        public byte[] SecretKey => Unprotect( _secretKey);

        public void Dispose()
        {
            if (_secretKey == null)
                return;
            //overwrite array memory
            for (int i = 0; i < _secretKey.Length; i++)
            {
                _secretKey[i] = 0;
            }

            //set-null
            _secretKey = null;
        }
        ~SecretKeySpec()
        {
            Dispose();
        }
    }

    internal class Mac : Protected,IDisposable
    {
        byte[] rawHmac;
        HMAC mac;
        public Mac(SecretKeySpec key, string data)
        {

            switch (key.Method)
            {
                case EncryptionMethods.HMACMD5:
                    mac = new HMACMD5(key.SecretKey);
                    break;
                case EncryptionMethods.HMACSHA512:
                    mac = new HMACSHA512(key.SecretKey);
                    break;
                case EncryptionMethods.HMACSHA384:
                    mac = new HMACSHA384(key.SecretKey);
                    break;
                case EncryptionMethods.HMACSHA256:
                    mac = new HMACSHA256(key.SecretKey);

                break;
                case EncryptionMethods.HMACSHA1:
                    mac = new HMACSHA1(key.SecretKey);
                    break;

                default:                    
                    throw new NotSupportedException("not supported HMAC");
            }
            rawHmac = Protect( mac.ComputeHash(Cardinity.ENCODING.GetBytes(data)));            

        }

        public string AsBase64()
        {
            return System.Convert.ToBase64String(Unprotect(rawHmac));
        }

        public void Dispose()
        {
            if (rawHmac != null)
            {
                //overwrite memory address
                for (int i = 0; i < rawHmac.Length; i++)
                {
                    rawHmac[i] = 0;
                }

                //release memory now
                rawHmac = null;

            }
            mac?.Dispose();
            mac = null;

        }
        ~Mac()
        {
            Dispose();
        }
    }
}
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.