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;
028
029
030/**
031 * Trust chain resolver.
032 *
033 * <p>Related specifications:
034 *
035 * <ul>
036 *     <li>OpenID Connect Federation 1.0, section 7.
037 * </ul>
038 */
039public class TrustChainResolver {
040        
041        
042        /**
043         * The configured trust anchors with their public JWK sets.
044         */
045        private final Map<EntityID, JWKSet> trustAnchors;
046        
047        
048        /**
049         * The entity statement retriever.
050         */
051        private final EntityStatementRetriever statementRetriever;
052        
053        
054        /**
055         * Creates a new trust chain resolver with a single trust anchor.
056         *
057         * @param trustAnchor The trust anchor. Must not be {@code null}.
058         */
059        public TrustChainResolver(final EntityID trustAnchor) {
060                this(trustAnchor, null);
061        }
062        
063        
064        /**
065         * Creates a new trust chain resolver with a single trust anchor.
066         *
067         * @param trustAnchor       The trust anchor. Must not be {@code null}.
068         * @param trustAnchorJWKSet The trust anchor public JWK set,
069         *                          {@code null} if not available.
070         */
071        public TrustChainResolver(final EntityID trustAnchor,
072                                  final JWKSet trustAnchorJWKSet) {
073                this(Collections.singletonMap(trustAnchor, trustAnchorJWKSet), new DefaultEntityStatementRetriever());
074        }
075        
076        
077        /**
078         * Creates a new trust chain resolver with multiple trust anchors.
079         *
080         * @param trustAnchors         The trust anchors with their public JWK
081         *                             sets (if available). Must contain at
082         *                             least one anchor.
083         * @param httpConnectTimeoutMs The HTTP connect timeout in
084         *                             milliseconds, zero means timeout
085         *                             determined by the underlying HTTP
086         *                             client.
087         * @param httpReadTimeoutMs    The HTTP read timeout in milliseconds,
088         *                             zero means timout determined by the
089         *                             underlying HTTP client.
090         */
091        public TrustChainResolver(final Map<EntityID, JWKSet> trustAnchors,
092                                  final int httpConnectTimeoutMs,
093                                  final int httpReadTimeoutMs) {
094                this(trustAnchors, new DefaultEntityStatementRetriever(httpConnectTimeoutMs, httpReadTimeoutMs));
095        }
096        
097        
098        /**
099         * Creates new trust chain resolver.
100         *
101         * @param trustAnchors       The trust anchors with their public JWK
102         *                           sets. Must contain at least one anchor.
103         * @param statementRetriever The entity statement retriever to use.
104         *                           Must not be {@code null}.
105         */
106        public TrustChainResolver(final Map<EntityID, JWKSet> trustAnchors,
107                                  final EntityStatementRetriever statementRetriever) {
108                if (MapUtils.isEmpty(trustAnchors)) {
109                        throw new IllegalArgumentException("The trust anchors map must not be empty or null");
110                }
111                this.trustAnchors = trustAnchors;
112                
113                if (statementRetriever == null) {
114                        throw new IllegalArgumentException("The entity statement retriever must not be null");
115                }
116                this.statementRetriever = statementRetriever;
117        }
118        
119        
120        /**
121         * Returns the configured trust anchors.
122         *
123         * @return The trust anchors with their public JWK sets (if available).
124         *         Contains at least one anchor.
125         */
126        public Map<EntityID, JWKSet> getTrustAnchors() {
127                return Collections.unmodifiableMap(trustAnchors);
128        }
129        
130        
131        /**
132         * Returns the configured entity statement retriever.
133         *
134         * @return The configured entity statement retriever.
135         */
136        public EntityStatementRetriever getEntityStatementRetriever() {
137                return statementRetriever;
138        }
139        
140        
141        /**
142         * Resolves the trust chains for the specified target.
143         *
144         * @param target The target. Must not be {@code null}.
145         *
146         * @return The resolved trust chains, containing at least one valid and
147         *         verified chain.
148         *
149         * @throws ResolveException If no trust chain could be resolved.
150         */
151        public TrustChainSet resolveTrustChains(final EntityID target)
152                throws ResolveException {
153                
154                if (trustAnchors.get(target) != null) {
155                        throw new ResolveException("Target is trust anchor");
156                }
157                
158                TrustChainRetriever retriever = new DefaultTrustChainRetriever(statementRetriever);
159                
160                Set<TrustChain> fetchedTrustChains = retriever.retrieve(target, trustAnchors.keySet());
161                
162                if (fetchedTrustChains.isEmpty()) {
163                
164                        if (retriever.getAccumulatedExceptions().isEmpty()) {
165                                throw new ResolveException("No trust chain leading up to a trust anchor");
166                        } else if (retriever.getAccumulatedExceptions().size() == 1){
167                                Throwable cause = retriever.getAccumulatedExceptions().get(0);
168                                throw new ResolveException("Couldn't resolve trust chain: " + cause.getMessage(), cause);
169                        } else {
170                                throw new ResolveException("Couldn't resolve trust chain due to multiple causes", retriever.getAccumulatedExceptions());
171                        }
172                }
173                
174                List<Throwable> verificationExceptions = new LinkedList<>();
175                
176                TrustChainSet verifiedTrustChains = new TrustChainSet();
177                
178                for (TrustChain chain: fetchedTrustChains) {
179                        
180                        EntityID anchor = chain.getTrustAnchorEntityID();
181                        JWKSet anchorJWKSet = trustAnchors.get(anchor);
182                        if (anchorJWKSet == null) {
183                                anchorJWKSet = retriever.getAccumulatedTrustAnchorJWKSets().get(anchor);
184                        }
185                        
186                        try {
187                                chain.verifySignatures(anchorJWKSet);
188                        } catch (BadJOSEException | JOSEException e) {
189                                verificationExceptions.add(e);
190                                continue;
191                        }
192                        
193                        verifiedTrustChains.add(chain);
194                }
195                
196                if (verifiedTrustChains.isEmpty()) {
197                        
198                        List<Throwable> accumulatedExceptions = new LinkedList<>(retriever.getAccumulatedExceptions());
199                        accumulatedExceptions.addAll(verificationExceptions);
200                        
201                        if (verificationExceptions.size() == 1) {
202                                Throwable cause = verificationExceptions.get(0);
203                                throw new ResolveException("Couldn't resolve trust chain: " + cause.getMessage(), accumulatedExceptions);
204                        } else {
205                                throw new ResolveException("Couldn't resolve trust chain due to multiple causes", accumulatedExceptions);
206                        }
207                }
208                
209                return verifiedTrustChains;
210        }
211}