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}