001/*
002 * nimbus-jose-jwt
003 *
004 * Copyright 2012-2016, Connect2id Ltd.
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.jose.crypto;
019
020
021import java.security.InvalidKeyException;
022import java.security.PrivateKey;
023import java.security.Signature;
024import java.security.SignatureException;
025import java.security.interfaces.ECPrivateKey;
026import java.util.Collections;
027import java.util.Set;
028
029import net.jcip.annotations.ThreadSafe;
030
031import com.nimbusds.jose.*;
032import com.nimbusds.jose.crypto.impl.AlgorithmSupportMessage;
033import com.nimbusds.jose.crypto.impl.ECDSA;
034import com.nimbusds.jose.crypto.impl.ECDSAProvider;
035import com.nimbusds.jose.crypto.opts.OptionUtils;
036import com.nimbusds.jose.crypto.opts.UserAuthenticationRequired;
037import com.nimbusds.jose.jwk.Curve;
038import com.nimbusds.jose.jwk.ECKey;
039import com.nimbusds.jose.util.Base64URL;
040
041
042/**
043 * Elliptic Curve Digital Signature Algorithm (ECDSA) signer of
044 * {@link com.nimbusds.jose.JWSObject JWS objects}. Expects a private EC key
045 * (with a P-256, P-384, P-521 or secp256k1 curve).
046 *
047 * <p>See RFC 7518
048 * <a href="https://tools.ietf.org/html/rfc7518#section-3.4">section 3.4</a>
049 * for more information.
050 *
051 * <p>This class is thread-safe.
052 *
053 * <p>Supports the following algorithms:
054 *
055 * <ul>
056 *     <li>{@link com.nimbusds.jose.JWSAlgorithm#ES256}
057 *     <li>{@link com.nimbusds.jose.JWSAlgorithm#ES256K}
058 *     <li>{@link com.nimbusds.jose.JWSAlgorithm#ES384}
059 *     <li>{@link com.nimbusds.jose.JWSAlgorithm#ES512}
060 * </ul>
061 *
062 * <p>Supports the following {@link JWSSignerOption options}:
063 *
064 * <ul>
065 *     <li>{@link UserAuthenticationRequired} -- to prompt the user to
066 *         authenticate in order to complete the signing operation. Android
067 *         applications can use this option to trigger a biometric prompt that
068 *         is required to unlock a private key created with
069 *         {@code setUserAuthenticationRequired(true)}.
070 * </ul>
071 *
072 * @author Axel Nennker
073 * @author Vladimir Dzhuvinov
074 * @version 2023-04-20
075 */
076@ThreadSafe
077public class ECDSASigner extends ECDSAProvider implements JWSSigner {
078
079
080        /**
081         * The private EC key. Represented by generic private key interface to
082         * support key stores that prevent exposure of the private key
083         * parameters via the {@link java.security.interfaces.ECPrivateKey}
084         * API.
085         *
086         * See https://bitbucket.org/connect2id/nimbus-jose-jwt/issues/169
087         */
088        private final PrivateKey privateKey;
089
090
091        /**
092         * The configured options, empty set if none.
093         */
094        private final Set<JWSSignerOption> opts;
095
096
097        /**
098         * Creates a new Elliptic Curve Digital Signature Algorithm (ECDSA)
099         * signer.
100         *
101         * @param privateKey The private EC key. Must not be {@code null}.
102         *
103         * @throws JOSEException If the elliptic curve of key is not supported.
104         */
105        public ECDSASigner(final ECPrivateKey privateKey)
106                throws JOSEException {
107
108                this(privateKey, (Set<JWSSignerOption>) null);
109        }
110
111
112        /**
113         * Creates a new Elliptic Curve Digital Signature Algorithm (ECDSA)
114         * signer.
115         *
116         * @param privateKey The private EC key. Must not be {@code null}.
117         * @param opts       The signing options, empty or {@code null} if
118         *                   none.
119         *
120         * @throws JOSEException If the elliptic curve of key is not supported.
121         */
122        public ECDSASigner(final ECPrivateKey privateKey, final Set<JWSSignerOption> opts)
123                        throws JOSEException {
124
125                super(ECDSA.resolveAlgorithm(privateKey));
126
127                this.privateKey = privateKey;
128                this.opts = opts != null ? opts : Collections.<JWSSignerOption>emptySet();
129        }
130
131
132        /**
133         * Creates a new Elliptic Curve Digital Signature Algorithm (ECDSA)
134         * signer. This constructor is intended for a private EC key located
135         * in a PKCS#11 store that doesn't expose the private key parameters
136         * (such as a smart card or HSM).
137         *
138         * @param privateKey The private EC key. Its algorithm must be "EC".
139         *                   Must not be {@code null}.
140         * @param curve      The elliptic curve for the key. Must not be
141         *                   {@code null}.
142         *
143         * @throws JOSEException If the elliptic curve of key is not supported.
144         */
145        public ECDSASigner(final PrivateKey privateKey, final Curve curve)
146                throws JOSEException {
147                this(privateKey, curve, null);
148        }
149
150
151        /**
152         * Creates a new Elliptic Curve Digital Signature Algorithm (ECDSA)
153         * signer. This constructor is intended for a private EC key located
154         * in a PKCS#11 store that doesn't expose the private key parameters
155         * (such as a smart card or HSM).
156         *
157         * @param privateKey The private EC key. Its algorithm must be "EC".
158         *                   Must not be {@code null}.
159         * @param curve      The elliptic curve for the key. Must not be
160         *                   {@code null}.
161         * @param opts       The signing options, empty or {@code null} if
162         *                   none.
163         *
164         * @throws JOSEException If the elliptic curve of key is not supported.
165         */
166        public ECDSASigner(final PrivateKey privateKey, final Curve curve, final Set<JWSSignerOption> opts)
167                        throws JOSEException {
168
169                super(ECDSA.resolveAlgorithm(curve));
170
171                if (! "EC".equalsIgnoreCase(privateKey.getAlgorithm())) {
172                        throw new IllegalArgumentException("The private key algorithm must be EC");
173                }
174
175                this.privateKey = privateKey;
176                this.opts = opts != null ? opts : Collections.<JWSSignerOption>emptySet();
177        }
178
179
180        /**
181         * Creates a new Elliptic Curve Digital Signature Algorithm (ECDSA)
182         * signer.
183         *
184         * @param ecJWK The EC JSON Web Key (JWK). Must contain a private part.
185         *              Must not be {@code null}.
186         *
187         * @throws JOSEException If the EC JWK doesn't contain a private part,
188         *                       its extraction failed, or the elliptic curve
189         *                       is not supported.
190         */
191        public ECDSASigner(final ECKey ecJWK)
192                throws JOSEException {
193                this(ecJWK, null);
194        }
195
196
197        /**
198         * Creates a new Elliptic Curve Digital Signature Algorithm (ECDSA)
199         * signer.
200         *
201         * @param ecJWK The EC JSON Web Key (JWK). Must contain a private part.
202         *              Must not be {@code null}.
203         * @param opts  The signing options, empty or {@code null} if
204         *              none.
205         *
206         * @throws JOSEException If the EC JWK doesn't contain a private part,
207         *                       its extraction failed, or the elliptic curve
208         *                       is not supported.
209         */
210        public ECDSASigner(final ECKey ecJWK, final Set<JWSSignerOption> opts)
211                        throws JOSEException {
212
213                super(ECDSA.resolveAlgorithm(ecJWK.getCurve()));
214
215                if (! ecJWK.isPrivate()) {
216                        throw new JOSEException("The EC JWK doesn't contain a private part");
217                }
218
219                privateKey = ecJWK.toPrivateKey();
220                this.opts = opts != null ? opts : Collections.<JWSSignerOption>emptySet();
221        }
222
223
224        /**
225         * Gets the private EC key.
226         *
227         * @return The private EC key. Casting to
228         *         {@link java.security.interfaces.ECPrivateKey} may not be
229         *         possible if the key is located in a PKCS#11 store that
230         *         doesn't expose the private key parameters.
231         */
232        public PrivateKey getPrivateKey() {
233
234                return privateKey;
235        }
236
237
238        @Override
239        public Base64URL sign(final JWSHeader header, final byte[] signingInput)
240                throws JOSEException {
241
242                final JWSAlgorithm alg = header.getAlgorithm();
243
244                if (! supportedJWSAlgorithms().contains(alg)) {
245                        throw new JOSEException(AlgorithmSupportMessage.unsupportedJWSAlgorithm(alg, supportedJWSAlgorithms()));
246                }
247
248                // DER-encoded signature, according to JCA spec
249                final byte[] jcaSignature;
250                try {
251                        final Signature dsa = ECDSA.getSignerAndVerifier(alg, getJCAContext().getProvider());
252                        dsa.initSign(privateKey, getJCAContext().getSecureRandom());
253
254                        if (OptionUtils.optionIsPresent(opts, UserAuthenticationRequired.class)) {
255
256                                throw new ActionRequiredForJWSCompletionException(
257                                                "Authenticate user to complete signing",
258                                                UserAuthenticationRequired.getInstance(),
259                                                new CompletableJWSObjectSigning() {
260                                                        @Override
261                                                        public Signature getInitializedSignature() {
262                                                                return dsa;
263                                                        }
264
265                                                        @Override
266                                                        public Base64URL complete() throws JOSEException {
267
268                                                                try {
269                                                                        dsa.update(signingInput);
270                                                                        final byte[] jcaSignature = dsa.sign();
271                                                                        final int rsByteArrayLength = ECDSA.getSignatureByteArrayLength(header.getAlgorithm());
272                                                                        final byte[] jwsSignature = ECDSA.transcodeSignatureToConcat(jcaSignature, rsByteArrayLength);
273                                                                        return Base64URL.encode(jwsSignature);
274                                                                } catch (SignatureException e) {
275
276                                                                throw new JOSEException(e.getMessage(), e);
277                                                                }
278                                                        }
279                                                }
280                                );
281                        }
282                        dsa.update(signingInput);
283                        jcaSignature = dsa.sign();
284
285                } catch (InvalidKeyException | SignatureException e) {
286
287                        throw new JOSEException(e.getMessage(), e);
288                }
289
290                final int rsByteArrayLength = ECDSA.getSignatureByteArrayLength(header.getAlgorithm());
291                final byte[] jwsSignature = ECDSA.transcodeSignatureToConcat(jcaSignature, rsByteArrayLength);
292                return Base64URL.encode(jwsSignature);
293        }
294}