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.security.ProviderException;
022import java.util.Date;
023import java.util.Iterator;
024import java.util.LinkedList;
025import java.util.List;
026import java.util.concurrent.atomic.AtomicReference;
027
028import net.jcip.annotations.Immutable;
029import net.minidev.json.JSONObject;
030
031import com.nimbusds.jose.JOSEException;
032import com.nimbusds.jose.jwk.JWK;
033import com.nimbusds.jose.jwk.JWKSet;
034import com.nimbusds.jose.proc.BadJOSEException;
035import com.nimbusds.jose.util.Base64URL;
036import com.nimbusds.oauth2.sdk.ParseException;
037import com.nimbusds.oauth2.sdk.id.Subject;
038import com.nimbusds.oauth2.sdk.util.CollectionUtils;
039import com.nimbusds.openid.connect.sdk.federation.entities.EntityID;
040import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
041import com.nimbusds.openid.connect.sdk.federation.policy.MetadataPolicy;
042import com.nimbusds.openid.connect.sdk.federation.policy.MetadataPolicyEntry;
043import com.nimbusds.openid.connect.sdk.federation.policy.language.PolicyViolationException;
044import com.nimbusds.openid.connect.sdk.federation.policy.operations.DefaultPolicyOperationCombinationValidator;
045import com.nimbusds.openid.connect.sdk.federation.policy.operations.PolicyOperationCombinationValidator;
046
047
048/**
049 * Federation entity trust chain.
050 *
051 * <p>Related specifications:
052 *
053 * <ul>
054 *     <li>OpenID Connect Federation 1.0, sections 2.2 and 7.
055 * </ul>
056 */
057@Immutable
058public final class TrustChain {
059        
060        
061        /**
062         * The leaf entity self-statement.
063         */
064        private final EntityStatement leaf;
065        
066        
067        /**
068         * The superior entity statements.
069         */
070        private final List<EntityStatement> superiors;
071        
072        
073        /**
074         * Caches the resolved expiration time for this trust chain.
075         */
076        private Date exp;
077        
078        
079        /**
080         * Creates a new federation entity trust chain. Validates the subject -
081         * issuer chain, the signatures are not verified.
082         *
083         * @param leaf      The leaf entity self-statement. Must not be
084         *                  {@code null}.
085         * @param superiors The superior entity statements, starting with a
086         *                  statement of the first superior about the leaf,
087         *                  ending with the statement of the trust anchor about
088         *                  the last intermediate or the leaf (for a minimal
089         *                  trust chain). Must contain at least one entity
090         *                  statement.
091         *
092         * @throws IllegalArgumentException If the subject - issuer chain is
093         *                                  broken.
094         */
095        public TrustChain(final EntityStatement leaf, List<EntityStatement> superiors) {
096                if (leaf == null) {
097                        throw new IllegalArgumentException("The leaf statement must not be null");
098                }
099                this.leaf = leaf;
100                
101                if (CollectionUtils.isEmpty(superiors)) {
102                        throw new IllegalArgumentException("There must be at least one superior statement (issued by the trust anchor)");
103                }
104                this.superiors = superiors;
105                if (! hasValidIssuerSubjectChain(leaf, superiors)) {
106                        throw new IllegalArgumentException("Broken subject - issuer chain");
107                }
108        }
109        
110        
111        private static boolean hasValidIssuerSubjectChain(final EntityStatement leaf, final List<EntityStatement> superiors) {
112                
113                Subject nextExpectedSubject = leaf.getClaimsSet().getSubject();
114                
115                for (EntityStatement superiorStmt : superiors) {
116                        if (! nextExpectedSubject.equals(superiorStmt.getClaimsSet().getSubject())) {
117                                return false;
118                        }
119                        nextExpectedSubject = new Subject(superiorStmt.getClaimsSet().getIssuer().getValue());
120                }
121                return true;
122        }
123        
124        
125        /**
126         * Returns the leaf entity self-statement.
127         *
128         * @return The leaf entity self-statement.
129         */
130        public EntityStatement getLeafSelfStatement() {
131                return leaf;
132        }
133        
134        
135        /**
136         * Returns the superior entity statements.
137         *
138         * @return The superior entity statements, starting with a statement of
139         *         the first superior about the leaf, ending with the statement
140         *         of the trust anchor about the last intermediate or the leaf
141         *         (for a minimal trust chain).
142         */
143        public List<EntityStatement> getSuperiorStatements() {
144                return superiors;
145        }
146        
147        
148        /**
149         * Returns the entity ID of the trust anchor.
150         *
151         * @return The entity ID of the trust anchor.
152         */
153        public EntityID getTrustAnchorEntityID() {
154                
155                // Return last in superiors
156                return getSuperiorStatements()
157                        .get(getSuperiorStatements().size() - 1)
158                        .getClaimsSet()
159                        .getIssuerEntityID();
160        }
161        
162        
163        /**
164         * Returns the length of this trust chain. A minimal trust chain with a
165         * leaf and anchor has a length of one.
166         *
167         * @return The trust chain length.
168         */
169        public int length() {
170                
171                return getSuperiorStatements().size();
172        }
173        
174        
175        /**
176         * Resolves the combined metadata policy for this trust chain. Uses the
177         * {@link DefaultPolicyOperationCombinationValidator default policy
178         * combination validator}.
179         *
180         * @return The combined metadata policy, with no policy operations if
181         *         no policies were found.
182         *
183         * @throws ParseException           On a policy parse exception.
184         * @throws PolicyViolationException On a policy violation exception.
185         */
186        public MetadataPolicy resolveCombinedMetadataPolicy()
187                throws ParseException, PolicyViolationException {
188                
189                return resolveCombinedMetadataPolicy(MetadataPolicyEntry.DEFAULT_POLICY_COMBINATION_VALIDATOR);
190        }
191        
192        
193        /**
194         * Resolves the combined metadata policy for this trust chain.
195         *
196         * @param combinationValidator The policy operation combination
197         *                             validator. Must not be {@code null}.
198         *
199         * @return The combined metadata policy, with no policy operations if
200         *         no policies were found.
201         *
202         * @throws ParseException           On a policy parse exception.
203         * @throws PolicyViolationException On a policy violation exception.
204         */
205        public MetadataPolicy resolveCombinedMetadataPolicy(final PolicyOperationCombinationValidator combinationValidator)
206                throws ParseException, PolicyViolationException {
207                
208                List<MetadataPolicy> policies = new LinkedList<>();
209                
210                for (EntityStatement stmt: getSuperiorStatements()) {
211                        
212                        JSONObject jsonObject = stmt.getClaimsSet().getMetadataPolicyJSONObject();
213                        
214                        if (jsonObject == null) {
215                                continue;
216                        }
217                        
218                        policies.add(MetadataPolicy.parse(jsonObject));
219                }
220                
221                return MetadataPolicy.combine(policies, combinationValidator);
222        }
223        
224        
225        /**
226         * Return an iterator starting from the leaf entity statement.
227         *
228         * @return The iterator.
229         */
230        public Iterator<EntityStatement> iteratorFromLeaf() {
231                
232                // Init
233                final AtomicReference<EntityStatement> next = new AtomicReference<>(getLeafSelfStatement());
234                final Iterator<EntityStatement> superiorsIterator = getSuperiorStatements().iterator();
235                
236                return new Iterator<EntityStatement>() {
237                        @Override
238                        public boolean hasNext() {
239                                return next.get() != null;
240                        }
241                        
242                        
243                        @Override
244                        public EntityStatement next() {
245                                EntityStatement toReturn = next.get();
246                                if (toReturn == null) {
247                                        return null; // reached end on last iteration
248                                }
249                                
250                                // Set statement to return on next iteration
251                                if (toReturn.equals(getLeafSelfStatement())) {
252                                        // Return first superior
253                                        next.set(superiorsIterator.next());
254                                } else {
255                                        // Return next superior or end
256                                        if (superiorsIterator.hasNext()) {
257                                                next.set(superiorsIterator.next());
258                                        } else {
259                                                next.set(null);
260                                        }
261                                }
262                                
263                                return toReturn;
264                        }
265                        
266                        
267                        @Override
268                        public void remove() {
269                                throw new UnsupportedOperationException();
270                        }
271                };
272        }
273        
274        
275        /**
276         * Resolves the expiration time for this trust chain. Equals the
277         * nearest expiration when all entity statements in the trust chain are
278         * considered.
279         *
280         * @return The expiration time for this trust chain.
281         */
282        public Date resolveExpirationTime() {
283                
284                if (exp != null) {
285                        return exp;
286                }
287                
288                Iterator<EntityStatement> it = iteratorFromLeaf();
289                
290                Date nearestExp = null;
291                
292                while (it.hasNext()) {
293                        
294                        Date stmtExp = it.next().getClaimsSet().getExpirationTime();
295                        
296                        if (nearestExp == null) {
297                                nearestExp = stmtExp; // on first iteration
298                        } else if (stmtExp.before(nearestExp)) {
299                                nearestExp = stmtExp; // replace nearest
300                        }
301                }
302                
303                exp = nearestExp;
304                return exp;
305        }
306        
307        
308        /**
309         * Verifies the signatures in this trust chain.
310         *
311         * @param trustAnchorJWKSet The trust anchor JWK set. Must not be
312         *                          {@code null}.
313         *
314         * @throws BadJOSEException If a signature is invalid or a statement is
315         *                          expired or before the issue time.
316         * @throws JOSEException    On a internal JOSE exception.
317         */
318        public void verifySignatures(final JWKSet trustAnchorJWKSet)
319                throws BadJOSEException, JOSEException {
320                
321                Base64URL signingJWKThumbprint;
322                try {
323                        signingJWKThumbprint = leaf.verifySignatureOfSelfStatement();
324                } catch (BadJOSEException e) {
325                        throw new BadJOSEException("Invalid leaf statement: " + e.getMessage(), e);
326                }
327                
328                for (int i=0; i < superiors.size(); i++) {
329                        
330                        EntityStatement stmt = superiors.get(i);
331                        
332                        JWKSet verificationJWKSet;
333                        if (i+1 == superiors.size()) {
334                                verificationJWKSet = trustAnchorJWKSet;
335                        } else {
336                                verificationJWKSet = superiors.get(i+1).getClaimsSet().getJWKSet();
337                        }
338                        
339                        // Check that the signing JWK is registered with the superior
340                        if (! hasJWKWithThumbprint(stmt.getClaimsSet().getJWKSet(), signingJWKThumbprint)) {
341                                throw new BadJOSEException("Signing JWK with thumbprint " + signingJWKThumbprint + " not found in entity statement issued from superior " + stmt.getClaimsSet().getIssuerEntityID());
342                        }
343                        
344                        try {
345                                signingJWKThumbprint = stmt.verifySignature(verificationJWKSet);
346                        } catch (BadJOSEException e) {
347                                throw new BadJOSEException("Invalid statement from " + stmt.getClaimsSet().getIssuer() + ": " + e.getMessage(), e);
348                        }
349                }
350        }
351        
352        
353        private static boolean hasJWKWithThumbprint(final JWKSet jwkSet, final Base64URL thumbprint) {
354                
355                if (jwkSet == null) {
356                        return false;
357                }
358                
359                for (JWK jwk: jwkSet.getKeys()) {
360                        
361                        try {
362                                if (thumbprint.equals(jwk.computeThumbprint())) {
363                                        return true;
364                                }
365                        } catch (JOSEException e) {
366                                throw new ProviderException(e.getMessage(), e);
367                        }
368                        
369                }
370                
371                return false;
372        }
373}