diff --git a/jetty-core/jetty-siwe/src/main/java/org/eclipse/jetty/security/siwe/EthereumAuthenticator.java b/jetty-core/jetty-siwe/src/main/java/org/eclipse/jetty/security/siwe/EthereumAuthenticator.java index d6ed143411f..6c70f00b240 100644 --- a/jetty-core/jetty-siwe/src/main/java/org/eclipse/jetty/security/siwe/EthereumAuthenticator.java +++ b/jetty-core/jetty-siwe/src/main/java/org/eclipse/jetty/security/siwe/EthereumAuthenticator.java @@ -188,14 +188,17 @@ public class EthereumAuthenticator extends LoginAuthenticator implements Dumpabl } /** - * This setting is only meaningful if a non-null {@link LoginService} has been set. + * Configures the behavior for authenticating users not found by a wrapped {@link LoginService}. *
- * If set to true, any users not found by the {@link LoginService} will still - * be authenticated but with no roles, if set to false users will not be - * authenticated unless they are discovered by the wrapped {@link LoginService}. + * This setting is only meaningful if a wrapped {@link LoginService} has been set. *
- * @param authenticateNewUsers whether to authenticate users not found by a wrapping LoginService - */ + *+ * If set to {@code true}, users not found by a wrapped {@link LoginService} will authenticated with no roles. + * If set to {@code false}, only users found by a wrapped {@link LoginService} will be authenticated. + *
+ * + * @param authenticateNewUsers whether to authenticate users not found by the wrapped {@link LoginService} + **/ public void setAuthenticateNewUsers(boolean authenticateNewUsers) { this._authenticateNewUsers = authenticateNewUsers; @@ -817,4 +820,12 @@ public class EthereumAuthenticator extends LoginAuthenticator implements Dumpabl return super.add(element); } } + + public record SignedMessage(String message, String signature) + { + public String recoverAddress() + { + return EthereumUtil.recoverAddress(this); + } + } } diff --git a/jetty-core/jetty-siwe/src/main/java/org/eclipse/jetty/security/siwe/SignedMessage.java b/jetty-core/jetty-siwe/src/main/java/org/eclipse/jetty/security/siwe/SignedMessage.java deleted file mode 100644 index b54721fb3b3..00000000000 --- a/jetty-core/jetty-siwe/src/main/java/org/eclipse/jetty/security/siwe/SignedMessage.java +++ /dev/null @@ -1,24 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. -// -// This program and the accompanying materials are made available under the -// terms of the Eclipse Public License v. 2.0 which is available at -// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 -// which is available at https://www.apache.org/licenses/LICENSE-2.0. -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// ======================================================================== -// - -package org.eclipse.jetty.security.siwe; - -import org.eclipse.jetty.security.siwe.internal.EthereumSignatureVerifier; - -public record SignedMessage(String message, String signature) -{ - public String recoverAddress() - { - return EthereumSignatureVerifier.recoverAddress(this); - } -} diff --git a/jetty-core/jetty-siwe/src/main/java/org/eclipse/jetty/security/siwe/internal/EthereumSignatureVerifier.java b/jetty-core/jetty-siwe/src/main/java/org/eclipse/jetty/security/siwe/internal/EthereumSignatureVerifier.java deleted file mode 100644 index 5595d359f5d..00000000000 --- a/jetty-core/jetty-siwe/src/main/java/org/eclipse/jetty/security/siwe/internal/EthereumSignatureVerifier.java +++ /dev/null @@ -1,139 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. -// -// This program and the accompanying materials are made available under the -// terms of the Eclipse Public License v. 2.0 which is available at -// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 -// which is available at https://www.apache.org/licenses/LICENSE-2.0. -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// ======================================================================== -// - -package org.eclipse.jetty.security.siwe.internal; - -import java.math.BigInteger; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; - -import org.bouncycastle.asn1.x9.X9ECParameters; -import org.bouncycastle.asn1.x9.X9IntegerConverter; -import org.bouncycastle.crypto.ec.CustomNamedCurves; -import org.bouncycastle.crypto.params.ECDomainParameters; -import org.bouncycastle.jcajce.provider.digest.Keccak; -import org.bouncycastle.math.ec.ECAlgorithms; -import org.bouncycastle.math.ec.ECPoint; -import org.eclipse.jetty.security.siwe.SignedMessage; -import org.eclipse.jetty.util.StringUtil; - -/** - * Used to recover an Ethereum address from a message and signature. - *- * This uses algorithms and terminology defined in EIP-191 and - * ECDSA. - *
- */ -public class EthereumSignatureVerifier -{ - public static final String PREFIX = "\u0019Ethereum Signed Message:\n"; - - private static final int ADDRESS_LENGTH_BYTES = 20; - private static final X9ECParameters SEC_P256K1_PARAMS = CustomNamedCurves.getByName("secp256k1"); - private static final ECDomainParameters DOMAIN_PARAMS = new ECDomainParameters( - SEC_P256K1_PARAMS.getCurve(), SEC_P256K1_PARAMS.getG(), SEC_P256K1_PARAMS.getN(), SEC_P256K1_PARAMS.getH()); - private static final BigInteger PRIME = SEC_P256K1_PARAMS.getCurve().getField().getCharacteristic(); - private static final X9IntegerConverter INT_CONVERTER = new X9IntegerConverter(); - private static final Charset CHARSET = StandardCharsets.UTF_8; - - private EthereumSignatureVerifier() - { - } - - /** - * Recover the Ethereum Address from the {@link SignedMessage}. - * @param signedMessage the signed message used to recover the address. - * @return the ethereum address recovered from the signature. - */ - public static String recoverAddress(SignedMessage signedMessage) - { - String siweMessage = signedMessage.message(); - String signatureHex = signedMessage.signature(); - if (StringUtil.asciiStartsWithIgnoreCase(signatureHex, "0x")) - signatureHex = signatureHex.substring(2); - - int messageLength = siweMessage.getBytes(CHARSET).length; - String prefixedMessage = PREFIX + messageLength + siweMessage; - byte[] messageHash = keccak256(prefixedMessage.getBytes(CHARSET)); - byte[] signatureBytes = StringUtil.fromHexString(signatureHex); - - BigInteger r = new BigInteger(1, Arrays.copyOfRange(signatureBytes, 0, 32)); - BigInteger s = new BigInteger(1, Arrays.copyOfRange(signatureBytes, 32, 64)); - byte v = (byte)(signatureBytes[64] < 27 ? signatureBytes[64] : signatureBytes[64] - 27); - - ECPoint qPoint = ecRecover(messageHash, v, r, s); - if (qPoint == null) - return null; - return toAddress(qPoint); - } - - public static ECPoint ecRecover(byte[] hash, int v, BigInteger r, BigInteger s) - { - if (v < 0 || v >= 4) - throw new IllegalArgumentException("Invalid v value: " + v); - - // Verify that r and s are integers in [1, n-1]. If not, the signature is invalid. - BigInteger n = DOMAIN_PARAMS.getN(); - if (r.compareTo(BigInteger.ONE) < 0 || r.compareTo(n.subtract(BigInteger.ONE)) > 0) - return null; - if (s.compareTo(BigInteger.ONE) < 0 || s.compareTo(n.subtract(BigInteger.ONE)) > 0) - return null; - - // Calculate the curve point R. - BigInteger x = r.add(BigInteger.valueOf(v / 2).multiply(n)); - if (x.compareTo(PRIME) >= 0) - return null; - ECPoint rPoint = decodePoint(x, v); - if (!rPoint.multiply(n).isInfinity()) - return null; - - // Calculate the curve point Q = u1 * G + u2 * R, where u1=-zr^(-1)%n and u2=sr^(-1)%n. - // Note: for secp256k1 z=e as the hash is 256 bits and z is defined as the Ln leftmost bits of e. - BigInteger e = new BigInteger(1, hash); - BigInteger rInv = r.modInverse(n); - BigInteger u1 = e.negate().multiply(rInv).mod(n); - BigInteger u2 = s.multiply(rInv).mod(n); - return ECAlgorithms.sumOfTwoMultiplies(DOMAIN_PARAMS.getG(), u1, rPoint, u2); - } - - public static String toAddress(ECPoint point) - { - // Remove the 1-byte prefix and return the public key as an ethereum address. - byte[] qBytes = point.getEncoded(false); - byte[] qHash = keccak256(qBytes, 1, qBytes.length - 1); - byte[] address = new byte[ADDRESS_LENGTH_BYTES]; - System.arraycopy(qHash, qHash.length - ADDRESS_LENGTH_BYTES, address, 0, ADDRESS_LENGTH_BYTES); - return "0x" + StringUtil.toHexString(address); - } - - public static ECPoint decodePoint(BigInteger p, int v) - { - byte[] encodedPoint = INT_CONVERTER.integerToBytes(p, 1 + INT_CONVERTER.getByteLength(DOMAIN_PARAMS.getCurve())); - encodedPoint[0] = (byte)((v % 2) == 0 ? 0x02 : 0x03); - return DOMAIN_PARAMS.getCurve().decodePoint(encodedPoint); - } - - public static byte[] keccak256(byte[] bytes) - { - Keccak.Digest256 digest256 = new Keccak.Digest256(); - return digest256.digest(bytes); - } - - public static byte[] keccak256(byte[] buf, int offset, int len) - { - Keccak.Digest256 digest256 = new Keccak.Digest256(); - digest256.update(buf, offset, len); - return digest256.digest(); - } -} diff --git a/jetty-core/jetty-siwe/src/main/java/org/eclipse/jetty/security/siwe/internal/EthereumUtil.java b/jetty-core/jetty-siwe/src/main/java/org/eclipse/jetty/security/siwe/internal/EthereumUtil.java index e75b0370167..9e915f107fe 100644 --- a/jetty-core/jetty-siwe/src/main/java/org/eclipse/jetty/security/siwe/internal/EthereumUtil.java +++ b/jetty-core/jetty-siwe/src/main/java/org/eclipse/jetty/security/siwe/internal/EthereumUtil.java @@ -13,17 +13,129 @@ package org.eclipse.jetty.security.siwe.internal; +import java.math.BigInteger; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.security.SecureRandom; +import java.util.Arrays; + +import org.bouncycastle.asn1.x9.X9ECParameters; +import org.bouncycastle.asn1.x9.X9IntegerConverter; +import org.bouncycastle.crypto.ec.CustomNamedCurves; +import org.bouncycastle.crypto.params.ECDomainParameters; +import org.bouncycastle.jcajce.provider.digest.Keccak; +import org.bouncycastle.math.ec.ECAlgorithms; +import org.bouncycastle.math.ec.ECPoint; +import org.eclipse.jetty.security.siwe.EthereumAuthenticator; +import org.eclipse.jetty.util.StringUtil; public class EthereumUtil { + public static final String PREFIX = "\u0019Ethereum Signed Message:\n"; private static final String NONCE_CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; private static final SecureRandom RANDOM = new SecureRandom(); + private static final int ADDRESS_LENGTH_BYTES = 20; + private static final X9ECParameters SEC_P256K1_PARAMS = CustomNamedCurves.getByName("secp256k1"); + private static final ECDomainParameters DOMAIN_PARAMS = new ECDomainParameters( + SEC_P256K1_PARAMS.getCurve(), SEC_P256K1_PARAMS.getG(), SEC_P256K1_PARAMS.getN(), SEC_P256K1_PARAMS.getH()); + private static final BigInteger PRIME = SEC_P256K1_PARAMS.getCurve().getField().getCharacteristic(); + private static final X9IntegerConverter INT_CONVERTER = new X9IntegerConverter(); + private static final Charset CHARSET = StandardCharsets.UTF_8; private EthereumUtil() { } + /** + * Recover the Ethereum Address from the {@link EthereumAuthenticator.SignedMessage}. + *+ * This uses algorithms and terminology defined in EIP-191 and + * ECDSA. + *
+ * @param signedMessage the signed message used to recover the address. + * @return the ethereum address recovered from the signature. + */ + public static String recoverAddress(EthereumAuthenticator.SignedMessage signedMessage) + { + String siweMessage = signedMessage.message(); + String signatureHex = signedMessage.signature(); + if (StringUtil.asciiStartsWithIgnoreCase(signatureHex, "0x")) + signatureHex = signatureHex.substring(2); + + int messageLength = siweMessage.getBytes(CHARSET).length; + String prefixedMessage = PREFIX + messageLength + siweMessage; + byte[] messageHash = keccak256(prefixedMessage.getBytes(CHARSET)); + byte[] signatureBytes = StringUtil.fromHexString(signatureHex); + + BigInteger r = new BigInteger(1, Arrays.copyOfRange(signatureBytes, 0, 32)); + BigInteger s = new BigInteger(1, Arrays.copyOfRange(signatureBytes, 32, 64)); + byte v = (byte)(signatureBytes[64] < 27 ? signatureBytes[64] : signatureBytes[64] - 27); + + ECPoint qPoint = ecRecover(messageHash, v, r, s); + if (qPoint == null) + return null; + return toAddress(qPoint); + } + + public static ECPoint ecRecover(byte[] hash, int v, BigInteger r, BigInteger s) + { + if (v < 0 || v >= 4) + throw new IllegalArgumentException("Invalid v value: " + v); + + // Verify that r and s are integers in [1, n-1]. If not, the signature is invalid. + BigInteger n = DOMAIN_PARAMS.getN(); + if (r.compareTo(BigInteger.ONE) < 0 || r.compareTo(n.subtract(BigInteger.ONE)) > 0) + return null; + if (s.compareTo(BigInteger.ONE) < 0 || s.compareTo(n.subtract(BigInteger.ONE)) > 0) + return null; + + // Calculate the curve point R. + BigInteger x = r.add(BigInteger.valueOf(v / 2).multiply(n)); + if (x.compareTo(PRIME) >= 0) + return null; + ECPoint rPoint = decodePoint(x, v); + if (!rPoint.multiply(n).isInfinity()) + return null; + + // Calculate the curve point Q = u1 * G + u2 * R, where u1=-zr^(-1)%n and u2=sr^(-1)%n. + // Note: for secp256k1 z=e as the hash is 256 bits and z is defined as the Ln leftmost bits of e. + BigInteger e = new BigInteger(1, hash); + BigInteger rInv = r.modInverse(n); + BigInteger u1 = e.negate().multiply(rInv).mod(n); + BigInteger u2 = s.multiply(rInv).mod(n); + return ECAlgorithms.sumOfTwoMultiplies(DOMAIN_PARAMS.getG(), u1, rPoint, u2); + } + + public static String toAddress(ECPoint point) + { + // Remove the 1-byte prefix and return the public key as an ethereum address. + byte[] qBytes = point.getEncoded(false); + byte[] qHash = keccak256(qBytes, 1, qBytes.length - 1); + byte[] address = new byte[ADDRESS_LENGTH_BYTES]; + System.arraycopy(qHash, qHash.length - ADDRESS_LENGTH_BYTES, address, 0, ADDRESS_LENGTH_BYTES); + return "0x" + StringUtil.toHexString(address); + } + + public static ECPoint decodePoint(BigInteger p, int v) + { + byte[] encodedPoint = INT_CONVERTER.integerToBytes(p, 1 + INT_CONVERTER.getByteLength(DOMAIN_PARAMS.getCurve())); + encodedPoint[0] = (byte)((v % 2) == 0 ? 0x02 : 0x03); + return DOMAIN_PARAMS.getCurve().decodePoint(encodedPoint); + } + + public static byte[] keccak256(byte[] bytes) + { + Keccak.Digest256 digest256 = new Keccak.Digest256(); + return digest256.digest(bytes); + } + + public static byte[] keccak256(byte[] buf, int offset, int len) + { + Keccak.Digest256 digest256 = new Keccak.Digest256(); + digest256.update(buf, offset, len); + return digest256.digest(); + } + public static String createNonce() { StringBuilder builder = new StringBuilder(8); diff --git a/jetty-core/jetty-siwe/src/main/java/org/eclipse/jetty/security/siwe/internal/SignInWithEthereumToken.java b/jetty-core/jetty-siwe/src/main/java/org/eclipse/jetty/security/siwe/internal/SignInWithEthereumToken.java index b351a05b4a3..ea8ecdfe6e5 100644 --- a/jetty-core/jetty-siwe/src/main/java/org/eclipse/jetty/security/siwe/internal/SignInWithEthereumToken.java +++ b/jetty-core/jetty-siwe/src/main/java/org/eclipse/jetty/security/siwe/internal/SignInWithEthereumToken.java @@ -18,7 +18,7 @@ import java.time.format.DateTimeFormatter; import java.util.function.Predicate; import org.eclipse.jetty.security.ServerAuthException; -import org.eclipse.jetty.security.siwe.SignedMessage; +import org.eclipse.jetty.security.siwe.EthereumAuthenticator; import org.eclipse.jetty.util.IncludeExcludeSet; import org.eclipse.jetty.util.StringUtil; @@ -54,13 +54,13 @@ public record SignInWithEthereumToken(String scheme, { /** - * @param signedMessage the {@link SignedMessage}. + * @param signedMessage the {@link EthereumAuthenticator.SignedMessage}. * @param validateNonce a {@link Predicate} used to validate the nonce. * @param domains the {@link IncludeExcludeSet} used to validate the domain. * @param chainIds the {@link IncludeExcludeSet} used to validate the chainId. - * @throws ServerAuthException if the {@link SignedMessage} fails validation. + * @throws ServerAuthException if the {@link EthereumAuthenticator.SignedMessage} fails validation. */ - public void validate(SignedMessage signedMessage, Predicate