ECDSASigningHandler.java

/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2016 ForgeRock AS.
 */

package org.forgerock.json.jose.jws.handlers;

import java.nio.ByteBuffer;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.Signature;
import java.security.SignatureException;
import java.security.interfaces.ECKey;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.util.Arrays;

import org.forgerock.json.jose.exceptions.JwsException;
import org.forgerock.json.jose.exceptions.JwsSigningException;
import org.forgerock.json.jose.jws.JwsAlgorithm;
import org.forgerock.json.jose.jws.JwsAlgorithmType;
import org.forgerock.json.jose.jws.SupportedEllipticCurve;
import org.forgerock.json.jose.utils.DerUtils;
import org.forgerock.json.jose.utils.Utils;
import org.forgerock.util.Reject;

/**
 * Elliptic Curve Digital Signature Algorithm (ECDSA) signing and verification.
 */
public class ECDSASigningHandler implements SigningHandler {
    private final ECPrivateKey signingKey;
    private final ECPublicKey verificationKey;
    private final SupportedEllipticCurve curve;

    /**
     * Constructs the ECDSA signing handler for signing only.
     *
     * @param signingKey the private key to use for signing. Must not be null.
     */
    public ECDSASigningHandler(final ECPrivateKey signingKey) {
        this.signingKey = signingKey;
        this.verificationKey = null;
        this.curve = validateKey(signingKey);
    }

    /**
     * Constructs the ECDSA signing handler for verification only.
     *
     * @param verificationKey the public key to use for verification. Must not be null.
     */
    public ECDSASigningHandler(final ECPublicKey verificationKey) {
        this.signingKey = null;
        this.verificationKey = verificationKey;
        this.curve = validateKey(verificationKey);
    }

    @Override
    public byte[] sign(final JwsAlgorithm algorithm, final String data) {
        return sign(algorithm, data.getBytes(Utils.CHARSET));
    }

    @Override
    public byte[] sign(final JwsAlgorithm algorithm, final byte[] data) {
        validateAlgorithm(algorithm);

        try {
            final Signature signature = Signature.getInstance(algorithm.getAlgorithm());
            signature.initSign(signingKey);
            signature.update(data);
            return derDecode(signature.sign(), curve.getSignatureSize());
        } catch (SignatureException | InvalidKeyException e) {
            throw new JwsSigningException(e);
        } catch (NoSuchAlgorithmException e) {
            throw new JwsSigningException("Unsupported Signing Algorithm, " + algorithm.getAlgorithm(), e);
        }
    }

    @Override
    public boolean verify(final JwsAlgorithm algorithm, final byte[] data, final byte[] signature) {
        validateAlgorithm(algorithm);

        try {
            final Signature validator = Signature.getInstance(algorithm.getAlgorithm());
            validator.initVerify(verificationKey);
            validator.update(data);
            return validator.verify(derEncode(signature));
        } catch (SignatureException | InvalidKeyException e) {
            throw new JwsSigningException(e);
        } catch (NoSuchAlgorithmException e) {
            throw new JwsSigningException("Unsupported Signing Algorithm, " + algorithm.getAlgorithm(), e);
        }
    }

    private void validateAlgorithm(JwsAlgorithm algorithm) {
        Reject.ifNull(algorithm, "Algorithm must not be null.");
        Reject.ifTrue(algorithm.getAlgorithmType() != JwsAlgorithmType.ECDSA, "Not an ECDSA algorithm.");
    }

    /**
     * Validate that the parameters of the key match the standard P-256 curve as required by the ES256 JWA standard.
     * @param key the key to validate.
     */
    private SupportedEllipticCurve validateKey(final ECKey key) {
        Reject.ifNull(key);
        try {
            return SupportedEllipticCurve.forKey(key);
        } catch (IllegalArgumentException ex) {
            throw new JwsException(ex);
        }
    }

    /**
     * Minimal DER decoder for the format returned by the SunEC signature provider.
     */
    private static byte[] derDecode(final byte[] signature, final int signatureSize) {
        final ByteBuffer buffer = ByteBuffer.wrap(signature);
        if (buffer.get() != DerUtils.SEQUENCE_TAG) {
            throw new JwsSigningException("Unable to decode DER signature");
        }
        // Skip overall size
        DerUtils.readLength(buffer);

        final byte[] output = new byte[signatureSize];
        final int componentSize = signatureSize >> 1;
        DerUtils.readUnsignedInteger(buffer, output, 0, componentSize);
        DerUtils.readUnsignedInteger(buffer, output, componentSize, componentSize);
        return output;
    }

    /**
     * Minimal DER encoder for the format expected by the SunEC signature provider.
     */
    private static byte[] derEncode(final byte[] signature) {
        Reject.ifNull(signature);
        SupportedEllipticCurve curve = SupportedEllipticCurve.forSignature(signature);

        int midPoint = curve.getSignatureSize() >> 1;
        final byte[] r = Arrays.copyOfRange(signature, 0, midPoint);
        final byte[] s = Arrays.copyOfRange(signature, midPoint, signature.length);

        // Each integer component needs at most 2 bytes for the length field and 1 byte for the tag, for a total of 6
        // bytes for both integers.
        final ByteBuffer params = ByteBuffer.allocate(signature.length + 6);
        DerUtils.writeInteger(params, r);
        DerUtils.writeInteger(params, s);

        final int size = params.position();
        // The overall sequence may need up to 4 bytes for the length field plus 1 byte for the sequence tag.
        final ByteBuffer sequence = ByteBuffer.allocate(size + 6);
        sequence.put(DerUtils.SEQUENCE_TAG);
        DerUtils.writeLength(sequence, size);
        sequence.put((ByteBuffer) params.flip());
        sequence.flip();
        final byte[] encodedSignature = new byte[sequence.remaining()];
        sequence.get(encodedSignature);
        return encodedSignature;
    }

}