Last active
October 2, 2023 21:02
-
-
Save xfthhxk/501cb2f5b88dd930efde777f973ed360 to your computer and use it in GitHub Desktop.
Digital signatures with Java
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
/* | |
# 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