How — is reached, step by step — and why it could not be chosen.
Steps 1 and 2 check the two inputs; 3 produces the number from them; 4 checks that the file declaring it is ours. None of them requires trusting us.
— | turn %%Fecha%% %%Turno%% UTC | version —
The seed was only revealed in the -emission. Its SHA-256 must give exactly what was already published in the -commitment, anchored before the drand round existed.
seed revealed in the -emission — edit it and see what happens
seed_sha256 from the -commitment… — if that is yours, you already know where it is.
You don't have to believe our file: the round is checked against the BLS signature of the drand group, or asked directly of any of its servers.
«Followed by» means joined, not added. The bytes of one and then those of the other, with no separator and no arithmetic between them:
And round does not go as text: it is 8 bytes, the number in binary, most significant first — what is called big-endian. Round %%Ronda%% is %%RondaBE%%, not "%%Ronda%%".
This message is what the group signed. It does not enter our calculation — it is there to check that the round is authentic.
previous_signature
round — 8 bytes big-endian
message — the only field you cannot edit: it is computeddrand_public_key — the group key, travels in the file
see it on drand
messageround.signature — the group signature, in the file
true or falseIt happens right here, in your browser. It takes a moment: these are pairings over BLS12-381.
randomnessBoth checks are needed: without the second one, somebody in the middle could hand over a made-up randomness/signature pair that is consistent with itself.
randomness — this one does enter the calculation, as the HKDF salt
open that round
previous_signature is an input to the BLS message. It is not a chain between our emissions — that does not exist in this format.
—Here is where the numbers come out. Pick the variant and the whole calculation is redone.
None of this is ours: HKDF-SHA256 (RFC 5869) turns the seed into bytes, rejection sampling turns those into numbers without bias, and Fisher-Yates makes the selections without replacement. Three published algorithms, so that anyone can reimplement them.
version — which rules it came out with
drand_round — from which round
ikm = seed — the one from step 1, edit it theresalt = randomness — the one from step 2, edit it thereinfo built with this turn's values.byte MOD 10 straight introduces bias: with 256 possible values and 10 results, digits 0..5 would also come from bytes 250..255 and would be over-represented — 26 bytes each against 25, about 4 % more often. If the byte falls there it is discarded and the next one is taken.(space DIV universe) × universe, where DIV is integer division — the decimal part is dropped. With universe 10 it gives (256 DIV 10) × 10 = 250; with universe 37, (256 DIV 37) × 37 = 222.
Ed25519 over header_b64 . payload_b64 exactly as they come. The public key comes from the registry, and its epoch must cover the date the file declares.
kid — which key the file says signed it
e1 and e2. Change it to e1 and the signature stops validating, because the key is another one.
kid, in hexadecimal
kid above puts it back on its own. Restore returns to the one for the kid currently set.
.jws
%%Ruta%%-emission.jwsalg field of the header is ignored: accepting it is the alg:none attack.
Step 1 in five languages. The rest is in the specification. The five complete verifiers, at: GitHub or GitLab
seed, _ := hex.DecodeString("%%Seed%%")
suma := sha256.Sum256(seed) // sobre los BYTES, no sobre el texto
ok := hex.EncodeToString(suma[:]) == "%%SeedSHA%%"
import hashlib
seed = bytes.fromhex("%%Seed%%")
ok = hashlib.sha256(seed).hexdigest() == "%%SeedSHA%%"
const hex = s => Uint8Array.from(s.match(/../g).map(h => parseInt(h, 16)));
const seed = hex("%%Seed%%");
const d = new Uint8Array(await crypto.subtle.digest("SHA-256", seed));
const ok = [...d].map(b => b.toString(16).padStart(2, "0")).join("")
=== "%%SeedSHA%%";
// OpenSSL
unsigned char seed[32]; // 5748e8b8afced252… decodificado
unsigned char out[SHA256_DIGEST_LENGTH];
SHA256(seed, sizeof seed, out); // comparar con
// %%SeedSHA%%
LSeed := TNetEncoding.Base16.Decode('5748E8B8AFCED2520B989A4A3CF013264B3792984B3C55564D2AEBD9D012FA1C');
LHash := THashSHA2.Create(SHA256);
LHash.Update(LSeed[0], LSeed.Length);
LOk := LHash.HashAsString = '%%SeedSHA%%';
header.payload.signature
%%Cab%%.%%Pay%%.%%Fir%%
kid
payload — the data and the 34 results
signature — the 64 bytes of Ed25519
What is signed is header.payload: the first two parts with the dot in between, exactly as they are up there.
—
What is signed is the text header.payload exactly as it is above. The JSON below is only for reading it: re-serializing it breaks the signature.
Download the signed file | See it anchored in: GitHub or GitLab | Format specification at: GitHub or GitLab