Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RelyingPartyRegistrations does not verify signatures #15018

Closed
OrangeDog opened this issue May 7, 2024 · 4 comments
Closed

RelyingPartyRegistrations does not verify signatures #15018

OrangeDog opened this issue May 7, 2024 · 4 comments
Assignees
Labels
in: saml2 An issue in SAML2 modules status: duplicate A duplicate of another issue type: bug A general bug

Comments

@OrangeDog
Copy link
Contributor

OrangeDog commented May 7, 2024

Describe the bug
When an EntitiesDescriptor, EntityDescriptor, or IDPSSODescriptor is being parsed, any included Signatures must be verified against a supplied trust store. This is especially critical if the metadata is fetched over an insecure channel. However, the methods in RelyingPartyRegistrations do not do this. Nor is it clear how someone might do it after the fact.

The underlying OpenSaml library provides all the necessary functionality. Spring Security just needs to call it. The now unsupported spring-security-saml does this correctly.

To Reproduce
The current API does not provide any way to provide trust material, even if it tried to verify signatures.

Expected behavior
See above.

Additional Information
This may qualify as a security issue, if people were expecting the previous behaviour of SAML metadata being cryptographically verified.

@OrangeDog OrangeDog added status: waiting-for-triage An issue we've not yet triaged type: bug A general bug labels May 7, 2024
@OrangeDog
Copy link
Contributor Author

Nor is it clear how someone might do it after the fact.

This I think does a good enough job, at least for me, but may have some flaws. I have no mapping of which metadata sources are expected to be using which signing certificates, so it just trusts all certs for all sources.

It will need some additional logic if some sources (e.g. https/local) should be trusted without signatures.
I'm also not sure in what situation you would be trusting signed RoleDescriptors but not their EntityDescriptor, so it skips checking those.

// Configure trust engine
Collection<Credential> certificates = new ArrayList<>();
keyStore.aliases().asIterator().forEachRemaining(alias -> {
    try {
        if (keyStore.isCertificateEntry(alias)) {
            certificates.add(new BasicX509Credential((X509Certificate) keyStore.getCertificate(alias)));
        }
    } catch (KeyStoreException | ClassCastException ex) {
        throw new RuntimeException(ex);
    }
});
CredentialResolver credentialResolver = new CollectionCredentialResolver(certificates);
KeyInfoCredentialResolver keyInfoResolver = DefaultSecurityConfigurationBootstrap
        .buildBasicInlineKeyInfoCredentialResolver();
SignatureTrustEngine trustEngine = new ExplicitKeySignatureTrustEngine(credentialResolver, keyInfoResolver);
SAMLSignatureProfileValidator profileValidator = new SAMLSignatureProfileValidator();

// Collect signatures from the metadata
Set<Signature> signatures = new HashSet<>();
for (RelyingPartyRegistration reg : relyingPartyRegistrationRepository) {
    if (reg.getAssertingPartyDetails() instanceof OpenSamlAssertingPartyDetails details) {
        EntityDescriptor descriptor = details.getEntityDescriptor();
        if (descriptor.hasParent() && descriptor.getParent() instanceof EntitiesDescriptor parent) {
            if (parent.isSigned()) {
                signatures.add(parent.getSignature());
            } else {
                throw new Saml2Exception("EntitiesDescriptor is not signed for " + descriptor.getEntityID());
            }
        } else if (descriptor.isSigned()) {
            signatures.add(descriptor.getSignature());
        } else {
            throw new Saml2Exception("EntityDescriptor is not signed for " + descriptor.getEntityID());
        }
    } else {
        throw new Saml2Exception("Cannot verify " + reg.getAssertingPartyDetails().getEntityId());
    }
}

// Verify all signatures
X509CertSelector certSelector = new X509CertSelector();
certSelector.setCertificateValid(Date.from(Instant.now()));
CriteriaSet criteria = new CriteriaSet(new EvaluableX509CertSelectorCredentialCriterion(certSelector));
for (Signature signature : signatures) {
    boolean verified;
    try {
        profileValidator.validate(signature);
        verified = trustEngine.validate(signature, criteria);
    } catch (SignatureException ex) {
        throw new Saml2Exception("Metadata validation failed", ex);
    }
    if (!verified) {
        throw new Saml2Exception("Metadata validation failed");
    }
}

@OrangeDog
Copy link
Contributor Author

Indeed, there's a big warning in the OpenSAML documentation against the design of spring-security-saml-provider:

It is very dangerous to attempt to use parts of the library in isolation without making use of all of its relevant components. In particular, implementing your own XML processing code, using XML parsing classes other than the ParserPool components provided by the library, using your own security processing code, omitting proper support for SAML metadata, etc. are all risky choices that may lead to security flaws and incomplete, unsafe, and ill-advised SAML solutions. The Shibboleth Project discourages such approaches in the strongest possible terms. Use all of it that applies to the task at hand, or use none of it.

@sjohnr sjohnr added the in: saml2 An issue in SAML2 modules label May 9, 2024
@OrangeDog
Copy link
Contributor Author

A safer approach is to have OpenSAML do the fetch and validation together, as the previous extension did.

Some example configuration:

@Bean(initMethod = "initialize", destroyMethod = "destroy")
public BasicParserPool parserPool() {
    return new BasicParserPool();
}

@Bean
public HttpClient httpClient() {
    return new net.shibboleth.utilities.java.support.httpclient.HttpClientBuilder().buildClient();
}

@Bean
public MetadataFilter metadataFilter(SignatureTrustEngine trustEngine) {
    MetadataFilter roleFilter = new EntityRoleFilter(List.of(IDPSSODescriptor.DEFAULT_ELEMENT_NAME));
    SignatureValidationFilter validationFilter = new SignatureValidationFilter(trustEngine);
    validationFilter.setRequireSignedRoot(METADATA_URI.getScheme().equals("http"));
    validationFilter.setSignaturePrevalidator(new SAMLSignatureProfileValidator());
    MetadataFilterChain filterChain = new MetadataFilterChain();
    filterChain.setFilters(List.of(validationFilter, roleFilter));
    return filterChain;
}

@Bean(initMethod = "initialize", destroyMethod = "destroy")
public HTTPMetadataResolver metadataResolver(HttpClient httpClient) {
    HTTPMetadataResolver resolver = new HTTPMetadataResolver(httpClient, METADATA_URI.toString());
    resolver.setId("default");
    resolver.setRequireValidMetadata(true);
    return resolver;
}

You then need an adaptor to expose the resulting EntityDescriptors to Spring Security.

@jzheaux jzheaux added status: duplicate A duplicate of another issue and removed status: waiting-for-triage An issue we've not yet triaged labels May 17, 2024
@jzheaux
Copy link
Contributor

jzheaux commented May 17, 2024

Thanks for the report and the suggested changes. Since this is connected with an earlier ticket, I'll close this as a duplicate and continue from there.

@jzheaux jzheaux closed this as completed May 17, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: saml2 An issue in SAML2 modules status: duplicate A duplicate of another issue type: bug A general bug
Projects
None yet
Development

No branches or pull requests

3 participants