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