Created
March 30, 2022 23:07
-
-
Save Mleekko/92889469ca723a5b3d581ceec66bef1a to your computer and use it in GitHub Desktop.
KeyManager - manage multiple signing keys for Radix APIs.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import com.radixdlt.crypto.ECDSASignature; | |
import com.radixdlt.crypto.ECKeyPair; | |
import com.radixdlt.crypto.ECPublicKey; | |
import com.radixdlt.crypto.exception.PrivateKeyException; | |
import com.radixdlt.crypto.exception.PublicKeyException; | |
import com.radixdlt.identifiers.REAddr; | |
import com.radixdlt.networks.Addressing; | |
import com.radixdlt.networks.Network; | |
import com.radixdlt.utils.Bytes; | |
import lombok.extern.slf4j.Slf4j; | |
import org.apache.commons.codec.DecoderException; | |
import org.apache.commons.codec.binary.Hex; | |
import org.bouncycastle.asn1.ASN1Encodable; | |
import org.bouncycastle.asn1.ASN1Integer; | |
import org.bouncycastle.asn1.ASN1OutputStream; | |
import org.bouncycastle.asn1.DLSequence; | |
import org.springframework.beans.factory.annotation.Value; | |
import org.springframework.stereotype.Component; | |
import radix.api.gateway.model.PublicKey; | |
import radix.api.gateway.model.Signature; | |
import javax.annotation.PostConstruct; | |
import java.io.ByteArrayOutputStream; | |
import java.io.File; | |
import java.io.FileInputStream; | |
import java.io.IOException; | |
import java.security.KeyStore; | |
import java.security.KeyStoreException; | |
import java.security.NoSuchAlgorithmException; | |
import java.security.cert.CertificateException; | |
import java.util.Enumeration; | |
import java.util.HashMap; | |
import java.util.Map; | |
import java.util.TreeSet; | |
/** | |
Requires `radixdlt-java-common` (https://github.com/radixdlt/radixdlt/tree/1.1.0/radixdlt-java-common), | |
also had to update `RadixKeyStore` to fix Bouncy Castle: | |
``` | |
static { | |
Provider provider = Security.getProvider(BouncyCastleProvider.PROVIDER_NAME); | |
System.out.println("Provider: " + provider); | |
if (provider == null) { | |
Security.insertProviderAt(new BouncyCastleProvider(), 1); | |
System.out.println("BouncyCastleProvider inserted."); | |
} | |
} | |
``` | |
*/ | |
@Slf4j | |
@Component | |
public class KeyManager { | |
private final Map<String, ECKeyPair> accountKeys = new HashMap<>(); | |
@Value("${app.keystore.file}") | |
private String keystoreFile; | |
@Value("${app.keystore.password}") | |
private String keystorePassword; | |
private Network network = Network.MAINNET; | |
public KeyManager() { | |
} | |
public KeyManager(String keystoreFile, String keystorePassword) { | |
this.keystoreFile = keystoreFile; | |
this.keystorePassword = keystorePassword; | |
} | |
@PostConstruct | |
public void init() throws KeyStoreException, CertificateException, IOException, NoSuchAlgorithmException, | |
com.radixdlt.crypto.exception.KeyStoreException, PrivateKeyException, PublicKeyException { | |
File file = new File(keystoreFile); | |
log.info("Loading keystore file: [{}]", file.getCanonicalPath()); | |
readKeystore(accountKeys, file, keystorePassword, network); | |
keystorePassword = null; | |
log.info("Loaded [{}] accounts: [{}]", accountKeys.size(), new TreeSet<>(accountKeys.keySet())); | |
} | |
public static void readKeystore(Map<String, ECKeyPair> accountKeys, File file, String password, Network network) | |
throws java.security.KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException, | |
com.radixdlt.crypto.exception.KeyStoreException, PrivateKeyException, PublicKeyException { | |
KeyStore ks = KeyStore.getInstance("pkcs12"); | |
try (var is = new FileInputStream(file)) { | |
ks.load(is, password.toCharArray()); | |
} | |
log.info("KS file loaded."); | |
Enumeration<String> aliases = ks.aliases(); | |
Addressing addressing = Addressing.ofNetwork(network); | |
while (aliases.hasMoreElements()) { | |
String alias = aliases.nextElement(); | |
log.info("Loading alias: {}", alias); | |
try (RadixKeyStore keyStore = RadixKeyStore.fromFile(file, password.toCharArray(), false)) { | |
ECKeyPair keyPair = keyStore.readKeyPair(alias, false); | |
REAddr addr = REAddr.ofPubKeyAccount(keyPair.getPublicKey()); | |
String account = addressing.forAccounts().of(addr); | |
accountKeys.put(account, keyPair); | |
} | |
} | |
} | |
public ECPublicKey getKey(String account) { | |
ECKeyPair keyPair = accountKeys.get(account); | |
return keyPair.getPublicKey(); | |
} | |
public Signature sign(String account, String payloadToSignHex) throws DecoderException, IOException { | |
ECKeyPair keyPair = accountKeys.get(account); | |
byte[] bytes = Hex.decodeHex(payloadToSignHex); | |
byte[] derSignature = toDerSignature(keyPair.sign(bytes)); | |
return new Signature() | |
.publicKey(new PublicKey().hex(keyPair.getPublicKey().toHex())) | |
.bytes(Bytes.toHexString(derSignature)); | |
} | |
private byte[] toDerSignature(ECDSASignature sign) throws IOException { | |
var os = new ByteArrayOutputStream(); | |
var asn1OutputStream = ASN1OutputStream.create(os); | |
asn1OutputStream.writeObject( | |
new DLSequence( | |
new ASN1Encodable[]{new ASN1Integer(sign.getR()), new ASN1Integer(sign.getS())})); | |
return os.toByteArray(); | |
} | |
public void setNetwork(Network network) { | |
this.network = network; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment