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