En Java, nous faisons confiance

Faites confiance à tout le monde? Ne fais confiance a personne? Cela ressemble un peu aux X-Files, mais quand il s'agit d'informations confidentielles, savoir à qui vous faites confiance est aussi important que de savoir en quoi vous leur faites confiance. Ce concept est aussi important pour les applications que pour les personnes. Après tout, nous avons fait des demandes les gardiens de nos informations et les gardiens de nos ressources. C'est vrai dans toute l'entreprise - les applications contiennent des informations essentielles sur notre entreprise et nos clients - et c'est vrai sur le bureau. Je ne peux pas vous dire combien de fois on m'a demandé comment écrire une applet qui analyse le lecteur d'un utilisateur afin qu'un utilisateur puisse réquisitionner le navigateur d'un autre utilisateur ou capturer des informations privées.

Java, étant la plate-forme de développement de réseau qu'il est, a dû s'attaquer de front au problème de la confiance. Le résultat est l'API de sécurité Java et l'architecture de cryptographie Java.

Un bref regard en arrière

Avant de plonger tête baissée dans les API, le code et les commentaires, j'aimerais revenir brièvement sur la discussion du mois dernier. Si vous nous rejoignez pour la première fois, vous voudrez peut-être sauvegarder un mois et lire "Signé et livré: une introduction à la sécurité et à l'authentification". Cette colonne fournit une introduction complète à tous les termes et concepts que j'utiliserai ce mois-ci.

La sécurité et l'authentification répondent à deux préoccupations cruciales: celle de prouver qu'un message a été créé par une entité particulière, et celle de prouver qu'un message n'a pas été falsifié après sa création. Une façon d'atteindre ces deux objectifs consiste à utiliser des signatures numériques.

Les signatures numériques dépendent fortement d'une branche de la cryptographie connue sous le nom de cryptographie à clé publique. Les algorithmes à clé publique se caractérisent par le fait qu'ils reposent sur une paire de clés appariées (une privée et une publique) plutôt que sur une seule clé. Une entité garde sa clé privée secrète, mais rend sa clé publique disponible.

Un algorithme de signature numérique prend en entrée un message et la clé privée d'une entité, et génère une signature numérique. La signature numérique est créée de telle manière que n'importe qui peut prendre la clé publique de l'entité et l'utiliser pour vérifier que l'entité a bien signé le message en question. De plus, si le message d'origine a été falsifié, la signature ne peut plus être vérifiée. Les signatures numériques offrent un avantage supplémentaire: une fois qu'une entité a signé et distribué un message, il est impossible pour son expéditeur de nier avoir signé le message (sans prétendre que sa clé privée a été volée, de toute façon).

Des moteurs et des fournisseurs

L'API de cryptographie Java définit la boîte à outils Java pour la sécurité et l'authentification. L'architecture de cryptographie Java (JCA) décrit comment utiliser l'API. Pour assurer le plus haut degré de flexibilité tant pour le développeur que pour l'utilisateur final, le JCA adopte deux principes directeurs:

  1. L'architecture doit prendre en charge l'indépendance et l'extensibilité des algorithmes. Un développeur doit être capable d'écrire des applications sans les lier trop étroitement à un algorithme particulier. De plus, au fur et à mesure que de nouveaux algorithmes sont développés, ils doivent être facilement intégrés aux algorithmes existants.

  2. L'architecture doit prendre en charge l'indépendance de la mise en œuvre et l'interopérabilité. Un développeur doit être capable d'écrire des applications sans les lier à l'implémentation d'un algorithme par un fournisseur particulier. De plus, les implémentations d'un algorithme fourni par différents fournisseurs doivent interopérer.

Pour répondre à ces deux exigences, les développeurs de l'API Java Cryptography ont basé leur conception sur un système de moteurs et de fournisseurs.

Les moteurs produisent des instances de générateurs de résumé de messages, de générateurs de signature numérique et de générateurs de paires de clés. Chaque instance est utilisée pour exécuter sa fonction correspondante.

Le moteur canonique du JCA est une classe qui fournit une méthode statique (ou des méthodes) nommée getInstance(), qui retourne une instance d'une classe qui implémente un algorithme cryptographiquement significatif. La getInstance()méthode se présente à la fois sous une forme à un argument et à deux arguments. Dans les deux cas, le premier argument est le nom de l'algorithme. Le JCA fournit une liste de noms standard, bien que tous ne soient pas fournis dans une version particulière. Le deuxième argument sélectionne un fournisseur.

Le fournisseur SUN

Un seul fournisseur - SUN - est fourni dans JDK 1.1. SUN fournit à la fois une implémentation de l'algorithme de signature numérique NIST (DSA) et une implémentation des algorithmes de résumé de message MD5 et NIST SHA-1.

Message de classeDigest

Nous allons commencer par examiner le code qui génère un résumé de message à partir d'un message.

MessageDigest messagedigest = MessageDigest.getInstance ("SHA");

MessageDigest messagedigest = MessageDigest.getInstance ("SHA", "SUN");

Comme je l'ai mentionné il y a un instant, la getInstance()méthode se décline en deux saveurs. Le premier nécessite que seul l'algorithme soit spécifié. La seconde nécessite à la fois la spécification de l'algorithme et du fournisseur. Les deux renvoient une instance d'une classe qui implémente l'algorithme SHA.

Ensuite, nous transmettons le message via le générateur de résumé de message.

int n = 0; byte [] rgb = nouvel octet [1000]; while ((n = inputstreamMessage.read (rgb))> -1) {messagedigest.update (rgb, 0, n); }

Ici, nous supposons que le message est disponible en tant que flux d'entrée. Ce code fonctionne bien pour les messages volumineux de longueur inconnue. La update()méthode accepte également un seul octet comme argument pour les messages de quelques octets de longueur, et un tableau d'octets pour les messages d'une taille fixe ou prévisible.

rgb = messagedigest.digest ();

La dernière étape consiste à générer le résumé du message lui-même. Le condensé résultant est codé dans un tableau d'octets.

Comme vous pouvez le voir, le JCA cache commodément toutes les implémentations de bas niveau et les détails spécifiques à l'algorithme, vous permettant de travailler à un niveau plus élevé et plus abstrait.

Bien sûr, l'un des risques d'une telle approche abstraite est la probabilité accrue que nous ne reconnaissions pas les sorties erronées résultant de bogues. Compte tenu du rôle de la cryptographie, cela peut être un problème important.

Considérez le bogue "off-by-one" dans la ligne de mise à jour ci-dessous:

int n = 0; byte [] rgb = nouvel octet [1000]; while ((n = inputstreamMessage.read (rgb))> -1) {messagedigest.update (rgb, 0, n - 1); }

Les programmeurs C, C ++ et Java utilisent l'idiome limit-minus-one si fréquemment que sa saisie devient presque automatique, même si ce n'est pas approprié. Le code ci-dessus se compilera et l'exécutable s'exécutera sans erreur ni avertissement, mais le résumé de message résultant sera erroné.

Heureusement, le JCA est bien pensé et bien conçu, ce qui rend les pièges potentiels comme celui ci-dessus relativement rares.

Avant de passer aux générateurs de paires de clés, jetez un œil à

MessageDigestGenerator, le code source complet d'un programme qui génère un résumé de message.

Classe KeyPairGenerator

Pour générer une signature numérique (et crypter les données), nous avons besoin de clés.

Key generation, in its algorithm-independent form, is not substantially more difficult than creating and using a message digest.

KeyPairGenerator keypairgenerator = KeyPairGenerator.getInstance("DSA");

As in the message digest example above, this code creates an instance of a class that generates DSA-compatible keys. A second (if necessary) argument specifies the provider.

After a key-pair generator instance is created, it must be initialized. We can initialize key-pair generators in one of two ways: algorithm-independent or algorithm-dependent. Which method you use depends on the amount of control you want over the final result.

keypairgenerator.initialize(1024, new SecureRandom());

Keys based on different algorithms differ in how they're generated, but they have one parameter in common -- the key's strength. Strength is a relative term that corresponds roughly to how hard the key will be to "break." If you use the algorithm-independent initializer, you can specify only the strength -- any algorithm-dependent values assume reasonable defaults.

DSAKeyPairGenerator dsakeypairgenerator = (DSAKeyPairGenerator) keypairgenerator; DSAParams dsaparams = new DSAParams() { private BigInteger p = BigInteger(...); private BigInteger q = BigInteger(...); private BigInteger g = BigInteger(...); public BigInteger getP() { return p; } public BigInteger getQ() { return q; } public BigInteger getG() { return g; } }; dsakeypairgenerator.initialize(dsaparams, new SecureRandom());

While the defaults are usually good enough, if you need more control, it is available. Let's assume you used the engine to create a generator of DSA-compatible keys, as in the code above. Behind the scenes, the engine loaded and instantiated an instance of a class that implements the DSAKeyPairGenerator interface. If we cast the generic key-pair generator we received to DSAKeyPairGenerator, we then gain access to the algorithm-dependent method of initialization.

To initialize a DSA key-pair generator, we need three values: the prime P, the subprime Q, and the base G. These values are captured in an inner class instance that is passed to the initialize() method.

The SecureRandom class provides a secure source of random numbers used in the key-pair generation.

return keypairgenerator.generateKeyPair();

The final step involves generating the key pair itself.

Before we move on to digital signatures, take a look at KeyTools, the complete source code for a program that generates a key pair.

Class Signature

The creation and use of an instance of the Signature class is not substantially different from either of the two previous examples. The differences lie in how the instance is used -- either to sign or to verify a message.

Signature signature = Signature.getInstance("DSA");

Just as before, we use the engine to get an instance of the appropriate type. What we do next depends on whether or not we are signing or verifying a message.

signature.initSign(privatekey);

In order to sign a message, we must first initialize the signature instance with the private key of the entity that is signing the message.

signature.initVerify(publickey);

In order to verify a message, we must initialize the signature instance with the public key of the entity that claims it signed the message.

int n = 0; byte [] rgb = new byte [1000]; while ((n = inputstreamMessage.read(rgb)) > -1) { signature.update(rgb, 0, n); }

Next, regardless of whether or not we are signing or verifying, we must pass the message through the signature generator. You'll notice how similar the process is to the earlier example of generating a message digest.

The final step consists of generating the signature or verifying a signature.

rgb = signature.sign();

If we are signing a message, the sign() method returns the signature.

signature.verify(rgbSignature);

If we are verifying the signature previously generated from a message, we must use the verify() method. It takes as a parameter the previously generated signature and determines whether or not it is still valid.

Before we wrap things up, take a look at Sign.java, the complete source code for a program that signs a message, and Verify.java, the complete source code for a program that verifies a message.

Conclusion

If you arm yourself with the tools and techniques I've presented this month, you'll be more than ready to secure your applications. The Java Cryptography API makes the process almost effortless. Release 1.2 of the Java Developers Kit promises even more. Stay tuned.

Le mois prochain, je retournerai dans le territoire du middleware. Je vais prendre un petit RMI, un peu de threading et un tas de code, et vous montrer comment créer votre propre middleware orienté message.

Todd Sundsted a écrit des programmes depuis que les ordinateurs sont devenus disponibles dans des modèles de bureau pratiques. Bien qu'intéressé à l'origine par la création d'applications d'objets distribués en C ++, Todd est passé au langage de programmation Java lorsqu'il est devenu le choix évident pour ce genre de choses. En plus de l'écriture, Todd est président d'Etcee, qui offre des services de formation, de mentorat, de conseil et de développement de logiciels.

En savoir plus sur ce sujet

  • Téléchargez le code source complet //www.javaworld.com/jw-01-1999/howto/jw-01-howto.zip
  • Présentation de l'API de sécurité Java //www.javasoft.com/products/jdk/1.1/docs/guide/security/JavaSecurityOverview.html
  • Architecture de cryptographie Java //www.javasoft.com/products/jdk/1.1/docs/guide/security/CryptoSpec.html
  • Page de sécurité Java de Sun //java.sun.com/security/index.html
  • FAQ de RSA sur la cryptographie //www.rsa.com/rsalabs/faq/
  • Politique et informations cryptographiques //www.crypto.com/
  • Lisez les précédentes colonnes How-To Java de Todd //www.javaworld.com/topicalindex/jw-ti-howto.html

Cette histoire, "In Java we trust" a été initialement publiée par JavaWorld.