@@ -90,8 +90,9 @@ private record LogEntry(long index, byte[] tbs, byte[] entryHash) {}
9090 private record Batch (List <LogEntry > entries , byte [] root ) {}
9191
9292 private record SignedCheckpoint (
93- long treeSize , byte [] rootHash , byte [] body , byte [] issuerSig ,
94- List <WitnessCosig > cosigs ) {}
93+ long treeSize , byte [] rootHash ,
94+ byte [] body , byte [] issuerSig , List <WitnessCosig > cosigs ,
95+ byte [] plainBody , byte [] plainSig , List <WitnessCosig > plainCosigs ) {}
9596
9697 private record WitnessCosig (byte [] keyId , long timestamp , byte [] signature ) {}
9798
@@ -269,23 +270,43 @@ private CompletableFuture<Void> publishCheckpoint() {
269270 treeSize = totalEntriesLocked ();
270271 }
271272 byte [] parentRoot = merkleRoot (bRoots );
272- byte [] body = checkpointBody (origin , treeSize , parentRoot );
273-
274- return signer .sign (body ).thenAccept (issuerSig -> {
273+ byte [] plainBody = checkpointBody (origin , treeSize , parentRoot );
274+ // Build revocation artifact first; commit its hash in the checkpoint body.
275+ String revocSnap ;
276+ synchronized (lock ) { revocSnap = latestRevArtifact ; }
277+ byte [] body = (revocSnap != null )
278+ ? checkpointBodyWithRevoc (origin , treeSize , parentRoot ,
279+ revocSnap .getBytes (java .nio .charset .StandardCharsets .UTF_8 ))
280+ : plainBody ;
281+ final byte [] finalBody = body , finalPlainBody = plainBody ;
282+
283+ return signer .sign (body ).thenCompose (issuerSig ->
284+ signer .sign (plainBody ).thenAccept (plainSig -> {
275285 long ts = Instant .now ().getEpochSecond ();
276286 List <WitnessCosig > cosigs = new ArrayList <>();
287+ List <WitnessCosig > plainCosigs = new ArrayList <>();
288+ boolean same = java .util .Arrays .equals (finalBody , finalPlainBody );
277289 for (WitnessKey w : witnesses ) {
278- byte [] msg = cosignatureMessage (body , ts );
290+ byte [] msg = cosignatureMessage (finalBody , ts );
279291 Ed25519Signer sv = new Ed25519Signer ();
280- sv .init (true , w .priv ());
281- sv .update (msg , 0 , msg .length );
282- cosigs .add (new WitnessCosig (w .keyId (), ts , sv .generateSignature ()));
292+ sv .init (true , w .priv ()); sv .update (msg , 0 , msg .length );
293+ byte [] sig = sv .generateSignature ();
294+ cosigs .add (new WitnessCosig (w .keyId (), ts , sig ));
295+ if (same ) { plainCosigs .add (new WitnessCosig (w .keyId (), ts , sig )); }
296+ else {
297+ byte [] pmsg = cosignatureMessage (finalPlainBody , ts );
298+ Ed25519Signer pv = new Ed25519Signer ();
299+ pv .init (true , w .priv ()); pv .update (pmsg , 0 , pmsg .length );
300+ plainCosigs .add (new WitnessCosig (w .keyId (), ts , pv .generateSignature ()));
301+ }
283302 }
284303 synchronized (lock ) {
285- latestCkpt = new SignedCheckpoint (treeSize , parentRoot , body , issuerSig , cosigs );
304+ latestCkpt = new SignedCheckpoint (
305+ treeSize , parentRoot , finalBody , issuerSig , cosigs ,
306+ finalPlainBody , plainSig , plainCosigs );
286307 latestRevArtifact = buildRevocationArtifact (treeSize );
287308 }
288- });
309+ })) ;
289310 }
290311
291312 private byte [] buildPayload (long globalIdx , byte [] tbs ) {
@@ -322,11 +343,11 @@ private byte[] buildPayload(long globalIdx, byte[] tbs) {
322343 // root_hash (32 bytes)
323344 out .writeBytes (ckpt .rootHash ());
324345 // issuer_sig_len (2 bytes big-endian) + issuer_sig
325- int slen = ckpt .issuerSig ().length ;
346+ int slen = ckpt .plainSig ().length ;
326347 out .write ((slen >> 8 ) & 0xff ); out .write (slen & 0xff );
327- out .writeBytes (ckpt .issuerSig ());
348+ out .writeBytes (ckpt .plainSig ());
328349 // witness_count (1 byte) + cosigs
329- List <WitnessCosig > cosigs0 = ckpt .cosigs ();
350+ List <WitnessCosig > cosigs0 = ckpt .plainCosigs ();
330351 out .write (cosigs0 .size () & 0xff );
331352 for (WitnessCosig c : cosigs0 ) {
332353 out .writeBytes (c .keyId ());
@@ -536,6 +557,23 @@ public static byte[] computeRootFromProof(byte[] start, int idx, int treeSize, L
536557
537558 // --- Checkpoint ---
538559
560+ /** Checkpoint body with 4th extension line: revoc:<hex(SHA-256(artifact))>\n */
561+ public static byte [] checkpointBodyWithRevoc (
562+ String origin , long treeSize , byte [] rootHash , byte [] revocArtifact ) {
563+ byte [] base = checkpointBody (origin , treeSize , rootHash );
564+ try {
565+ java .security .MessageDigest md = java .security .MessageDigest .getInstance ("SHA-256" );
566+ byte [] hash = md .digest (revocArtifact );
567+ StringBuilder hex = new StringBuilder ();
568+ for (byte b : hash ) hex .append (String .format ("%02x" , b & 0xff ));
569+ byte [] ext = ("revoc:" + hex + "\n " ).getBytes (java .nio .charset .StandardCharsets .UTF_8 );
570+ byte [] combined = new byte [base .length + ext .length ];
571+ System .arraycopy (base , 0 , combined , 0 , base .length );
572+ System .arraycopy (ext , 0 , combined , base .length , ext .length );
573+ return combined ;
574+ } catch (java .security .NoSuchAlgorithmException e ) { throw new RuntimeException (e ); }
575+ }
576+
539577 public static byte [] checkpointBody (String origin , long treeSize , byte [] rootHash ) {
540578 String b64 = Base64 .getEncoder ().encodeToString (rootHash );
541579 return (origin + "\n " + treeSize + "\n " + b64 + "\n " ).getBytes (StandardCharsets .UTF_8 );
0 commit comments