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.Date;
022import java.util.Iterator;
023import java.util.List;
024import java.util.concurrent.atomic.AtomicReference;
025
026import net.jcip.annotations.Immutable;
027
028import com.nimbusds.jose.JOSEException;
029import com.nimbusds.jose.jwk.JWKSet;
030import com.nimbusds.jose.proc.BadJOSEException;
031import com.nimbusds.oauth2.sdk.id.Subject;
032import com.nimbusds.oauth2.sdk.util.CollectionUtils;
033import com.nimbusds.openid.connect.sdk.federation.entities.EntityID;
034import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
035
036
037/**
038 * Federation entity trust chain.
039 *
040 * <p>Related specifications:
041 *
042 * <ul>
043 *     <li>OpenID Connect Federation 1.0, section 2.2.
044 * </ul>
045 */
046@Immutable
047public final class TrustChain {
048        
049        
050        /**
051         * The leaf entity self-statement.
052         */
053        private final EntityStatement leaf;
054        
055        
056        /**
057         * The superior entity statements.
058         */
059        private final List<EntityStatement> superiors;
060        
061        
062        /**
063         * Caches the resolved expiration time for this trust chain.
064         */
065        private Date exp;
066        
067        
068        /**
069         * Creates a new federation entity trust chain. Validates the subject -
070         * issuer chain, the signatures are not verified.
071         *
072         * @param leaf      The leaf entity self-statement. Must not be
073         *                  {@code null}.
074         * @param superiors The superior entity statements, starting with a
075         *                  statement of the first superior about the leaf,
076         *                  ending with the statement of the trust anchor about
077         *                  the last intermediate or the leaf (for a minimal
078         *                  trust chain). Must contain at least one entity
079         *                  statement.
080         *
081         * @throws IllegalArgumentException If the subject - issuer chain is
082         *                                  broken.
083         */
084        public TrustChain(final EntityStatement leaf, List<EntityStatement> superiors) {
085                if (leaf == null) {
086                        throw new IllegalArgumentException("The leaf statement must not be null");
087                }
088                this.leaf = leaf;
089                
090                if (CollectionUtils.isEmpty(superiors)) {
091                        throw new IllegalArgumentException("There must be at least one superior statement (issued by the trust anchor)");
092                }
093                this.superiors = superiors;
094                if (! hasValidIssuerSubjectChain(leaf, superiors)) {
095                        throw new IllegalArgumentException("Broken subject - issuer chain");
096                }
097        }
098        
099        
100        private static boolean hasValidIssuerSubjectChain(final EntityStatement leaf, final List<EntityStatement> superiors) {
101                
102                Subject nextExpectedSubject = leaf.getClaimsSet().getSubject();
103                
104                for (EntityStatement superiorStmt : superiors) {
105                        if (! nextExpectedSubject.equals(superiorStmt.getClaimsSet().getSubject())) {
106                                return false;
107                        }
108                        nextExpectedSubject = new Subject(superiorStmt.getClaimsSet().getIssuer().getValue());
109                }
110                return true;
111        }
112        
113        
114        /**
115         * Returns the leaf entity self-statement.
116         *
117         * @return The leaf entity self-statement.
118         */
119        public EntityStatement getLeafSelfStatement() {
120                return leaf;
121        }
122        
123        
124        /**
125         * Returns the superior entity statements.
126         *
127         * @return The superior entity statements, starting with a statement of
128         *         the first superior about the leaf, ending with the statement
129         *         of the trust anchor about the last intermediate or the leaf
130         *         (for a minimal trust chain).
131         */
132        public List<EntityStatement> getSuperiorStatements() {
133                return superiors;
134        }
135        
136        
137        /**
138         * Returns the entity ID of the trust anchor.
139         *
140         * @return The entity ID of the trust anchor.
141         */
142        public EntityID getTrustAnchorEntityID() {
143                
144                // Return last in superiors
145                return getSuperiorStatements()
146                        .get(getSuperiorStatements().size() - 1)
147                        .getClaimsSet()
148                        .getIssuerEntityID();
149        }
150        
151        
152        /**
153         * Returns the length of this trust chain. A minimal trust chain with a
154         * leaf and anchor has a length of one.
155         *
156         * @return The trust chain length.
157         */
158        public int length() {
159                
160                return getSuperiorStatements().size();
161        }
162        
163        
164        /**
165         * Return an iterator starting from the leaf entity statement.
166         *
167         * @return The iterator.
168         */
169        public Iterator<EntityStatement> iteratorFromLeaf() {
170                
171                // Init
172                final AtomicReference<EntityStatement> next = new AtomicReference<>(getLeafSelfStatement());
173                final Iterator<EntityStatement> superiorsIterator = getSuperiorStatements().iterator();
174                
175                return new Iterator<EntityStatement>() {
176                        @Override
177                        public boolean hasNext() {
178                                return next.get() != null;
179                        }
180                        
181                        
182                        @Override
183                        public EntityStatement next() {
184                                EntityStatement toReturn = next.get();
185                                if (toReturn == null) {
186                                        return null; // reached end on last iteration
187                                }
188                                
189                                // Set statement to return on next iteration
190                                if (toReturn.equals(getLeafSelfStatement())) {
191                                        // Return first superior
192                                        next.set(superiorsIterator.next());
193                                } else {
194                                        // Return next superior or end
195                                        if (superiorsIterator.hasNext()) {
196                                                next.set(superiorsIterator.next());
197                                        } else {
198                                                next.set(null);
199                                        }
200                                }
201                                
202                                return toReturn;
203                        }
204                        
205                        
206                        @Override
207                        public void remove() {
208                                throw new UnsupportedOperationException();
209                        }
210                };
211        }
212        
213        
214        /**
215         * Resolves the expiration time for this trust chain. Equals the
216         * nearest expiration when all entity statements in the trust chain are
217         * considered.
218         *
219         * @return The expiration time for this trust chain.
220         */
221        public Date resolveExpirationTime() {
222                
223                if (exp != null) {
224                        return exp;
225                }
226                
227                Iterator<EntityStatement> it = iteratorFromLeaf();
228                
229                Date nearestExp = null;
230                
231                while (it.hasNext()) {
232                        
233                        Date stmtExp = it.next().getClaimsSet().getExpirationTime();
234                        
235                        if (nearestExp == null) {
236                                nearestExp = stmtExp; // on first iteration
237                        } else if (stmtExp.before(nearestExp)) {
238                                nearestExp = stmtExp; // replace nearest
239                        }
240                }
241                
242                exp = nearestExp;
243                return exp;
244        }
245        
246        
247        /**
248         * Verifies the signatures in this trust chain.
249         *
250         * @param trustAnchorJWKSet The trust anchor JWK set. Must not be
251         *                          {@code null}.
252         *
253         * @throws BadJOSEException If a signature is invalid or a statement is
254         *                          expired or before the issue time.
255         * @throws JOSEException    On a internal JOSE exception.
256         */
257        public void verifySignatures(final JWKSet trustAnchorJWKSet)
258                throws BadJOSEException, JOSEException {
259                
260                try {
261                        leaf.verifySignatureOfSelfStatement();
262                } catch (BadJOSEException e) {
263                        throw new BadJOSEException("Invalid leaf statement: " + e.getMessage(), e);
264                }
265                
266                for (int i=0; i < superiors.size(); i++) {
267                        
268                        EntityStatement stmt = superiors.get(i);
269                        
270                        JWKSet verificationJWKSet;
271                        if (i+1 == superiors.size()) {
272                                verificationJWKSet = trustAnchorJWKSet;
273                        } else {
274                                verificationJWKSet = superiors.get(i+1).getClaimsSet().getJWKSet();
275                        }
276                        
277                        try {
278                                stmt.verifySignature(verificationJWKSet);
279                        } catch (BadJOSEException e) {
280                                throw new BadJOSEException("Invalid statement from " + stmt.getClaimsSet().getIssuer() + ": " + e.getMessage(), e);
281                        }
282                }
283        }
284}