Your posted code includes the "spki" format, which is not a 'raw' or 'bare' public key. Whatever was referenced in your comment but not shown appears to be incorrect as well.
Below are correct ways to handle this using Bouncy LWAPI and 4 alternatives with JCA (utilizing either the Bouncy provider or the standard Oracle/OpenJDK provider in Java 15 and up). Even if you don't use typescript, your 'typescript' essentially translates to nodejs. I have presented all data in base64: traditional base64 for data signature, spki-format key, and base64url for raw key - the latter being natively used by JWK and easily handled by Java. You could opt for hex or any other encoding method as long as you maintain consistency.
# nodejs input
const crypto = require('crypto');
const { privateKey, publicKey } = crypto.generateKeyPairSync('ed25519');
const data = Buffer.from(JSON.stringify({example:'test data'}));
console.log( data.toString('base64') );
console.log( crypto.sign(null,data,privateKey).toString('base64') );
console.log( publicKey.export({format:'der',type:'spki'}).toString('base64') );
console.log( publicKey.export({format:'jwk'}).x ); // base64urlsafe
# output stored in file and input to java below
eyJleGFtcGxlIjoidGVzdCBkYXRhIn0=
g2L2cSrMskh+p62HJN48AGefLzaKf8TyN/6IzaaYyWUeGoBm3OvibHFjtAtXlD0pm/ldaQJq/LOhUtJcbhWYCQ==
MCowBQYDK2VwAyEA+XYOwM61UpixNFD89bo4OViD6HCm0G6DQnmSYbky5Hs=
-XYOwM61UpixNFD89bo4OViD6HCm0G6DQnmSYbky5Hs
// nopackage
import java.io.*;
import java.math.BigInteger;
import java.security.spec.*;
import java.security.*;
import java.util.Base64;
import org.bouncycastle.crypto.Signer;
import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters;
import org.bouncycastle.crypto.signers.Ed25519Signer;
import org.bouncycastle.jcajce.spec.RawEncodedKeySpec;
public class SO76753558 {
public static void main (String[] args) throws Exception {
Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
BufferedReader br = new BufferedReader (new InputStreamReader (System.in));
byte[] data = Base64.getDecoder().decode(br.readLine()),
sig = Base64.getDecoder().decode(br.readLine()),
spki = Base64.getDecoder().decode(br.readLine()),
bare = Base64.getUrlDecoder().decode(br.readLine());
// Bouncy LWAPI
{
Signer v = new Ed25519Signer();
v.init(false, new Ed25519PublicKeyParameters(bare));
v.update(data, 0, data.length);
System.out.println ("LWAPI:" + v.verifySignature(sig));
}
// standard algorithm-specific; requires Java 15 up and not very convenient
{
byte[] rev = new byte[bare.length];
for( int i = 0; i<bare.length; i++ ){ rev[i] = bare[bare.length-1-i]; }
boolean hibit = (rev[0]&0x80)>0; rev[0] &= ~0x80;
EdECPublicKeySpec spec = new EdECPublicKeySpec(NamedParameterSpec.ED25519,
new EdECPoint (hibit, new BigInteger(1,rev)) );
KeyFactory f = KeyFactory.getInstance("Ed25519","SunEC");
Signature v = Signature.getInstance("Ed25519","SunEC");
v.initVerify(f.generatePublic(spec));
v.update(data);
System.out.println ("SunEC bare:"+ v.verify(sig));
}
// Bouncy algorithm-specific
{
KeyFactory f = KeyFactory.getInstance("Ed25519","BC");
Signature v = Signature.getInstance("Ed25519","BC");
v.initVerify(f.generatePublic(new RawEncodedKeySpec(bare)));
v.update(data);
System.out.println ("BC bare:"+ v.verify(sig));
}
// JCA generic; requires Java 15 up for SunEC
for( String provider : new String[]{ "SunEC", "BC" } ){
KeyFactory f = KeyFactory.getInstance("Ed25519",provider);
Signature v = Signature.getInstance("Ed25519",provider);
v.initVerify(f.generatePublic(new X509EncodedKeySpec(spki)));
v.update(data);
System.out.println (provider+" spki:"+ v.verify(sig));
}
}
}
Note that utilizing
new Ed25519PublicKeyParameters(spki)
will throw an exception indicating it is the wrong length, serving as a warning sign. Instead, applying the
(spki,0)
constructor may suppress the exception, but the outcome remains inaccurate. This is akin to masking tape over a faulty brake light on your dashboard - it doesn't alleviate the issue, just blinds you to the impending danger.