package jstellarapi.keys;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import jstellarapi.core.StellarPrivateKey;
import jstellarapi.serialization.StellarBinaryObject;
import jstellarapi.serialization.StellarBinarySchema.BinaryFormatField;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.DERInteger;
import org.bouncycastle.asn1.DERSequenceGenerator;
import org.bouncycastle.asn1.DLSequence;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.crypto.signers.ECDSASigner;
import org.bouncycastle.math.ec.ECPoint;
public class StellarSigner {
StellarPrivateKey privateKey;
public StellarSigner(StellarPrivateKey privateKey) {
this.privateKey=privateKey;
}
/**
* see https://Stellar.com/forum/viewtopic.php?f=2&t=3206&p=13277&hilit=json+rpc#p13277
* see https://Stellar.com/wiki/User:Singpolyma/Transaction_Signing
* @param serObjToSign
* @return
* @throws Exception
*/
public StellarBinaryObject sign(StellarBinaryObject serObjToSign) throws Exception {
if(serObjToSign.getField(BinaryFormatField.TxnSignature)!=null){
throw new Exception("Object already signed");
}
StellarBinaryObject signedRBO = new StellarBinaryObject(serObjToSign);
signedRBO.putField(BinaryFormatField.SigningPubKey, privateKey.getPublicKey().getPublicPoint().getEncoded());
byte[] hashOfRBOBytes = signedRBO.generateHashFromBinaryObject();
ECDSASignature signature = signHash(hashOfRBOBytes);
//Add the Signature to the serializedObject as a field
signedRBO.putField(BinaryFormatField.TxnSignature, signature.encodeToDER());
return signedRBO;
}
private ECDSASignature signHash(byte[] hashOfBytes) throws Exception {
if(hashOfBytes.length!=32){
throw new RuntimeException("can sign only a hash of 32 bytes");
}
ECDSASigner signer = new ECDSASigner();
ECPrivateKeyParameters privKey = privateKey.getECPrivateKey();
signer.init(true, privKey);
BigInteger[] RandS = signer.generateSignature(hashOfBytes);
return new ECDSASignature(RandS[0], RandS[1], privateKey.getPublicKey().getPublicPoint());
}
public boolean isSignatureVerified(StellarBinaryObject serObj) {
try {
byte[] signatureBytes= (byte[]) serObj.getField(BinaryFormatField.TxnSignature);
if(signatureBytes==null){
throw new RuntimeException("The specified has no signature");
}
byte[] signingPubKeyBytes = (byte[]) serObj.getField(BinaryFormatField.SigningPubKey);
if(signingPubKeyBytes==null){
throw new RuntimeException("The specified has no public key associated to the signature");
}
StellarBinaryObject unsignedRBO = serObj.getUnsignedCopy();
byte[] hashToVerify = unsignedRBO.generateHashFromBinaryObject();
ECDSASigner signer = new ECDSASigner();
ECDSASignature signature = new ECDSASignature(signatureBytes, signingPubKeyBytes);
signer.init(false, new ECPublicKeyParameters(signature.publicSigningKey, StellarDeterministicKeyGenerator.SECP256K1_PARAMS));
return signer.verifySignature(hashToVerify, signature.r, signature.s);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static class ECDSASignature {
public BigInteger r, s;
private ECPoint publicSigningKey;
public ECDSASignature(BigInteger r, BigInteger s, ECPoint publicSigningKey) {
this.r = r;
this.s = s;
this.publicSigningKey= publicSigningKey;
}
public ECDSASignature(byte[] signatureDEREncodedBytes, byte[] signingPubKey) throws IOException {
publicSigningKey = StellarDeterministicKeyGenerator.SECP256K1_PARAMS.getCurve().decodePoint(signingPubKey);
ASN1InputStream decoder = new ASN1InputStream(signatureDEREncodedBytes);
DLSequence seq = (DLSequence) decoder.readObject();
DERInteger r = (DERInteger) seq.getObjectAt(0);
DERInteger s = (DERInteger) seq.getObjectAt(1);
// OpenSSL deviates from the DER spec by interpreting these values as unsigned, though they should not be
// Thus, we always use the positive versions.
// See: http://r6.ca/blog/20111119T211504Z.html
this.r = r.getPositiveValue();
this.s = s.getPositiveValue();
decoder.close();
}
/**
* What we get back from the signer are the two components of a signature, r and s. To get a flat byte stream
* of the type used by Bitcoin we have to encode them using DER encoding, which is just a way to pack the two
* components into a structure.
*/
public byte[] encodeToDER() {
try {
// Usually 70-72 bytes.
ByteArrayOutputStream bos = new ByteArrayOutputStream(72);
DERSequenceGenerator seq = new DERSequenceGenerator(bos);
seq.addObject(new DERInteger(r));
seq.addObject(new DERInteger(s));
seq.close();
return bos.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e); // Cannot happen.
}
}
}
}