/************************************************************************* * (c) Copyright 2016 Hewlett Packard Enterprise Development Company LP * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. ************************************************************************/ package com.eucalyptus.tokens.oidc; import java.io.ByteArrayInputStream; import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import java.security.Signature; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.security.spec.AlgorithmParameterSpec; import java.util.List; import java.util.function.Predicate; import javax.annotation.Nonnull; import com.eucalyptus.util.Pair; import com.google.common.io.BaseEncoding; import javaslang.collection.Stream; import javaslang.control.Option; /** * */ public class JsonWebSignatureVerifier { public interface KeyResolver { <K extends JsonWebKey> Option<K> resolve( Option<String> kid, Class<K> keyType ); } public static boolean isValid( @Nonnull final String jsonHeaderB64, @Nonnull final String jsonBodyB64, @Nonnull final String signatureB64, @Nonnull final KeyResolver keyResolver, @Nonnull final Predicate<String> signatureAlgorithmPredicate ) throws GeneralSecurityException, OidcParseException { // decode / syntax validation final Pair<JoseHeader,byte[]> decoded = decode( jsonHeaderB64, jsonBodyB64, signatureB64 ); final JoseHeader header = decoded.getLeft( ); final byte[] signature = decoded.getRight( ); // resolve and validate signing algorithm / key final Pair<JsonWebSignatureAlgorithm,JsonWebKey> resolved = resolve( header, keyResolver, signatureAlgorithmPredicate ); final JsonWebSignatureAlgorithm algorithm = resolved.getLeft( ); final JsonWebKey key = resolved.getRight( ); // verify final byte [] bytesToSign = ( jsonHeaderB64 + "." + jsonBodyB64 ).getBytes( StandardCharsets.UTF_8 ); final String sigAlgorithm = algorithm.getJcaSignatureAlgorithm( ); final Option<String> sigProvider = algorithm.getJcaSignatureProvider( ); final Signature jcaSignature = sigProvider.isDefined( ) ? Signature.getInstance( sigAlgorithm, sigProvider.get( ) ) : Signature.getInstance( sigAlgorithm ); final Option<AlgorithmParameterSpec> sigAlgorithmParameterSpec = algorithm.getJcaSignatureAlgorithmParameterSpec( ); if ( sigAlgorithmParameterSpec.isDefined( ) ) { jcaSignature.setParameter( sigAlgorithmParameterSpec.get( ) ); } jcaSignature.initVerify( algorithm.publicKey( key ) ); jcaSignature.update( bytesToSign ); return jcaSignature.verify( algorithm.signature( signature ) ); } private static Pair<JoseHeader,byte[]> decode( @Nonnull final String jsonHeaderB64, @Nonnull final String jsonBodyB64, @Nonnull final String signatureB64 ) throws GeneralSecurityException, OidcParseException { final String jsonHeader; final byte [] signature; try { final BaseEncoding b64Url = BaseEncoding.base64Url( ); // allow padding? jsonHeader = new String( b64Url.decode( jsonHeaderB64 ), StandardCharsets.UTF_8 ); b64Url.decode( jsonBodyB64 ); // ensures valid b64url encoding signature = b64Url.decode( signatureB64 ); } catch ( final IllegalArgumentException e ) { throw new GeneralSecurityException( "Unable to decode", e ); } final JoseHeader header = JoseHeader.parse( jsonHeader ); if ( header.getCrit( ).map( List::size ).getOrElse( 0 ) > 0 ) { throw new GeneralSecurityException( "Unsupported critical extension " + header.getCrit( ) ); } return Pair.pair( header, signature ); } private static Pair<JsonWebSignatureAlgorithm,JsonWebKey> resolve( @Nonnull final JoseHeader header, @Nonnull final KeyResolver keyResolver, @Nonnull final Predicate<String> signatureAlgorithmPredicate ) throws GeneralSecurityException { final JsonWebSignatureAlgorithm algorithm = JsonWebSignatureAlgorithm.lookup( header.getAlg( ) ) .filter( alg -> signatureAlgorithmPredicate.test( alg.name( ) ) ) .getOrElseThrow( () -> new GeneralSecurityException( "Unsupported algorithm: " + header.getAlg( ) ) ); final JsonWebKey key = keyResolver.resolve( header.getKid( ), algorithm.keyType( ) ) .getOrElseThrow( () -> new GeneralSecurityException( "Signing key not found" ) ); final Option<String> certB64 = key.getX5c( ).flatMap( list -> Stream.ofAll( list ).headOption( ) ); if ( certB64.isDefined( ) && !certB64.toTry( ) .mapTry( JsonWebSignatureVerifier::certificateFromB64Der ) .mapTry( cert -> algorithm.matches( key, cert ) ) .getOrElseThrow( ex -> new GeneralSecurityException( "Certificate decode error", ex ) ) ) { throw new GeneralSecurityException( "Certificate does not match public key material" ); } return Pair.pair( algorithm, key ); } private static X509Certificate certificateFromB64Der( @Nonnull final String b64 ) throws GeneralSecurityException { return (X509Certificate) CertificateFactory.getInstance( "X.509" ) .generateCertificate( new ByteArrayInputStream( BaseEncoding.base64( ).decode( b64 ) ) ); } }