WS-Security et Apache CXF

Lundi 27 Février 2017

Cette page est à utiliser comme un wiki, elle explique les bases de la sécurité WS-Security, écrite par retour d’expérience de 3 mois sur le sujet. Contexte: architecture SOA avec micro-services, sécuriser les échanges SOAP (identifier l’émetteur, vérifier la non répudiation, non altération, chiffrer les messages…). Cette page se focalise sur les grands principes et montre quelques exemples d’implémentation en les vulgarisant.

Un peu de vocabulaire

Un peu de vocabulaire rencontré ici et là.

  • Signature: signing a message is used to validate to the recipient that the message could only have come from a certain sender, and that the message was not altered in transit. It involves the sender encrypting a digest (hash) of the message with its private key, and the recipient decrypting the hash with the sender’s public key, and recalculating the digest of the message to make sure the message was not altered in transit (i.e., that the digest values calculated by both the sender and recipient are the same). For this process to occur you must ensure that the Client’s public key has been imported into the server’s keystore using keytool. http://cxf.apache.org/docs/ws-security.html#WS-Security-Signing
  • WSP: Web Service Provider: désigne le serveur qui fournit le service métier sécurisé
  • STS: Security Token Service, c’est un service qui donne des jetons d’authentification SAML (le bloc Security en gros). Le client peut aussi générer ses jetons lui-même (cf ci-dessous)
  • WSC: Web Service Client: un client. Ce client est le consommateur du service sécurisé. Il doit signer ses requêtes ou le faire auprès d’un STS
  • OASIS SAML (Security Assertion Markup Language): cette spécification décrit comment intégrer les assertions SAML (Security Assertion Markp Language) dans les jetons de sécurité. Il s’agit d’un framework XML dédié à l’échange d’informations d’authentification et d’autorisation. Tout ce qui concerne SAML est implémenté à l’intérieur de la balise <Assertion> elle-même placée à la racine de la balise <wsse:security>
  • X.509: cette spécification décrit comment intégrer des certificats X.509 dans les jetons de sécurité WSS-Core. Le certificat est obtenu auprès d’une autorité de certification
  • WSS4J: APIs Java de validation/génération de signatures et d’assertions SAML, basée entre autres sur opensaml
  • CXF: Apache CXF est un framework web service, il effectue (via WSS4J) pour nous toutes les vérifications de signatures et d’assertions SAML: à partir du moment où l’intercepteur WSS4J est branché sur le endpoint et où le tag Security est présent dans la requête/réponse, de façon automatique pour le développeur. CXF peut aussi signer les flux sortants. WSS4J est dépendance de CXF, de même que saml, donc laisser CXF décider de la version des APIs. Avec Maven par exemple, toujours étudier l’arbre de dépendances avant de commencer?!

Générer une paire de keystores via Java

Avant de parler de signature, il faut savoir ce qu’est un certificat et un keystore. Je vous laisse vous informer sur ce sujet. Le script suivant permet de créer 2 keystores qui pourront être utilisés par un WSP et un WSC: un pour le client (privé) l’autre pour le serveur (publique). Ce script peut-être être adapté pour Windows ou d’autres versions de Java… Testé sur Java 6.


cd /tmp
mkdir test
cd test/
keytool -genkeypair -alias myAlias -keypass myAliasPassword -keystore privatestore.jks -storepass keyStorePassword -dname "cn=myAlias" -keyalg RSA
keytool -exportcert -alias myAlias -file key.rsa -keystore privatestore.jks -storepass keyStorePassword
keytool -importcert -alias myAlias -file key.rsa -keystore publicstore.jks -storepass keyStorePassword

Ce script génère une clé privée et une clé publique (via -genkeypair) de type RSA (-keyalg) en prenant soin de mettre la clé publique dans un certificat auto-signé X-509 v3, puis donne un nom d’alias au certificat (-alias), et stocke tout ça dans un nouveau fichier keystore (-privatestore). La clé et le keystore ont tous 2 leur mot de passe associé (optionnels).

Ensuite, le script extrait du keystore le certificat précédemment créé et l’enregistre dans un fichier key.rsa (-file).

Enfin Il crée un nouveau keystore (publicstore.jks) dans lequel il importe ce certificat.

Pour affiche le contenu d’un fichier jks on peut utiliser?:

keytool -list -keystore privatestore.jks -storepass keyStorePassword -v

Signature de Flux

Grands principes

Il faut bien comprendre que le client signe la(les) partie(s) du flux qui l’intéresse(nt) via des "References". Le tag "Signature" comporte la liste de ces références, qui ne sont ni plus ni moins que des clés étrangères vers les tags xml (les balises) qu’on veut signer (cf attributs "ID" sur tous les tags signés). On peut ainsi signer le Body et/ou le Header, ou seulement un sous-tag si on le souhaite. La plupart des implémentations signent par défaut le Body.

Si on part de cet exemple: https://www.w3.org/TR/SOAP-dsig/#ex

<SOAP-ENV:Envelope
    xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
    <SOAP-ENV:Header>
        <SOAP-SEC:Signature
            xmlns:SOAP-SEC="http://schemas.xmlsoap.org/soap/security/2000-12"
      SOAP-ENV:actor="some-URI"
      SOAP-ENV:mustUnderstand="1">
            <ds:Signature
                xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
                <ds:SignedInfo>
                    <ds:CanonicalizationMethod   
            Algorithm="http://www.w3.org/TR/2000/CR-xml-c14n-20001026"></ds:CanonicalizationMethod>
                    <ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#dsa-sha1"/>
                    <ds:Reference URI="#Body">
                        <ds:Transforms>
                            <ds:Transform Algorithm="http://www.w3.org/TR/2000/CR-xml-c14n-20001026"/>
                        </ds:Transforms>
                        <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
                        <ds:DigestValue>j6lwx3rvEPO0vKtMup4NbeVu8nk=</ds:DigestValue>
                    </ds:Reference>
                </ds:SignedInfo>
                <ds:SignatureValue>MC0CFFrVLtRlk=...</ds:SignatureValue>
            </ds:Signature>
        </SOAP-SEC:Signature>
    </SOAP-ENV:Header>
    <SOAP-ENV:Body
        xmlns:SOAP-SEC="http://schemas.xmlsoap.org/soap/security/2000-12"
    SOAP-SEC:id="Body">
        <m:GetLastTradePrice
            xmlns:m="some-URI">
            <m:symbol>IBM</m:symbol>
        </m:GetLastTradePrice>
    </SOAP-ENV:Body>
</SOAP-ENV:Envelope>
Vulgarisons
  • les References disent ce qui est signé, via l’attribut "URI".
  • la DigestValue (ou digest) est l’empreinte (le hash) du contenu référencé, elle est utilisée pour calculer la signature
  • l’algorithme de hashage de la Réference est défini dans le tag DigestMethod
  • les références ont donc chacune une DigestValue qui contient le résultat de l’algorithme de hashage appliqué au contenu à signer
  • le tag CanonicalizationMethod (c14n ) dit comment doivent être lues les References lors du calcul de la DigestValue, c’est via ce tag qu’on décide si la DigestValue (donc la signature) dépend du "formatage" du contenu signé (espaces/sauts de lignes/préfixes des namespaces, commentaires XML…). Ce qui veut dire que la signature du contenu signé peut dépendre de ces éléments. Le mieux est peut-être de paramétrer le c14n pour que la DigestValue ne dépende pas du formatage du document. Malheureusement jusqu’ici je n’ai pas trouvé d’algorithme de c14n standard qui formate le flux d’une manière avancée et assez intelligente pour ne pas se soucier du format … Donc j’ai utilisé un les algorithmes c14n par défaut. Cela peut être gênant quand on doit redistribuer un flux signé à un autre destinataire (requête ou réponse). Dans ce cas on doit faire attention à ne pas reformater le flux. Cela peut être fait par exemple en envoyant le stream du client vers le destinataire via un autre intercepteur (à coder soi-même), mais cela est une autre histoire!
  • la signatureValue est le résultat du cryptage des DigestValue via la clé privée du certificat du client.
  • le tag KeyInfo contient les informations (publiques) sur le certificat (du client) qui a permis de générer la SignatureValue.
  • si le tag X509Certificate est renseigné, il contient le résultat de la fonction Base64 appliquée sur son fichier .cer du client. Bien sûr le client ne communique pas sa clé privée au serveur, seule la clé publique est dans le fichier .cer, accompagnée de plusieurs informations de contexte.

Toutes ces informations permettent au destinataire du message de valider la signature, pour cela il calcule à son tour les hashs de chaque référence puis les crypte à l’aide de son certificat (le certificat public), et compare la valeur obtenue avec la SignatureValue. Si ces 2 valeurs sont égales, cela signifie que les tags signés n’ont pas été altérés et que les certificats sont cohérents. Le serveur peut effectuer des contrôles supplémentaires s’il le souhaite. Pour le besoin de mon projet, nous avons surchargé les intercepteurs WSS4J embarqués par CXF afin de contrôler les algorithmes utilisés pour signer et comparer le certificat avec une base de certificats.

Et SAML??

L’assertion (ou jeton) SAML n’est autre qu’un tag xml qui porte des infos sur le client, ni plus ni moins. C’est une norme OASIS universelle. L’assertion SAML peut être signée de la même manière que les autres tags du document. Dans ce cas la signature est dite "enveloppée dans le tag SAML", elle n’est évidement pas signée elle-même (cela est paramétré par un tag Transform). La ConfirmationMethod est importante, cf http://coheigea.blogspot.fr/2014/11/security-semantics-of-saml.html. Le plus simple est d’utiliser "BEARER". Mais ReadyAPI apporte peu de support sur ce paramétrage dans les requêtes SOAP…

Pas mal d’outils sur SAML ici: https://www.samltool.com/

Outils

Les outils que j’ai utilisés pour signer et vérifier des signatures et des assertions SAML sont SoapUI/ReadyAPI et Apache CXF (pour la partie Java). Pour signer avec SoapUI c’est assez simple, lire la doc ici: https://www.soapui.org/soapui-projects/ws-security.html

J’ai utilisé Apache CXF 2.7 sur Java 6, ainsi que CXF 3.1.10 sur Java 8. Je n’ai pas rencontré de problème majeur lors de la migration vers Java 8, hormis quelques classes qui ont changé de nom ou de package (dû aux dépendances Maven transitives WSS4J et openSAML qui ont bougé). Toujours utiliser un intercepteur WSS4J, car c’est mieux de réutiliser. Le code suivant effectue une signature de requête SOAP avec le WSS4JOutInterceptor, par défaut le Body est signé, l’action est réglée sur SIGNATURE, ce qui veut dire que seule la signature est effectuée et insérée dans la requête.

Côté client (code CXF 2.7.18)
Bus bus = BusFactory.newInstance().createBus();
try {
    BusFactory.setThreadDefaultBus( bus );
    final String serviceURL = "http://someService";
    final QName serviceName = new QName( "", "DoubleItService" );
    final QName portName = new QName( "", "DoubleItPort" );
    final URL wsdlURL = new URL( serviceURL );
    Service service = Service.create( wsdlURL, serviceName );
    DoubleItPortType proxy = service.getPort( portName, DoubleItPortType.class );
    Client client = ClientProxy.getClient( proxy );
    client.getInInterceptors().add( new LoggingInInterceptor() );
    client.getOutInterceptors().add( new LoggingOutInterceptor() );
    Map<String, Object> outProps = new HashMap<String, Object>();
    WSS4JOutInterceptor wssOut = new WSS4JOutInterceptor( outProps );
    client.getOutInterceptors().add( wssOut );
    outProps.put( WSHandlerConstants.USER, "myAlias" );
    // outProps.put( WSHandlerConstants.MUST_UNDERSTAND, "false" );
    outProps.put( WSHandlerConstants.PW_CALLBACK_CLASS, CustomClientCallbackHandler.class.getName() );
    outProps.put( WSHandlerConstants.SIG_PROP_FILE, "clientKeystore.properties" );
    outProps.put( WSHandlerConstants.ACTION, WSHandlerConstants.SIGNATURE );
    System.out.println( "Result " + proxy.doubleIt( 25 ) );
}
catch ( Exception e ) {
    e.printStackTrace();
}
finally {
    bus.shutdown( true );
}

Note: il est possible d’injecter l’intercepteur via Spring… De plus le mécanisme de callBack pour le mot de passe est inutile si le keystore n’est pas sécurisé par mot de passe. Exemple de fichier clientKeystore.properties, cf https://ws.apache.org/wss4j/config.html)

Côté serveur

Voici un endpoint configuré par Spring qui injecte l’intercepteur WSS4J de validation de la signature:

<jaxws:endpoint id="epWSDemo" implementor="com.demo.WSDemo" address="/services/WSDemo">
         <jaxws:inInterceptors>
            <bean class="com.demo.WSS4JDemoInInterceptor">
                <property name="properties">
                    <map>
                        <entry key="action" value="SAMLTokenSigned Signature" />
                        <entry key="signaturePropFile" value="serviceKeystore.properties"/>
                    </map>
                </property>
            </bean>
        </jaxws:inInterceptors>
           <jaxws:binding>
               <soap:soapBinding version="1.2" />
           </jaxws:binding>
           <jaxws:properties>
               <entry key="schema-validation-enabled" value="true" />
           </jaxws:properties>
</jaxws:endpoint>

Les actions (séparées par un espace) définissent quels éléments de sécurité sont attendus et contrôlés par WSS4J, et quels éléments le client a le droit d’envoyer au serveur. Dans cet exemple on vérifie que le Body est signé (Signature) et qu’il y a bien une assertion SAML signée (SAMLTokenSigned).

CXF vérifie les actions 1 à 1 et à la moindre différence (par exemple si l’assertion SAML n’est pas signée) il throws une erreur. La liste des actions disponibles pour CXF 2.7.18 sont ici: https://svn.apache.org/repos/asf/webservices/wss4j/tags/wss4j-1.6.19/src/main/java/org/apache/ws/security/handler/WSHandlerConstants.java.

Documentation CXF en ligne: http://cxf.apache.org/docs/ws-security.html Attention la partie configuration est mal documentée sur le site d’Apache?!


En espérant que cela servira à d’autres

Partagez sur les réseaux sociaux

Catégorie

Commentaires :

Laisser un commentaire
Aucun commentaire n'a été laissé pour le moment... Soyez le premier !
 



Créer un site
Créer un site