Skip to content

Instantly share code, notes, and snippets.

@xfthhxk
Last active October 2, 2023 21:02
Show Gist options
  • Save xfthhxk/501cb2f5b88dd930efde777f973ed360 to your computer and use it in GitHub Desktop.
Save xfthhxk/501cb2f5b88dd930efde777f973ed360 to your computer and use it in GitHub Desktop.
Digital signatures with Java
/*
# 1. Generate public and private keys
# ```shell
# openssl genpkey -algorithm ED25519 -out ed25519.pem
# openssl pkey -in ed25519.pem -pubout > ed25519.pem.pub
# ```
# 2. Register the public key with the remote service
# 3. Compile
# ```shell
# javac DigitalSignature
# ```
# 4. Invoke:
# ```shell
# java DigitalSignature ed25519.pem 'myKeyId' 'https://example.com/api/v1/thing?x=a'
# ```
*/
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Key;
import java.security.Signature;
import java.security.NoSuchAlgorithmException;
import java.security.MessageDigest;
import java.security.interfaces.EdECPrivateKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import java.time.format.DateTimeFormatter;
import java.time.Instant;
import java.time.ZoneOffset;
class RequestInfo {
public URI uri;
public String method;
public String body;
public String date;
public String digest;
public String keyId;
public String signature;
}
public class DigitalSignature {
public static byte[] readPemFile(String fileName) throws Exception {
var pem = new String(Files.readAllBytes(new File(fileName).toPath()));
pem = pem.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replaceAll(System.lineSeparator(), "");
return Base64.getDecoder().decode(pem);
}
public static PrivateKey loadPrivateKey(String fileName, String algo) throws Exception {
var spec = new PKCS8EncodedKeySpec(readPemFile(fileName));
return KeyFactory.getInstance(algo).generatePrivate(spec);
}
public static PublicKey loadPublicKey(String fileName, String algo) throws Exception {
var spec = new X509EncodedKeySpec(readPemFile(fileName));
return KeyFactory.getInstance(algo).generatePublic(spec);
}
public static String httpDate() {
return DateTimeFormatter.RFC_1123_DATE_TIME.withZone(ZoneOffset.UTC).format(Instant.now());
}
public static String fingerprint(RequestInfo ri) {
var uri = ri.uri;
var pathWithQuery = uri.getRawSchemeSpecificPart().replaceFirst("//" + uri.getHost(), "");
var s = "(request-target): " + ri.method.toLowerCase() + " " + pathWithQuery + "\n";
s = s + "host: " + uri.getHost() + "\n";
s = s + "date: " + ri.date;
if (null != ri.digest) {
s = s + "\ndigest: " + ri.digest;
}
return s;
}
public static String b64str(byte[] bs) {
return Base64.getEncoder().encodeToString(bs);
}
public static String sign(PrivateKey pk, String fingerprint) throws Exception {
var sig = Signature.getInstance(pk.getAlgorithm());
sig.initSign(pk);
sig.update(fingerprint.getBytes());
return b64str(sig.sign());
}
public static String dq(String s) {
return "\"" + s + "\"";
}
public static String genSignatureHeader(RequestInfo ri) {
// same order as in fingerprint
var headers = "(request-target) host date";
if (null != ri.digest) {
headers += " digest";
}
return String.format("keyId=%s,algorithm=%s,headers=%s,signature=%s",
dq(ri.keyId), dq("ed25519"), dq(headers), dq(ri.signature));
}
public static String genDigest(String body) throws Exception {
String ans = null;
if(null != body) {
var md = MessageDigest.getInstance("SHA-256");
ans = "sha-256=" + b64str(md.digest(body.getBytes()));
}
return ans;
}
public static HttpRequest request(RequestInfo ri) {
var b = HttpRequest.newBuilder();
var bodyPublisher = HttpRequest.BodyPublishers.noBody();
if (null != ri.body) {
bodyPublisher = HttpRequest.BodyPublishers.ofString(ri.body);
}
b = b.uri(ri.uri)
.method(ri.method.toUpperCase(), bodyPublisher)
.header("date", ri.date)
.header("signature", genSignatureHeader(ri))
.header("accept", "application/json");
if (null != ri.digest) {
b = b.header("digest", ri.digest);
}
return b.build();
}
public static void main(String[] args) throws Exception {
var algo = "EDDSA";
var privateKey = loadPrivateKey(args[0], algo);
var ri = new RequestInfo();
ri.uri = URI.create(args[2]);
ri.method = "GET";
ri.date = httpDate();
ri.keyId = args[1];
ri.digest = genDigest(ri.body);
var fp = fingerprint(ri);
ri.signature = sign(privateKey, fp);
var req = request(ri);
var client = HttpClient.newBuilder().build();
var resp = client.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(resp.body());
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment