001/*
002 * oauth2-oidc-sdk
003 *
004 * Copyright 2012-2020, Connect2id Ltd and contributors.
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use
007 * this file except in compliance with the License. You may obtain a copy of the
008 * License at
009 *
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software distributed
013 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
014 * CONDITIONS OF ANY KIND, either express or implied. See the License for the
015 * specific language governing permissions and limitations under the License.
016 */
017
018package com.nimbusds.openid.connect.sdk.federation.trust;
019
020
021import java.util.*;
022
023import com.nimbusds.jose.JOSEException;
024import com.nimbusds.jose.jwk.JWKSet;
025import com.nimbusds.jose.proc.BadJOSEException;
026import com.nimbusds.oauth2.sdk.util.MapUtils;
027import com.nimbusds.openid.connect.sdk.federation.entities.EntityID;
028import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
029import com.nimbusds.openid.connect.sdk.federation.trust.constraints.TrustChainConstraints;
030
031
032/**
033 * Trust chain resolver.
034 *
035 * <p>Related specifications:
036 *
037 * <ul>
038 *     <li>OpenID Connect Federation 1.0, section 9.
039 * </ul>
040 */
041public class TrustChainResolver {
042        
043        
044        /**
045         * The configured trust anchors with their public JWK sets.
046         */
047        private final Map<EntityID, JWKSet> trustAnchors;
048        
049        
050        /**
051         * The entity statement retriever.
052         */
053        private final EntityStatementRetriever statementRetriever;
054        
055        
056        /**
057         * The trust chain constraints.
058         */
059        private final TrustChainConstraints constraints;
060        
061        
062        /**
063         * Creates a new trust chain resolver with a single trust anchor, with
064         * {@link TrustChainConstraints#NO_CONSTRAINTS no trust chain
065         * constraints}.
066         *
067         * @param trustAnchor The trust anchor. Must not be {@code null}.
068         */
069        public TrustChainResolver(final EntityID trustAnchor) {
070                this(trustAnchor, null);
071        }
072        
073        
074        /**
075         * Creates a new trust chain resolver with a single trust anchor, with
076         * {@link TrustChainConstraints#NO_CONSTRAINTS no trust chain
077         * constraints}.
078         *
079         * @param trustAnchor       The trust anchor. Must not be {@code null}.
080         * @param trustAnchorJWKSet The trust anchor public JWK set,
081         *                          {@code null} if not available.
082         */
083        public TrustChainResolver(final EntityID trustAnchor,
084                                  final JWKSet trustAnchorJWKSet) {
085                this(
086                        Collections.singletonMap(trustAnchor, trustAnchorJWKSet),
087                        TrustChainConstraints.NO_CONSTRAINTS,
088                        new DefaultEntityStatementRetriever()
089                );
090        }
091        
092        
093        /**
094         * Creates a new trust chain resolver with multiple trust anchors, with
095         * {@link TrustChainConstraints#NO_CONSTRAINTS no trust chain
096         * constraints}.
097         *
098         * @param trustAnchors         The trust anchors with their public JWK
099         *                             sets (if available). Must contain at
100         *                             least one anchor.
101         * @param httpConnectTimeoutMs The HTTP connect timeout in
102         *                             milliseconds, zero means timeout
103         *                             determined by the underlying HTTP
104         *                             client.
105         * @param httpReadTimeoutMs    The HTTP read timeout in milliseconds,
106         *                             zero means timeout determined by the
107         *                             underlying HTTP client.
108         */
109        public TrustChainResolver(final Map<EntityID, JWKSet> trustAnchors,
110                                  final int httpConnectTimeoutMs,
111                                  final int httpReadTimeoutMs) {
112                this(
113                        trustAnchors,
114                        TrustChainConstraints.NO_CONSTRAINTS,
115                        new DefaultEntityStatementRetriever(httpConnectTimeoutMs, httpReadTimeoutMs)
116                );
117        }
118        
119        
120        /**
121         * Creates new trust chain resolver.
122         *
123         * @param trustAnchors       The trust anchors with their public JWK
124         *                           sets. Must contain at least one anchor.
125         * @param constraints        The constraints to apply during retrieval.
126         *                           Must not be {@code null}.
127         * @param statementRetriever The entity statement retriever to use.
128         *                           Must not be {@code null}.
129         */
130        public TrustChainResolver(final Map<EntityID, JWKSet> trustAnchors,
131                                  final TrustChainConstraints constraints,
132                                  final EntityStatementRetriever statementRetriever) {
133                
134                if (MapUtils.isEmpty(trustAnchors)) {
135                        throw new IllegalArgumentException("The trust anchors map must not be empty or null");
136                }
137                this.trustAnchors = trustAnchors;
138                
139                if (constraints == null) {
140                        throw new IllegalArgumentException("The trust chain constraints must not be null");
141                }
142                this.constraints = constraints;
143                
144                if (statementRetriever == null) {
145                        throw new IllegalArgumentException("The entity statement retriever must not be null");
146                }
147                this.statementRetriever = statementRetriever;
148        }
149        
150        
151        /**
152         * Returns the configured trust anchors.
153         *
154         * @return The trust anchors with their public JWK sets (if available).
155         *         Contains at least one anchor.
156         */
157        public Map<EntityID, JWKSet> getTrustAnchors() {
158                return Collections.unmodifiableMap(trustAnchors);
159        }
160        
161        
162        /**
163         * Returns the configured entity statement retriever.
164         *
165         * @return The entity statement retriever.
166         */
167        public EntityStatementRetriever getEntityStatementRetriever() {
168                return statementRetriever;
169        }
170        
171        
172        /**
173         * Returns the configured trust chain constraints.
174         *
175         * @return The constraints.
176         */
177        public TrustChainConstraints getConstraints() {
178                return constraints;
179        }
180        
181        
182        /**
183         * Resolves the trust chains for the specified target.
184         *
185         * @param target The target. Must not be {@code null}.
186         *
187         * @return The resolved trust chains, containing at least one valid and
188         *         verified chain.
189         *
190         * @throws ResolveException If no trust chain could be resolved.
191         */
192        public TrustChainSet resolveTrustChains(final EntityID target)
193                throws ResolveException {
194                
195                try {
196                        return resolveTrustChains(target, null);
197                } catch (InvalidEntityMetadataException e) {
198                        // Should never occur if target metadata validator omitted
199                        throw new IllegalStateException("Unexpected exception: " + e.getMessage(), e);
200                }
201        }
202        
203        
204        /**
205         * Resolves the trust chains for the specified target, with optional
206         * validation of the target entity metadata. The validator can for
207         * example check that for an entity which is expected to be an OpenID
208         * relying party the required party metadata is present.
209         *
210         * @param target                  The target. Must not be {@code null}.
211         * @param targetMetadataValidator To perform optional validation of the
212         *                                retrieved target entity metadata,
213         *                                before proceeding with retrieving the
214         *                                entity statements from the
215         *                                authorities, {@code null} if not
216         *                                specified.
217         *
218         * @return The resolved trust chains, containing at least one valid and
219         *         verified chain.
220         *
221         * @throws ResolveException               If a trust chain could not be
222         *                                        resolved.
223         * @throws InvalidEntityMetadataException If the optional target entity
224         *                                        metadata validation didn't
225         *                                        pass.
226         */
227        public TrustChainSet resolveTrustChains(final EntityID target,
228                                                final EntityMetadataValidator targetMetadataValidator)
229                throws ResolveException, InvalidEntityMetadataException {
230                
231                if (trustAnchors.get(target) != null) {
232                        throw new ResolveException("Target is trust anchor");
233                }
234                
235                TrustChainRetriever retriever = new DefaultTrustChainRetriever(statementRetriever, constraints);
236                Set<TrustChain> fetchedTrustChains = retriever.retrieve(target, targetMetadataValidator, trustAnchors.keySet());
237                return verifyTrustChains(
238                        fetchedTrustChains,
239                        retriever.getAccumulatedTrustAnchorJWKSets(),
240                        retriever.getAccumulatedExceptions());
241        }
242        
243        
244        /**
245         * Resolves the trust chains for the specified target.
246         *
247         * @param targetStatement The target entity statement. Must not be
248         *                        {@code null}.
249         *
250         * @return The resolved trust chains, containing at least one valid and
251         *         verified chain.
252         *
253         * @throws ResolveException If no trust chain could be resolved.
254         */
255        public TrustChainSet resolveTrustChains(final EntityStatement targetStatement)
256                throws ResolveException {
257                
258                if (trustAnchors.get(targetStatement.getEntityID()) != null) {
259                        throw new ResolveException("Target is trust anchor");
260                }
261                
262                TrustChainRetriever retriever = new DefaultTrustChainRetriever(statementRetriever, constraints);
263                Set<TrustChain> fetchedTrustChains = retriever.retrieve(targetStatement, trustAnchors.keySet());
264                return verifyTrustChains(
265                        fetchedTrustChains,
266                        retriever.getAccumulatedTrustAnchorJWKSets(),
267                        retriever.getAccumulatedExceptions());
268        }
269        
270        
271        /**
272         * Verifies the specified fetched trust chains.
273         *
274         * @param fetchedTrustChains            The fetched trust chains. Must
275         *                                      not be {@code null},
276         * @param accumulatedTrustAnchorJWKSets The accumulated trust anchor(s)
277         *                                      JWK sets, empty if none. Must
278         *                                      not be {@code null}.
279         * @param accumulatedExceptions         The accumulated exceptions,
280         *                                      empty if none. Must not be
281         *                                      {@code null}.
282         * @return The verified trust chain set.
283         *
284         * @throws ResolveException If no trust chain could be verified.
285         */
286        private TrustChainSet verifyTrustChains(final Set<TrustChain> fetchedTrustChains,
287                                                final Map<EntityID, JWKSet> accumulatedTrustAnchorJWKSets,
288                                                final List<Throwable> accumulatedExceptions)
289                throws ResolveException {
290                
291                if (fetchedTrustChains.isEmpty()) {
292                        if (accumulatedExceptions.isEmpty()) {
293                                throw new ResolveException("No trust chain leading up to a trust anchor");
294                        } else if (accumulatedExceptions.size() == 1){
295                                Throwable cause = accumulatedExceptions.get(0);
296                                throw new ResolveException("Couldn't resolve trust chain: " + cause.getMessage(), cause);
297                        } else {
298                                throw new ResolveException("Couldn't resolve trust chain due to multiple causes", accumulatedExceptions);
299                        }
300                }
301                
302                List<Throwable> verificationExceptions = new LinkedList<>();
303                
304                TrustChainSet verifiedTrustChains = new TrustChainSet();
305                
306                for (TrustChain chain: fetchedTrustChains) {
307                        
308                        EntityID anchor = chain.getTrustAnchorEntityID();
309                        JWKSet anchorJWKSet = trustAnchors.get(anchor);
310                        if (anchorJWKSet == null) {
311                                anchorJWKSet = accumulatedTrustAnchorJWKSets.get(anchor);
312                        }
313                        
314                        try {
315                                chain.verifySignatures(anchorJWKSet);
316                        } catch (BadJOSEException | JOSEException e) {
317                                verificationExceptions.add(e);
318                                continue;
319                        }
320                        
321                        verifiedTrustChains.add(chain);
322                }
323                
324                if (verifiedTrustChains.isEmpty()) {
325                        
326                        List<Throwable> moreAccumulatedExceptions = new LinkedList<>(accumulatedExceptions);
327                        moreAccumulatedExceptions.addAll(verificationExceptions);
328                        
329                        if (verificationExceptions.size() == 1) {
330                                Throwable cause = verificationExceptions.get(0);
331                                throw new ResolveException("Couldn't resolve trust chain: " + cause.getMessage(), moreAccumulatedExceptions);
332                        } else {
333                                throw new ResolveException("Couldn't resolve trust chain due to multiple causes", moreAccumulatedExceptions);
334                        }
335                }
336                
337                return verifiedTrustChains;
338        }
339}