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.entities;
019
020
021import java.security.Key;
022import java.util.LinkedList;
023import java.util.List;
024
025import net.jcip.annotations.Immutable;
026
027import com.nimbusds.jose.*;
028import com.nimbusds.jose.crypto.factories.DefaultJWSSignerFactory;
029import com.nimbusds.jose.jwk.*;
030import com.nimbusds.jose.proc.BadJOSEException;
031import com.nimbusds.jose.proc.JWSKeySelector;
032import com.nimbusds.jose.proc.SecurityContext;
033import com.nimbusds.jwt.JWTClaimsSet;
034import com.nimbusds.jwt.SignedJWT;
035import com.nimbusds.jwt.proc.DefaultJWTProcessor;
036import com.nimbusds.oauth2.sdk.ParseException;
037import com.nimbusds.oauth2.sdk.util.CollectionUtils;
038
039
040/**
041 * Federation entity statement.
042 *
043 * <p>Related specifications:
044 *
045 * <ul>
046 *     <li>OpenID Connect Federation 1.0, section 2.1.
047 * </ul>
048 */
049@Immutable
050public final class EntityStatement {
051        
052        
053        /**
054         * The signed statement as signed JWT.
055         */
056        private final SignedJWT statementJWT;
057        
058        
059        /**
060         * The statement claims.
061         */
062        private final EntityStatementClaimsSet statementClaimsSet;
063        
064        
065        /**
066         * Creates a new federation entity statement.
067         *
068         * @param statementJWT       The signed statement as signed JWT. Must
069         *                           not be {@code null}.
070         * @param statementClaimsSet The statement claims. Must not be
071         *                           {@code null}.
072         */
073        private EntityStatement(final SignedJWT statementJWT,
074                                final EntityStatementClaimsSet statementClaimsSet) {
075                
076                if (statementJWT == null) {
077                        throw new IllegalArgumentException("The entity statement must not be null");
078                }
079                if (JWSObject.State.UNSIGNED.equals(statementJWT.getState())) {
080                        throw new IllegalArgumentException("The statement is not signed");
081                }
082                this.statementJWT = statementJWT;
083                
084                if (statementClaimsSet == null) {
085                        throw new IllegalArgumentException("The entity statement claims set must not be null");
086                }
087                this.statementClaimsSet = statementClaimsSet;
088        }
089        
090        
091        /**
092         * Returns the entity ID.
093         *
094         * @return The entity ID.
095         */
096        public EntityID getEntityID() {
097                return getClaimsSet().getSubjectEntityID();
098        }
099        
100        
101        /**
102         * Returns the signed statement.
103         *
104         * @return The signed statement as signed JWT.
105         */
106        public SignedJWT getSignedStatement() {
107                return statementJWT;
108        }
109        
110        
111        /**
112         * Returns the statement claims.
113         *
114         * @return The statement claims.
115         */
116        public EntityStatementClaimsSet getClaimsSet() {
117                return statementClaimsSet;
118        }
119        
120        
121        /**
122         * Returns {@code true} if this entity statement is for a
123         * {@link EntityRole#TRUST_ANCHOR trust anchor}.
124         *
125         * @return {@code true} for a trust anchor, else {@code false}.
126         */
127        public boolean isTrustAnchor() {
128                
129                return getClaimsSet().isSelfStatement() && CollectionUtils.isEmpty(getClaimsSet().getAuthorityHints());
130        }
131        
132        
133        /**
134         * Verifies the signature for a self-statement (typically for a trust
135         * anchor or leaf) and checks the statement issue and expiration times.
136         *
137         * @throws BadJOSEException If the signature is invalid or the
138         *                          statement is expired or before the issue
139         *                          time.
140         * @throws JOSEException    On a internal JOSE exception.
141         */
142        public void verifySignatureOfSelfStatement() throws BadJOSEException, JOSEException {
143                
144                if (! getClaimsSet().isSelfStatement()) {
145                        throw new BadJOSEException("Entity statement not self-issued");
146                }
147                
148                verifySignature(getClaimsSet().getJWKSet());
149        }
150        
151        
152        /**
153         * Verifies the signature and checks the statement issue and expiration
154         * times.
155         *
156         * @param jwkSet The JWK set to use for the signature validation.
157         *               Must not be {@code null}.
158         *
159         * @throws BadJOSEException If the signature is invalid or the
160         *                          statement is expired or before the issue
161         *                          time.
162         * @throws JOSEException    On a internal JOSE exception.
163         */
164        public void verifySignature(final JWKSet jwkSet)
165                throws BadJOSEException, JOSEException {
166                
167                DefaultJWTProcessor<?> jwtProcessor = new DefaultJWTProcessor<>();
168                jwtProcessor.setJWSKeySelector(new JWSKeySelector() {
169                        @Override
170                        public List<? extends Key> selectJWSKeys(final JWSHeader header, final SecurityContext context) {
171                                List<JWK> jwkMatches = new JWKSelector(JWKMatcher.forJWSHeader(header)).select(jwkSet);
172                                return new LinkedList<>(KeyConverter.toJavaKeys(jwkMatches));
173                        }
174                });
175                
176                // Double check claims with JWT framework
177                jwtProcessor.setJWTClaimsSetVerifier(new EntityStatementClaimsVerifier(null));
178                jwtProcessor.process(getSignedStatement(), null);
179        }
180        
181        
182        /**
183         * Signs the specified federation entity claims set.
184         *
185         * @param claimsSet  The claims set. Must not be {@code null}.
186         * @param signingJWK The private signing JWK. Must be contained in the
187         *                   entity JWK set and not {@code null}.
188         *
189         * @return The signed federation entity statement.
190         *
191         * @throws JOSEException On a internal signing exception.
192         */
193        public static EntityStatement sign(final EntityStatementClaimsSet claimsSet,
194                                           final JWK signingJWK)
195                throws JOSEException {
196                
197                return sign(claimsSet, signingJWK, resolveSigningAlgorithm(signingJWK));
198        }
199        
200        
201        /**
202         * Signs the specified federation entity claims set.
203         *
204         * @param claimsSet  The claims set. Must not be {@code null}.
205         * @param signingJWK The private signing JWK. Must be contained in the
206         *                   entity JWK set and not {@code null}.
207         * @param jwsAlg     The signing algorithm. Must be supported by the
208         *                   JWK and not {@code null}.
209         *
210         * @return The signed federation entity statement.
211         *
212         * @throws JOSEException On a internal signing exception.
213         */
214        public static EntityStatement sign(final EntityStatementClaimsSet claimsSet,
215                                           final JWK signingJWK,
216                                           final JWSAlgorithm jwsAlg)
217                throws JOSEException {
218                
219                if (claimsSet.isSelfStatement() && ! claimsSet.getJWKSet().containsJWK(signingJWK)) {
220                        throw new JOSEException("Signing JWK not found in JWK set of self-statement");
221                }
222                
223                JWSSigner jwsSigner = new DefaultJWSSignerFactory().createJWSSigner(signingJWK, jwsAlg);
224                
225                JWSHeader jwsHeader = new JWSHeader.Builder(jwsAlg)
226                        .keyID(signingJWK.getKeyID())
227                        .build();
228                
229                SignedJWT signedJWT;
230                try {
231                        signedJWT = new SignedJWT(jwsHeader, claimsSet.toJWTClaimsSet());
232                } catch (ParseException e) {
233                        throw new JOSEException(e.getMessage(), e);
234                }
235                signedJWT.sign(jwsSigner);
236                return new EntityStatement(signedJWT, claimsSet);
237        }
238        
239        
240        private static JWSAlgorithm resolveSigningAlgorithm(final JWK jwk)
241                throws JOSEException {
242                
243                KeyType jwkType = jwk.getKeyType();
244                
245                if (KeyType.RSA.equals(jwkType)) {
246                        if (jwk.getAlgorithm() != null) {
247                                return new JWSAlgorithm(jwk.getAlgorithm().getName());
248                        } else {
249                                return JWSAlgorithm.RS256; // assume RS256 as default
250                        }
251                } else if (KeyType.EC.equals(jwkType)) {
252                        ECKey ecJWK = jwk.toECKey();
253                        if (jwk.getAlgorithm() != null) {
254                                return new JWSAlgorithm(ecJWK.getAlgorithm().getName());
255                        } else {
256                                if (Curve.P_256.equals(ecJWK.getCurve())) {
257                                        return JWSAlgorithm.ES256;
258                                } else if (Curve.P_384.equals(ecJWK.getCurve())) {
259                                        return JWSAlgorithm.ES384;
260                                } else if (Curve.P_521.equals(ecJWK.getCurve())) {
261                                        return JWSAlgorithm.ES512;
262                                } else {
263                                        throw new JOSEException("Unsupported ECDSA curve: " + ecJWK.getCurve());
264                                }
265                        }
266                } else if (KeyType.OKP.equals(jwkType)){
267                        OctetKeyPair okp = jwk.toOctetKeyPair();
268                        if (Curve.Ed25519.equals(okp.getCurve())) {
269                                return JWSAlgorithm.EdDSA;
270                        } else {
271                                throw new JOSEException("Unsupported EdDSA curve: " + okp.getCurve());
272                        }
273                } else {
274                        throw new JOSEException("Unsupported JWK type: " + jwkType);
275                }
276        }
277        
278        
279        /**
280         * Parses a federation entity statement.
281         *
282         * @param signedStmt The signed statement as a signed JWT. Must not
283         *                   be {@code null}.
284         *
285         * @return The federation entity statement.
286         *
287         * @throws ParseException If parsing failed.
288         */
289        public static EntityStatement parse(final SignedJWT signedStmt)
290                throws ParseException {
291                
292                if (JWSObject.State.UNSIGNED.equals(signedStmt.getState())) {
293                        throw new ParseException("The statement is not signed");
294                }
295                
296                JWTClaimsSet jwtClaimsSet;
297                try {
298                        jwtClaimsSet = signedStmt.getJWTClaimsSet();
299                } catch (java.text.ParseException e) {
300                        throw new ParseException(e.getMessage(), e);
301                }
302                
303                EntityStatementClaimsSet claimsSet = new EntityStatementClaimsSet(jwtClaimsSet);
304                return new EntityStatement(signedStmt, claimsSet);
305        }
306        
307        
308        /**
309         * Parses a federation entity statement.
310         *
311         * @param signedStmtString The signed statement as a signed JWT string.
312         *                         Must not be {@code null}.
313         *
314         * @return The federation entity statement.
315         *
316         * @throws ParseException If parsing failed.
317         */
318        public static EntityStatement parse(final String signedStmtString)
319                throws ParseException {
320                
321                try {
322                        return parse(SignedJWT.parse(signedStmtString));
323                } catch (java.text.ParseException e) {
324                        throw new ParseException("Invalid entity statement: " + e.getMessage(), e);
325                }
326        }
327}