CMSgov/dpc-app

View on GitHub
dpc-api/src/main/java/gov/cms/dpc/api/resources/v1/KeyResource.java

Summary

Maintainability
A
0 mins
Test Coverage
B
88%
package gov.cms.dpc.api.resources.v1;


import com.codahale.metrics.annotation.ExceptionMetered;
import com.codahale.metrics.annotation.Timed;
import gov.cms.dpc.api.auth.OrganizationPrincipal;
import gov.cms.dpc.api.auth.annotations.Authorizer;
import gov.cms.dpc.api.auth.jwt.PublicKeyHandler;
import gov.cms.dpc.api.entities.PublicKeyEntity;
import gov.cms.dpc.api.exceptions.PublicKeyException;
import gov.cms.dpc.api.jdbi.PublicKeyDAO;
import gov.cms.dpc.api.models.CollectionResponse;
import gov.cms.dpc.api.resources.AbstractKeyResource;
import gov.cms.dpc.common.annotations.NoHtml;
import gov.cms.dpc.common.entities.OrganizationEntity;
import io.dropwizard.auth.Auth;
import io.dropwizard.hibernate.UnitOfWork;
import io.swagger.annotations.*;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.eclipse.jetty.http.HttpStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.security.SecureRandom;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

@Api(tags = {"Auth", "Key"}, authorizations = @Authorization(value = "access_token"))
@Path("/v1/Key")
public class KeyResource extends AbstractKeyResource {

    public static final String SNIPPET = "This is the snippet used to verify a key pair in DPC.";
    private static final Logger logger = LoggerFactory.getLogger(KeyResource.class);

    private final PublicKeyDAO dao;
    private final SecureRandom random;

    @Inject
    public KeyResource(PublicKeyDAO dao) {
        this.dao = dao;
        this.random = new SecureRandom();
    }

    @GET
    @Timed
    @ExceptionMetered
    @Authorizer
    @UnitOfWork
    @Produces(MediaType.APPLICATION_JSON)
    @ApiOperation(value = "Fetch public keys for Organization",
            notes = "This endpoint returns all the public keys currently associated with the organization." +
                    "<p>The returned keys are serialized using PEM encoding.",
            authorizations = @Authorization(value = "access_token"))
    @Override
    public CollectionResponse<PublicKeyEntity> getPublicKeys(@ApiParam(hidden = true) @Auth OrganizationPrincipal organizationPrincipal) {
        return new CollectionResponse<>(this.dao.fetchPublicKeys(organizationPrincipal.getID()));
    }

    @GET
    @Timed
    @ExceptionMetered
    @Path("/{keyID}")
    @UnitOfWork
    @Authorizer
    @Produces(MediaType.APPLICATION_JSON)
    @ApiOperation(value = "Fetch public key for Organization",
            notes = "This endpoint returns the specified public key associated with the organization." +
                    "<p>The returned keys are serialized using PEM encoding.")
    @ApiResponses(@ApiResponse(code = 404, message = "Cannot find public key for organization"))
    @Override
    public PublicKeyEntity getPublicKey(@ApiParam(hidden = true) @Auth OrganizationPrincipal organizationPrincipal, @NotNull @PathParam(value = "keyID") UUID keyID) {
        final List<PublicKeyEntity> publicKeys = this.dao.publicKeySearch(keyID, organizationPrincipal.getID());
        if (publicKeys.isEmpty()) {
            throw new WebApplicationException("Cannot find public key", Response.Status.NOT_FOUND);
        }
        return publicKeys.get(0);
    }

    @DELETE
    @Timed
    @ExceptionMetered
    @Path("/{keyID}")
    @UnitOfWork
    @Produces(MediaType.APPLICATION_JSON)
    @Authorizer
    @ApiOperation(value = "Delete public key for Organization",
            notes = "This endpoint deletes the specified public key associated with the organization.")
    @ApiResponses(value = {
            @ApiResponse(code = 404, message = "Cannot find public key for organization"),
            @ApiResponse(code = 200, message = "Key successfully removed")
    })
    @Override
    public Response deletePublicKey(@ApiParam(hidden = true) @Auth OrganizationPrincipal organizationPrincipal, @NotNull @PathParam(value = "keyID") UUID keyID) {
        final List<PublicKeyEntity> keys = this.dao.publicKeySearch(keyID, organizationPrincipal.getID());

        if (keys.isEmpty()) {
            throw new WebApplicationException("Cannot find certificate", Response.Status.NOT_FOUND);
        }
        keys.forEach(this.dao::deletePublicKey);

        return Response.ok().build();
    }

    @POST
    @Timed
    @ExceptionMetered
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    @Authorizer
    @ApiOperation(value = "Register public key for Organization",
            notes = "This endpoint registers the provided public key with the organization." +
                    "<p>The provided key MUST be PEM encoded." +
                    "<p>RSA keys of 4096-bits or greater are supported, as well as ECC keys using one of the following curves:" +
                    "- secp256r1" +
                    "- secp384r1")
    @ApiResponses(@ApiResponse(code = 400, message = "Public key is not valid."))
    @UnitOfWork
    @Override
    public PublicKeyEntity submitKey(@ApiParam(hidden = true) @Auth OrganizationPrincipal organizationPrincipal,
                                     @ApiParam KeySignature keySignature,
                                     @ApiParam(name = "label", value = "Public Key Label (cannot be more than 25 characters in length)", defaultValue = "key:{random integer}", allowableValues = "range[-infinity, 25]")
                                     @QueryParam(value = "label") Optional<String> keyLabelOptional) {
        final String keyLabel;
        if (keyLabelOptional.isPresent()) {
            if (keyLabelOptional.get().length() > 25) {
                throw new WebApplicationException("Key label cannot be more than 25 characters", Response.Status.BAD_REQUEST);
            }
            keyLabel = keyLabelOptional.get();
        } else {
            keyLabel = this.buildDefaultKeyID();
        }

        final String key = keySignature.getKey();
        final String signature = keySignature.getSignature();

        final SubjectPublicKeyInfo publicKey = parseAndValidateKey(key, signature);
        return savePublicKeyEntry(organizationPrincipal, keyLabel, publicKey);
    }

    private SubjectPublicKeyInfo parseAndValidateKey(String publicKeyPem, String sigStr) {
        final SubjectPublicKeyInfo publicKeyInfo;
        try {
            publicKeyInfo = PublicKeyHandler.parsePEMString(publicKeyPem);
        } catch (PublicKeyException e) {
            logger.error("Cannot parse provided public key.", e);
            throw new WebApplicationException("Public key could not be parsed", Response.Status.BAD_REQUEST);
        }

        if (PublicKeyHandler.ECC_KEY.equals(publicKeyInfo.getAlgorithm().getAlgorithm())) {
            throw new WebApplicationException("ECC keys are not currently supported", HttpStatus.UNPROCESSABLE_ENTITY_422);
        }

        // Validate public key
        try {
            PublicKeyHandler.validatePublicKey(publicKeyInfo);
        } catch (PublicKeyException e) {
            logger.error("Cannot validate provided public key.", e);
            throw new WebApplicationException("Public key is not valid", Response.Status.BAD_REQUEST);
        }

        try {
            PublicKeyHandler.verifySignature(publicKeyPem, SNIPPET, sigStr);
        } catch (PublicKeyException e) {
            logger.error("Cannot verify public key with signature.", e);
            throw new WebApplicationException("Public key could not be verified", Response.Status.BAD_REQUEST);
        }

        return publicKeyInfo;
    }

    private PublicKeyEntity savePublicKeyEntry(OrganizationPrincipal organizationPrincipal, String keyLabel, SubjectPublicKeyInfo publicKey) {
        final PublicKeyEntity publicKeyEntity = new PublicKeyEntity();
        final OrganizationEntity organizationEntity = new OrganizationEntity();
        organizationEntity.setId(organizationPrincipal.getID());

        publicKeyEntity.setOrganization_id(organizationEntity.getId());
        publicKeyEntity.setId(UUID.randomUUID());
        publicKeyEntity.setPublicKey(publicKey);
        publicKeyEntity.setLabel(keyLabel);

        return this.dao.persistPublicKey(publicKeyEntity);
    }

    private String buildDefaultKeyID() {
        final int newKeyID = this.random.nextInt();
        return String.format("key:%d", newKeyID);
    }

    public static class KeySignature {
        @NoHtml
        @NotEmpty
        private String key;
        @NoHtml
        @NotEmpty
        private String signature;

        public KeySignature() {}

        public KeySignature(String key, String signature) {
            this.key = key;
            this.signature = signature;
        }

        public String getKey() {
            return key;
        }

        public String getSignature() {
            return signature;
        }
    }
}