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 rencontré ici et là.
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
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> |
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.
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/
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.
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)
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