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 7.
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 timout 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 statementRetriever The entity statement retriever to use.
126         *                           Must not be {@code null}.
127         */
128        public TrustChainResolver(final Map<EntityID, JWKSet> trustAnchors,
129                                  final TrustChainConstraints constraints,
130                                  final EntityStatementRetriever statementRetriever) {
131                
132                if (MapUtils.isEmpty(trustAnchors)) {
133                        throw new IllegalArgumentException("The trust anchors map must not be empty or null");
134                }
135                this.trustAnchors = trustAnchors;
136                
137                if (constraints == null) {
138                        throw new IllegalArgumentException("The trust chain constraints must not be null");
139                }
140                this.constraints = constraints;
141                
142                if (statementRetriever == null) {
143                        throw new IllegalArgumentException("The entity statement retriever must not be null");
144                }
145                this.statementRetriever = statementRetriever;
146        }
147        
148        
149        /**
150         * Returns the configured trust anchors.
151         *
152         * @return The trust anchors with their public JWK sets (if available).
153         *         Contains at least one anchor.
154         */
155        public Map<EntityID, JWKSet> getTrustAnchors() {
156                return Collections.unmodifiableMap(trustAnchors);
157        }
158        
159        
160        /**
161         * Returns the configured entity statement retriever.
162         *
163         * @return The entity statement retriever.
164         */
165        public EntityStatementRetriever getEntityStatementRetriever() {
166                return statementRetriever;
167        }
168        
169        
170        /**
171         * Returns the configured trust chain constraints.
172         *
173         * @return The constraints.
174         */
175        public TrustChainConstraints getConstraints() {
176                return constraints;
177        }
178        
179        
180        /**
181         * Resolves the trust chains for the specified target.
182         *
183         * @param target The target. Must not be {@code null}.
184         *
185         * @return The resolved trust chains, containing at least one valid and
186         *         verified chain.
187         *
188         * @throws ResolveException If no trust chain could be resolved.
189         */
190        public TrustChainSet resolveTrustChains(final EntityID target)
191                throws ResolveException {
192                
193                try {
194                        return resolveTrustChains(target, null);
195                } catch (InvalidEntityMetadataException e) {
196                        // Should never occur if target metadata validator omitted
197                        throw new IllegalStateException("Unexpected exception: " + e.getMessage(), e);
198                }
199        }
200        
201        
202        /**
203         * Resolves the trust chains for the specified target, with optional
204         * validation of the target entity metadata. The validator can for
205         * example check that for an entity which is expected to be an OpenID
206         * relying party the required party metadata is present.
207         *
208         * @param target                  The target. Must not be {@code null}.
209         * @param targetMetadataValidator To perform optional validation of the
210         *                                retrieved target entity metadata,
211         *                                before proceeding with retrieving the
212         *                                entity statements from the
213         *                                authorities, {@code null} if not
214         *                                specified.
215         *
216         * @return The resolved trust chains, containing at least one valid and
217         *         verified chain.
218         *
219         * @throws ResolveException If no trust chain could be resolved.
220         */
221        public TrustChainSet resolveTrustChains(final EntityID target,
222                                                final EntityMetadataValidator targetMetadataValidator)
223                throws ResolveException, InvalidEntityMetadataException {
224                
225                if (trustAnchors.get(target) != null) {
226                        throw new ResolveException("Target is trust anchor");
227                }
228                
229                TrustChainRetriever retriever = new DefaultTrustChainRetriever(statementRetriever, constraints);
230                Set<TrustChain> fetchedTrustChains = retriever.retrieve(target, targetMetadataValidator, trustAnchors.keySet());
231                return verifyTrustChains(
232                        fetchedTrustChains,
233                        retriever.getAccumulatedTrustAnchorJWKSets(),
234                        retriever.getAccumulatedExceptions());
235        }
236        
237        
238        /**
239         * Resolves the trust chains for the specified target.
240         *
241         * @param targetStatement The target entity statement. Must not be
242         *                        {@code null}.
243         *
244         * @return The resolved trust chains, containing at least one valid and
245         *         verified chain.
246         *
247         * @throws ResolveException If no trust chain could be resolved.
248         */
249        public TrustChainSet resolveTrustChains(final EntityStatement targetStatement)
250                throws ResolveException {
251                
252                if (trustAnchors.get(targetStatement.getEntityID()) != null) {
253                        throw new ResolveException("Target is trust anchor");
254                }
255                
256                TrustChainRetriever retriever = new DefaultTrustChainRetriever(statementRetriever, constraints);
257                Set<TrustChain> fetchedTrustChains = retriever.retrieve(targetStatement, trustAnchors.keySet());
258                return verifyTrustChains(
259                        fetchedTrustChains,
260                        retriever.getAccumulatedTrustAnchorJWKSets(),
261                        retriever.getAccumulatedExceptions());
262        }
263        
264        
265        /**
266         * Verifies the specified fetched trust chains.
267         *
268         * @param fetchedTrustChains            The fetched trust chains. Must
269         *                                      not be {@code null},
270         * @param accumulatedTrustAnchorJWKSets The accumulated trust anchor(s)
271         *                                      JWK sets, empty if none. Must
272         *                                      not be {@code null}.
273         * @param accumulatedExceptions         The accumulated exceptions,
274         *                                      empty if none. Must not be
275         *                                      {@code null}.
276         * @return The verified trust chain set.
277         *
278         * @throws ResolveException If no trust chain could be verified.
279         */
280        private TrustChainSet verifyTrustChains(final Set<TrustChain> fetchedTrustChains,
281                                                final Map<EntityID, JWKSet> accumulatedTrustAnchorJWKSets,
282                                                final List<Throwable> accumulatedExceptions)
283                throws ResolveException {
284                
285                if (fetchedTrustChains.isEmpty()) {
286                        if (accumulatedExceptions.isEmpty()) {
287                                throw new ResolveException("No trust chain leading up to a trust anchor");
288                        } else if (accumulatedExceptions.size() == 1){
289                                Throwable cause = accumulatedExceptions.get(0);
290                                throw new ResolveException("Couldn't resolve trust chain: " + cause.getMessage(), cause);
291                        } else {
292                                throw new ResolveException("Couldn't resolve trust chain due to multiple causes", accumulatedExceptions);
293                        }
294                }
295                
296                List<Throwable> verificationExceptions = new LinkedList<>();
297                
298                TrustChainSet verifiedTrustChains = new TrustChainSet();
299                
300                for (TrustChain chain: fetchedTrustChains) {
301                        
302                        EntityID anchor = chain.getTrustAnchorEntityID();
303                        JWKSet anchorJWKSet = trustAnchors.get(anchor);
304                        if (anchorJWKSet == null) {
305                                anchorJWKSet = accumulatedTrustAnchorJWKSets.get(anchor);
306                        }
307                        
308                        try {
309                                chain.verifySignatures(anchorJWKSet);
310                        } catch (BadJOSEException | JOSEException e) {
311                                verificationExceptions.add(e);
312                                continue;
313                        }
314                        
315                        verifiedTrustChains.add(chain);
316                }
317                
318                if (verifiedTrustChains.isEmpty()) {
319                        
320                        List<Throwable> moreAccumulatedExceptions = new LinkedList<>(accumulatedExceptions);
321                        moreAccumulatedExceptions.addAll(verificationExceptions);
322                        
323                        if (verificationExceptions.size() == 1) {
324                                Throwable cause = verificationExceptions.get(0);
325                                throw new ResolveException("Couldn't resolve trust chain: " + cause.getMessage(), moreAccumulatedExceptions);
326                        } else {
327                                throw new ResolveException("Couldn't resolve trust chain due to multiple causes", moreAccumulatedExceptions);
328                        }
329                }
330                
331                return verifiedTrustChains;
332        }
333}