Signing and verifying signatures with OpenSAML 4

One of the most important things in SAML communication is signing. Digital signatures allow the receiver of a message to be sure that the message has not changed since it was sent by the sender. If not for digital signatures someone in the middle of the SAML communication could for example change what user was authenticated in the SAML assertion from the IdP.

Digital signatures are also used to authenticate the sender sending a message.

Getting credentials for signing and verification

In the examples below we use credentials, being keys and certificates used for signing and verification. Lets first have a look on ways to get the credentials.

In OpenSAML 4 and other versions of OpenSAML, credentials are read using credential resolvers that resolve credentials from a source based on defined criterion. There are different credential resolvers that fetch credentials from different places. Two popular resolvers are KeyStoreCredentialResolver and MetadataCredentialResolver.

KeyStoreCredentialResolver

KeyStoreCredentialResolver resolves credentials from a Java keystore file. The KeyStoreCredentialResolver takes a KeyStore object and a map containing the passwords for the entries in the key store.

First read the create a KeyStore object from a Java key store file.

1KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
2InputStream inputStream = this.getClass().getResourceAsStream(pathToKeyStore);
3keystore.load(inputStream, keyStore	Password.toCharArray());
4inputStream.close();

Create KeyStoreCredentialResolver and resolve the a credential based on criteria

1Map<String, String> passwordMap = new HashMap<String, String>();
2passwordMap.put(ENTITY_ID, KEY_STORE_ENTRY_PASSWORD);
3
4KeyStoreCredentialResolver resolver = new KeyStoreCredentialResolver(keystore, passwordMap);
5
6Criterion criterion = new EntityIdCriterion(ENTITY_ID);
7CriteriaSet criteriaSet = new CriteriaSet();
8criteriaSet.add(criterion);
9Credential cred = resolver.resolveSingle(criteriaSet);

The criteria used above represents the entity id of the party that we are fetching credentials for. The resolver will fetch entries in the JKS with the same name as the entity id.

In the examples above the following command was used to produce the keystore.

1keytool -keystore senderKeystore.jks -genkey -keyalg RSA -alias sender.example.com

MetadataCredentialResolver

Another popular resolver is the MetadataCredentialResolver. This resolved public keys as credentials from SAML Metadata.

First create and initialize the FilesystemMetadataResolver to get the metadata file.

1File metadataFile = new File(getClass().getClassLoader().getResource(SENDER_METADATA_PATH).toURI());
2
3final FilesystemMetadataResolver metadataResolver = new  FilesystemMetadataResolver(metadataFile);
4metadataResolver.setId(metadataResolver.getClass().getCanonicalName());
5metadataResolver.setParserPool(OpenSAMLUtils.getParserPool());
6metadataResolver.initialize();

Create and init MetadataCredentialResolver. A RoleDescriptorResolver and KeyInfoCredentialResolver is added to find the right entity id in the metadata file and extract the keys.

 1final MetadataCredentialResolver metadataCredentialResolver = new MetadataCredentialResolver();
 2
 3final PredicateRoleDescriptorResolver roleResolver = new PredicateRoleDescriptorResolver(metadataResolver);
 4
 5final KeyInfoCredentialResolver keyResolver = DefaultSecurityConfigurationBootstrap .buildBasicInlineKeyInfoCredentialResolver();
 6
 7metadataCredentialResolver.setKeyInfoCredentialResolver(keyResolver);
 8metadataCredentialResolver.setRoleDescriptorResolver(roleResolver);
 9
10metadataCredentialResolver.initialize();
11roleResolver.initialize();

Finally, resolve the public key from metadata using criterion.

1CriteriaSet criteriaSet = new CriteriaSet();
2criteriaSet.add(new UsageCriterion(UsageType.SIGNING));
3criteriaSet.add(new EntityRoleCriterion(SPSSODescriptor.DEFAULT_ELEMENT_NAME));
4criteriaSet.add(new ProtocolCriterion(SAMLConstants.SAML20P_NS));
5criteriaSet.add(new EntityIdCriterion(SENDER_ENTITY_ID));
6
7Credential credential = metadataCredentialResolver.resolveSingle(criteriaSet);

Signing messages

OpenSAML is a very low level library forcing the developer to do a lot themselves, for example signing messages.

Message signing in OpenSAML 4 is done using message handlers. Message handlers can do a lot of the surrounding work needed before sending a message or after receiving one. This work can for example be signing and verifying digital signatures, validating time validity of a message and checking that the message was intended for the receiver.

Signing is done using the SAMLOutboundProtocolMessageSigningHandler, but It also needs information about how to sign. This information is stored in a message context SecurityParametersContext containing signature signing parameters.

First lets create the signature signing parameters

1SignatureSigningParameters signingParameters = new SignatureSigningParameters();
2signingParameters.setSigningCredential(getSenderSigningCredential());
3signingParameters.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256);
4signingParameters.setSignatureReferenceDigestMethod(SignatureConstants.ALGO_ID_DIGEST_SHA256);
5signingParameters.setSignatureCanonicalizationAlgorithm(SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS);

And add it to the message context

1MessageContext context = new MessageContext();
2context.getSubcontext(SecurityParametersContext.class, true)
3.setSignatureSigningParameters(signingParameters);

And finally sign the message using SAMLOutboundProtocolMessageSigningHandler

1SAMLOutboundProtocolMessageSigningHandler handler = new SAMLOutboundProtocolMessageSigningHandler();
2handler.setSignErrorResponses(false);
3handler.initialize();
4handler.invoke(context);

The SAMLOutboundProtocolMessageSigningHandler will calculate a signature based on the parameters in the SignatureSigningParameters and add the signature to the XML message.

Validating signatures

In OpenSAML many features are manual and dependent on that the developer uses them, this is also true for both signing and verifying signatures. If verification is not done in your use of OpenSAML, you will simply go ahead and use the message without having verified the message, leading to all kinds of security issues.

There are several ways to verify a signature in OpenSAML. The most straightforward and easy to understand is using SignatureValidator

First the signature is checked to follow certain security rules of the SAML signature format. The signature is then cryptographically verified against a credential.

1SAMLSignatureProfileValidator profileValidator = new SAMLSignatureProfileValidator();
2profileValidator.validate(message.getSignature());
3SignatureValidator.validate(message.getSignature(), credential);

This works well but involves writing extra code for different things, for example getting the certificate to use for validation

A more modern approach as introduced in OpenSAML 3 and also in OpenSAML 4 is to use message handlers.

Verifying signatures using message handler and trust engine

The SAMLProtocolMessageXMLSignatureSecurityHandler is used to verify signatures. As with the message handler for signing messages, this one also needs information in the message context saying how to verify the signature.

For signature verification in message handlers, the whole logic for verifying signatures is delegated to a trust engine which encapsulates this.

Trust engines is a more advanced way of verifying signatures that uses credential resolvers, key key info in the message and the trust engine's own logic to evaluate the signature.

There are two trust engines supplied in OpenSAML 4, ExplicitKeySignatureTrustEngine and PKIXSignatureTrustEngine. ExplicitKeySignatureTrustEngine validates a signature if the key used to sign a message is one of a number of trusted certificates.

PKIXSignatureTrustEngine is a different beast in that it does not require a key to be the exact same key that you have, but instead validates trust using PKI. Meaning, the certificate used for signing is considered trusted if it is signed by a certificate you trust. We will not dig into PKIXSignatureTrustEngine right now but focus on ExplicitKeySignatureTrustEngine.

The abstract trust engine works like this.

  1. Use the KeyInfo resolver to get the KeyInfo object from the signature. The KeyInfo object is used to tell the receiver what key was used.
  2. The trust engine tries to validate the signature using the supplied key inside the KeyInfo object.
  3. If validation was successful. The trust engine gets all credentials it trusts for this sender and validates that the key inside KeyInfor is one of these.
  4. If it is not possible to get a key from key info it tries to verify the signature using any of the trusted credentials.

Ok let's look on how to use all of this

Create the ExplicitKeySignatureTrustEngine with a MetadataCredentialResolver and the default KeyInfoCredentialResolver

1KeyInfoCredentialResolver keyInfoResolver = DefaultSecurityConfigurationBootstrap
2.buildBasicInlineKeyInfoCredentialResolver();
3ExplicitKeySignatureTrustEngine trustEngine = new ExplicitKeySignatureTrustEngine(metadataCredentialResolver, keyInfoResolver);

Set the trust engine in the SecurityParametersContext

1SignatureValidationParameters validationParameters = new SignatureValidationParameters();
2validationParameters.setSignatureTrustEngine(trustEngine);
3
4SecurityParametersContext secParamsContext = context.getSubcontext(SecurityParametersContext.class, true);
5secParamsContext.setSignatureValidationParameters(validationParameters);

Add other required message contexts and run the handler to verify the message

1SAMLPeerEntityContext peerEntityContext = context.getSubcontext(SAMLPeerEntityContext.class, true);
2peerEntityContext.setEntityId(SENDER_ENTITY_ID);
3peerEntityContext.setRole(SPSSODescriptor.DEFAULT_ELEMENT_NAME);
4
5SAMLProtocolContext protocolContext = context.getSubcontext(SAMLProtocolContext.class, true);
6protocolContext.setProtocol(SAMLConstants.SAML20P_NS);
7
8SAMLProtocolMessageXMLSignatureSecurityHandler signatureValidationHanlder = new SAMLProtocolMessageXMLSignatureSecurityHandler();
9signatureValidationHanlder.invoke(context);

!!!!!!!! WARNING !!!!!!!

It is very important to understand that the SAMLProtocolMessageXMLSignatureSecurityHandler does not throw any exception if the message is not signed only if the verification of an existing signature failed. If the message handler was able to verify a signature it writes this information to the message context and it must then be checked for example like this.

1if (!peerEntityContext.isAuthenticated()) {
2throw new SecurityException("Message not signed");
3}

Get it on Github!

The full running sample is availible on Github at https://github.com/rasmusson/OpenSAML-sample-code under opensaml-signing-and-verification

Just clone it, run it, go nuts!

1git clone https://github.com/rasmusson/OpenSAML-sample-code/
2cd opensaml-signing-and-verification
3mvn tomcat:run

All messages are printed in the console

Browse to http://localhost:8080/opensaml-signing-and-verification/senderPage