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 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 a trust chain could not be
220         *                                        resolved.
221         * @throws InvalidEntityMetadataException If the optional target entity
222         *                                        metadata validation didn't
223         *                                        pass.
224         */
225        public TrustChainSet resolveTrustChains(final EntityID target,
226                                                final EntityMetadataValidator targetMetadataValidator)
227                throws ResolveException, InvalidEntityMetadataException {
228                
229                if (trustAnchors.get(target) != null) {
230                        throw new ResolveException("Target is trust anchor");
231                }
232                
233                TrustChainRetriever retriever = new DefaultTrustChainRetriever(statementRetriever, constraints);
234                Set<TrustChain> fetchedTrustChains = retriever.retrieve(target, targetMetadataValidator, trustAnchors.keySet());
235                return verifyTrustChains(
236                        fetchedTrustChains,
237                        retriever.getAccumulatedTrustAnchorJWKSets(),
238                        retriever.getAccumulatedExceptions());
239        }
240        
241        
242        /**
243         * Resolves the trust chains for the specified target.
244         *
245         * @param targetStatement The target entity statement. Must not be
246         *                        {@code null}.
247         *
248         * @return The resolved trust chains, containing at least one valid and
249         *         verified chain.
250         *
251         * @throws ResolveException If no trust chain could be resolved.
252         */
253        public TrustChainSet resolveTrustChains(final EntityStatement targetStatement)
254                throws ResolveException {
255                
256                if (trustAnchors.get(targetStatement.getEntityID()) != null) {
257                        throw new ResolveException("Target is trust anchor");
258                }
259                
260                TrustChainRetriever retriever = new DefaultTrustChainRetriever(statementRetriever, constraints);
261                Set<TrustChain> fetchedTrustChains = retriever.retrieve(targetStatement, trustAnchors.keySet());
262                return verifyTrustChains(
263                        fetchedTrustChains,
264                        retriever.getAccumulatedTrustAnchorJWKSets(),
265                        retriever.getAccumulatedExceptions());
266        }
267        
268        
269        /**
270         * Verifies the specified fetched trust chains.
271         *
272         * @param fetchedTrustChains            The fetched trust chains. Must
273         *                                      not be {@code null},
274         * @param accumulatedTrustAnchorJWKSets The accumulated trust anchor(s)
275         *                                      JWK sets, empty if none. Must
276         *                                      not be {@code null}.
277         * @param accumulatedExceptions         The accumulated exceptions,
278         *                                      empty if none. Must not be
279         *                                      {@code null}.
280         * @return The verified trust chain set.
281         *
282         * @throws ResolveException If no trust chain could be verified.
283         */
284        private TrustChainSet verifyTrustChains(final Set<TrustChain> fetchedTrustChains,
285                                                final Map<EntityID, JWKSet> accumulatedTrustAnchorJWKSets,
286                                                final List<Throwable> accumulatedExceptions)
287                throws ResolveException {
288                
289                if (fetchedTrustChains.isEmpty()) {
290                        if (accumulatedExceptions.isEmpty()) {
291                                throw new ResolveException("No trust chain leading up to a trust anchor");
292                        } else if (accumulatedExceptions.size() == 1){
293                                Throwable cause = accumulatedExceptions.get(0);
294                                throw new ResolveException("Couldn't resolve trust chain: " + cause.getMessage(), cause);
295                        } else {
296                                throw new ResolveException("Couldn't resolve trust chain due to multiple causes", accumulatedExceptions);
297                        }
298                }
299                
300                List<Throwable> verificationExceptions = new LinkedList<>();
301                
302                TrustChainSet verifiedTrustChains = new TrustChainSet();
303                
304                for (TrustChain chain: fetchedTrustChains) {
305                        
306                        EntityID anchor = chain.getTrustAnchorEntityID();
307                        JWKSet anchorJWKSet = trustAnchors.get(anchor);
308                        if (anchorJWKSet == null) {
309                                anchorJWKSet = accumulatedTrustAnchorJWKSets.get(anchor);
310                        }
311                        
312                        try {
313                                chain.verifySignatures(anchorJWKSet);
314                        } catch (BadJOSEException | JOSEException e) {
315                                verificationExceptions.add(e);
316                                continue;
317                        }
318                        
319                        verifiedTrustChains.add(chain);
320                }
321                
322                if (verifiedTrustChains.isEmpty()) {
323                        
324                        List<Throwable> moreAccumulatedExceptions = new LinkedList<>(accumulatedExceptions);
325                        moreAccumulatedExceptions.addAll(verificationExceptions);
326                        
327                        if (verificationExceptions.size() == 1) {
328                                Throwable cause = verificationExceptions.get(0);
329                                throw new ResolveException("Couldn't resolve trust chain: " + cause.getMessage(), moreAccumulatedExceptions);
330                        } else {
331                                throw new ResolveException("Couldn't resolve trust chain due to multiple causes", moreAccumulatedExceptions);
332                        }
333                }
334                
335                return verifiedTrustChains;
336        }
337}