Recovery check: open fixtures with the annex and no DateKeys code
scripts/recovery is a program that opens a capsule with only the Go
standard library, golang.org/x/crypto, filippo.io/age and the BLS12-381
library of drand/kyber-bls12381, as the informative annex of the draft
v0.15 describes it: the pinned Quicknet parameters, the release object,
the frame, the BLS verification of the release, the tlock stanza with H2,
H3 and H4, the age file of SEALED_CONTROL opened with its file key (HKDF,
header MAC and STREAM written out), the X25519 layers with age, and the
content of formats 1, 2 and 3. A test forbids importing this module, tlock
and drand. scripts/recovery_check.sh opens a time_only and a time_and_key
fixture of format 3 with it and compares what it recovers; scripts/check.sh
runs it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 day ago
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"crypto/sha256"
|
|
|
|
|
"encoding/hex"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"errors"
|
|
|
|
|
"go/parser"
|
|
|
|
|
"go/token"
|
|
|
|
|
"io"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
|
|
"filippo.io/age"
|
|
|
|
|
bls "github.com/drand/kyber-bls12381"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
fixturesDir = "../../testdata/fixtures"
|
|
|
|
|
releasesDir = "../../testdata/releases"
|
|
|
|
|
vectorsDir = "../../testdata/vectors"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type fixtureRecord struct {
|
|
|
|
|
Format int `json:"format"`
|
|
|
|
|
Release struct {
|
|
|
|
|
Round uint64 `json:"round"`
|
|
|
|
|
Signature string `json:"signature"`
|
|
|
|
|
} `json:"release"`
|
|
|
|
|
PayloadLength uint64 `json:"payload_length"`
|
|
|
|
|
PlaintextFile string `json:"plaintext_file"`
|
|
|
|
|
PlaintextSHA256 string `json:"plaintext_sha256"`
|
|
|
|
|
AccessKeyFile string `json:"access_key_file"`
|
v0.16: the key of words in the recovery annex, and a full last chunk
scripts/recovery opens a time_and_key capsule with the words of a key of
words (-words FILE), as the annex says in 79.7: the normalization without
tables for the text of the DateKeys lists (printable ASCII, the ASCII
spaces, á é í ó ú ü ñ and their capitals, and the marks U+0300 to U+036F),
and for any other text the full one, NFD, without the marks, simple lower
case and the spaces of 38.1, from UnicodeData.txt of Unicode 18.0.0
(-unicodedata FILE), checked by its SHA-256; then PBKDF2-HMAC-SHA256 of the
standard library. Its tests check both normalizations against every case
of wordkey.json and the two vectors of the annex.
Two fixtures of v0.16: format3_time_and_key_words, opened with the text of
the second vector of the annex, recorded in words_text with the identity it
gives; and format3_full_chunk, whose BODY and P are 65536 bytes, so that
PAYLOAD_AGE ends in a full STREAM chunk (79.5). recovery_check.sh opens
both. wordkey.json gains the text of the annex, also with its marks apart.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 hours ago
|
|
|
WordsText string `json:"words_text"`
|
Recovery check: open fixtures with the annex and no DateKeys code
scripts/recovery is a program that opens a capsule with only the Go
standard library, golang.org/x/crypto, filippo.io/age and the BLS12-381
library of drand/kyber-bls12381, as the informative annex of the draft
v0.15 describes it: the pinned Quicknet parameters, the release object,
the frame, the BLS verification of the release, the tlock stanza with H2,
H3 and H4, the age file of SEALED_CONTROL opened with its file key (HKDF,
header MAC and STREAM written out), the X25519 layers with age, and the
content of formats 1, 2 and 3. A test forbids importing this module, tlock
and drand. scripts/recovery_check.sh opens a time_only and a time_and_key
fixture of format 3 with it and compares what it recovers; scripts/check.sh
runs it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 day ago
|
|
|
Files []struct {
|
|
|
|
|
Path string `json:"path"`
|
|
|
|
|
Size uint64 `json:"size"`
|
|
|
|
|
SHA256 string `json:"sha256"`
|
|
|
|
|
} `json:"files"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func readJSON(t *testing.T, path string, v any) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
b, err := os.ReadFile(path)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if err := json.Unmarshal(b, v); err != nil {
|
|
|
|
|
t.Fatalf("%s: %v", path, err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func unhex(t *testing.T, s string) []byte {
|
|
|
|
|
t.Helper()
|
|
|
|
|
b, err := hex.DecodeString(s)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
return b
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A tiny deterministic CBOR encoder, enough for a release object.
|
|
|
|
|
func cborHead(major byte, n uint64) []byte {
|
|
|
|
|
switch {
|
|
|
|
|
case n < 24:
|
|
|
|
|
return []byte{major<<5 | byte(n)}
|
|
|
|
|
case n < 1<<8:
|
|
|
|
|
return []byte{major<<5 | 24, byte(n)}
|
|
|
|
|
case n < 1<<16:
|
|
|
|
|
return []byte{major<<5 | 25, byte(n >> 8), byte(n)}
|
|
|
|
|
case n < 1<<32:
|
|
|
|
|
return []byte{major<<5 | 26, byte(n >> 24), byte(n >> 16), byte(n >> 8), byte(n)}
|
|
|
|
|
}
|
|
|
|
|
return []byte{major<<5 | 27, byte(n >> 56), byte(n >> 48), byte(n >> 40), byte(n >> 32),
|
|
|
|
|
byte(n >> 24), byte(n >> 16), byte(n >> 8), byte(n)}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func encodeRelease(chainHash []byte, round uint64, sig []byte) []byte {
|
|
|
|
|
var b []byte
|
|
|
|
|
b = append(b, cborHead(5, 5)...)
|
|
|
|
|
b = append(b, cborHead(0, 0)...)
|
|
|
|
|
b = append(b, cborHead(3, 16)...)
|
|
|
|
|
b = append(b, "datekeys-release"...)
|
|
|
|
|
b = append(b, cborHead(0, 1)...)
|
|
|
|
|
b = append(b, cborHead(0, 1)...)
|
|
|
|
|
b = append(b, cborHead(0, 2)...)
|
|
|
|
|
b = append(b, cborHead(2, uint64(len(chainHash)))...)
|
|
|
|
|
b = append(b, chainHash...)
|
|
|
|
|
b = append(b, cborHead(0, 3)...)
|
|
|
|
|
b = append(b, cborHead(0, round)...)
|
|
|
|
|
b = append(b, cborHead(0, 4)...)
|
|
|
|
|
b = append(b, cborHead(2, uint64(len(sig)))...)
|
|
|
|
|
b = append(b, sig...)
|
|
|
|
|
return b
|
|
|
|
|
}
|
|
|
|
|
|
Spec v0.15 draft: remove the .dkr file
The author's decision of 7 October 2026. A release saved next to a capsule
cannot exist when the capsule is made, and once the date comes the capsule
can be opened: such a file only opens it again and does not cover the real
case, someone opening it decades later when drand is gone and nobody saved
anything. Long-term recovery rests instead on archives and cache services
that keep the releases of all rounds; a reader asks for its round and
verifies the signature against the pinned key.
Spec: the .dkr extension (section 20, back to v0.14), sections 1, 4, 8, 45,
47.1, 49, 50 (rewritten), 53, 62.1 (rule 28 removed, rule 26 reworded),
63, 70, 73, 74 (the datekeys.release .dkk extension dropped too), 76
(the v0.15 block, with the discarded design) and the annex 79. The
release object, the chain hash at step 10, step 9.c option B and the
archive format stay.
Code: decrypt -save-release and the command datekeys release are gone,
with writeRelease and their tests; decrypt -release FILE stays. The
release objects of testdata/releases are now <round>.cbor, and
TestVectorFilesAreCurrent fails on a file the generator no longer writes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
23 hours ago
|
|
|
// committedRelease reads testdata/releases/<round>.cbor and checks that it is
|
Recovery check: open fixtures with the annex and no DateKeys code
scripts/recovery is a program that opens a capsule with only the Go
standard library, golang.org/x/crypto, filippo.io/age and the BLS12-381
library of drand/kyber-bls12381, as the informative annex of the draft
v0.15 describes it: the pinned Quicknet parameters, the release object,
the frame, the BLS verification of the release, the tlock stanza with H2,
H3 and H4, the age file of SEALED_CONTROL opened with its file key (HKDF,
header MAC and STREAM written out), the X25519 layers with age, and the
content of formats 1, 2 and 3. A test forbids importing this module, tlock
and drand. scripts/recovery_check.sh opens a time_only and a time_and_key
fixture of format 3 with it and compares what it recovers; scripts/check.sh
runs it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 day ago
|
|
|
// the release object built here from the fixture record.
|
|
|
|
|
func committedRelease(t *testing.T, round uint64, sigHex string) []byte {
|
|
|
|
|
t.Helper()
|
|
|
|
|
want := encodeRelease(unhex(t, quicknetChainHash), round, unhex(t, sigHex))
|
Spec v0.15 draft: remove the .dkr file
The author's decision of 7 October 2026. A release saved next to a capsule
cannot exist when the capsule is made, and once the date comes the capsule
can be opened: such a file only opens it again and does not cover the real
case, someone opening it decades later when drand is gone and nobody saved
anything. Long-term recovery rests instead on archives and cache services
that keep the releases of all rounds; a reader asks for its round and
verifies the signature against the pinned key.
Spec: the .dkr extension (section 20, back to v0.14), sections 1, 4, 8, 45,
47.1, 49, 50 (rewritten), 53, 62.1 (rule 28 removed, rule 26 reworded),
63, 70, 73, 74 (the datekeys.release .dkk extension dropped too), 76
(the v0.15 block, with the discarded design) and the annex 79. The
release object, the chain hash at step 10, step 9.c option B and the
archive format stay.
Code: decrypt -save-release and the command datekeys release are gone,
with writeRelease and their tests; decrypt -release FILE stays. The
release objects of testdata/releases are now <round>.cbor, and
TestVectorFilesAreCurrent fails on a file the generator no longer writes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
23 hours ago
|
|
|
path := filepath.Join(releasesDir, strconv.FormatUint(round, 10)+".cbor")
|
Recovery check: open fixtures with the annex and no DateKeys code
scripts/recovery is a program that opens a capsule with only the Go
standard library, golang.org/x/crypto, filippo.io/age and the BLS12-381
library of drand/kyber-bls12381, as the informative annex of the draft
v0.15 describes it: the pinned Quicknet parameters, the release object,
the frame, the BLS verification of the release, the tlock stanza with H2,
H3 and H4, the age file of SEALED_CONTROL opened with its file key (HKDF,
header MAC and STREAM written out), the X25519 layers with age, and the
content of formats 1, 2 and 3. A test forbids importing this module, tlock
and drand. scripts/recovery_check.sh opens a time_only and a time_and_key
fixture of format 3 with it and compares what it recovers; scripts/check.sh
runs it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 day ago
|
|
|
got, err := os.ReadFile(path)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("the committed release is missing: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if !bytes.Equal(got, want) {
|
|
|
|
|
t.Fatalf("%s:\n got %x\nwant %x", path, got, want)
|
|
|
|
|
}
|
|
|
|
|
return got
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func loadFixture(t *testing.T, name string) (fixtureRecord, []byte, []byte, []byte) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
var rec fixtureRecord
|
|
|
|
|
readJSON(t, filepath.Join(fixturesDir, name+".json"), &rec)
|
|
|
|
|
dkc, err := os.ReadFile(filepath.Join(fixturesDir, name+".dkc"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
var dkk []byte
|
|
|
|
|
if rec.AccessKeyFile != "" {
|
|
|
|
|
if dkk, err = os.ReadFile(filepath.Join(fixturesDir, rec.AccessKeyFile)); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return rec, dkc, committedRelease(t, rec.Release.Round, rec.Release.Signature), dkk
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestRecoverFixtures(t *testing.T) {
|
|
|
|
|
for _, name := range []string{
|
|
|
|
|
"format3_single",
|
|
|
|
|
"format3_time_and_key_portable",
|
|
|
|
|
"format3_tree",
|
|
|
|
|
"format3_signed",
|
v0.16: the key of words in the recovery annex, and a full last chunk
scripts/recovery opens a time_and_key capsule with the words of a key of
words (-words FILE), as the annex says in 79.7: the normalization without
tables for the text of the DateKeys lists (printable ASCII, the ASCII
spaces, á é í ó ú ü ñ and their capitals, and the marks U+0300 to U+036F),
and for any other text the full one, NFD, without the marks, simple lower
case and the spaces of 38.1, from UnicodeData.txt of Unicode 18.0.0
(-unicodedata FILE), checked by its SHA-256; then PBKDF2-HMAC-SHA256 of the
standard library. Its tests check both normalizations against every case
of wordkey.json and the two vectors of the annex.
Two fixtures of v0.16: format3_time_and_key_words, opened with the text of
the second vector of the annex, recorded in words_text with the identity it
gives; and format3_full_chunk, whose BODY and P are 65536 bytes, so that
PAYLOAD_AGE ends in a full STREAM chunk (79.5). recovery_check.sh opens
both. wordkey.json gains the text of the annex, also with its marks apart.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 hours ago
|
|
|
"format3_time_and_key_words", // opened with its words (annex 79.7)
|
|
|
|
|
"format3_full_chunk", // PAYLOAD_AGE ends in a full chunk (79.5)
|
|
|
|
|
"format2_time_only", // two STREAM chunks
|
Recovery check: open fixtures with the annex and no DateKeys code
scripts/recovery is a program that opens a capsule with only the Go
standard library, golang.org/x/crypto, filippo.io/age and the BLS12-381
library of drand/kyber-bls12381, as the informative annex of the draft
v0.15 describes it: the pinned Quicknet parameters, the release object,
the frame, the BLS verification of the release, the tlock stanza with H2,
H3 and H4, the age file of SEALED_CONTROL opened with its file key (HKDF,
header MAC and STREAM written out), the X25519 layers with age, and the
content of formats 1, 2 and 3. A test forbids importing this module, tlock
and drand. scripts/recovery_check.sh opens a time_only and a time_and_key
fixture of format 3 with it and compares what it recovers; scripts/check.sh
runs it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 day ago
|
|
|
"format2_time_and_key_portable",
|
|
|
|
|
"format2_time_and_key_sixteen", // round 2000; no .dkk, so only its release is checked below
|
|
|
|
|
"time_only",
|
|
|
|
|
"time_and_key_portable",
|
|
|
|
|
} {
|
|
|
|
|
t.Run(name, func(t *testing.T) {
|
Spec v0.15 draft: remove the .dkr file
The author's decision of 7 October 2026. A release saved next to a capsule
cannot exist when the capsule is made, and once the date comes the capsule
can be opened: such a file only opens it again and does not cover the real
case, someone opening it decades later when drand is gone and nobody saved
anything. Long-term recovery rests instead on archives and cache services
that keep the releases of all rounds; a reader asks for its round and
verifies the signature against the pinned key.
Spec: the .dkr extension (section 20, back to v0.14), sections 1, 4, 8, 45,
47.1, 49, 50 (rewritten), 53, 62.1 (rule 28 removed, rule 26 reworded),
63, 70, 73, 74 (the datekeys.release .dkk extension dropped too), 76
(the v0.15 block, with the discarded design) and the annex 79. The
release object, the chain hash at step 10, step 9.c option B and the
archive format stay.
Code: decrypt -save-release and the command datekeys release are gone,
with writeRelease and their tests; decrypt -release FILE stays. The
release objects of testdata/releases are now <round>.cbor, and
TestVectorFilesAreCurrent fails on a file the generator no longer writes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
23 hours ago
|
|
|
rec, dkc, relObj, dkk := loadFixture(t, name)
|
Recovery check: open fixtures with the annex and no DateKeys code
scripts/recovery is a program that opens a capsule with only the Go
standard library, golang.org/x/crypto, filippo.io/age and the BLS12-381
library of drand/kyber-bls12381, as the informative annex of the draft
v0.15 describes it: the pinned Quicknet parameters, the release object,
the frame, the BLS verification of the release, the tlock stanza with H2,
H3 and H4, the age file of SEALED_CONTROL opened with its file key (HKDF,
header MAC and STREAM written out), the X25519 layers with age, and the
content of formats 1, 2 and 3. A test forbids importing this module, tlock
and drand. scripts/recovery_check.sh opens a time_only and a time_and_key
fixture of format 3 with it and compares what it recovers; scripts/check.sh
runs it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 day ago
|
|
|
if name == "format2_time_and_key_sixteen" {
|
v0.16: the key of words in the recovery annex, and a full last chunk
scripts/recovery opens a time_and_key capsule with the words of a key of
words (-words FILE), as the annex says in 79.7: the normalization without
tables for the text of the DateKeys lists (printable ASCII, the ASCII
spaces, á é í ó ú ü ñ and their capitals, and the marks U+0300 to U+036F),
and for any other text the full one, NFD, without the marks, simple lower
case and the spaces of 38.1, from UnicodeData.txt of Unicode 18.0.0
(-unicodedata FILE), checked by its SHA-256; then PBKDF2-HMAC-SHA256 of the
standard library. Its tests check both normalizations against every case
of wordkey.json and the two vectors of the annex.
Two fixtures of v0.16: format3_time_and_key_words, opened with the text of
the second vector of the annex, recorded in words_text with the identity it
gives; and format3_full_chunk, whose BODY and P are 65536 bytes, so that
PAYLOAD_AGE ends in a full STREAM chunk (79.5). recovery_check.sh opens
both. wordkey.json gains the text of the annex, also with its marks apart.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 hours ago
|
|
|
_, err := recoverCapsule(dkc, relObj, credential{}, io.Discard)
|
Recovery check: open fixtures with the annex and no DateKeys code
scripts/recovery is a program that opens a capsule with only the Go
standard library, golang.org/x/crypto, filippo.io/age and the BLS12-381
library of drand/kyber-bls12381, as the informative annex of the draft
v0.15 describes it: the pinned Quicknet parameters, the release object,
the frame, the BLS verification of the release, the tlock stanza with H2,
H3 and H4, the age file of SEALED_CONTROL opened with its file key (HKDF,
header MAC and STREAM written out), the X25519 layers with age, and the
content of formats 1, 2 and 3. A test forbids importing this module, tlock
and drand. scripts/recovery_check.sh opens a time_only and a time_and_key
fixture of format 3 with it and compares what it recovers; scripts/check.sh
runs it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 day ago
|
|
|
if err == nil || !strings.Contains(err.Error(), "needs its .dkk") {
|
|
|
|
|
t.Fatalf("got %v, want the .dkk to be required", err)
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
v0.16: the key of words in the recovery annex, and a full last chunk
scripts/recovery opens a time_and_key capsule with the words of a key of
words (-words FILE), as the annex says in 79.7: the normalization without
tables for the text of the DateKeys lists (printable ASCII, the ASCII
spaces, á é í ó ú ü ñ and their capitals, and the marks U+0300 to U+036F),
and for any other text the full one, NFD, without the marks, simple lower
case and the spaces of 38.1, from UnicodeData.txt of Unicode 18.0.0
(-unicodedata FILE), checked by its SHA-256; then PBKDF2-HMAC-SHA256 of the
standard library. Its tests check both normalizations against every case
of wordkey.json and the two vectors of the annex.
Two fixtures of v0.16: format3_time_and_key_words, opened with the text of
the second vector of the annex, recorded in words_text with the identity it
gives; and format3_full_chunk, whose BODY and P are 65536 bytes, so that
PAYLOAD_AGE ends in a full STREAM chunk (79.5). recovery_check.sh opens
both. wordkey.json gains the text of the annex, also with its marks apart.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 hours ago
|
|
|
key := credential{dkk: dkk}
|
|
|
|
|
if rec.WordsText != "" {
|
|
|
|
|
key.words = &rec.WordsText
|
|
|
|
|
}
|
|
|
|
|
res, err := recoverCapsule(dkc, relObj, key, io.Discard)
|
Recovery check: open fixtures with the annex and no DateKeys code
scripts/recovery is a program that opens a capsule with only the Go
standard library, golang.org/x/crypto, filippo.io/age and the BLS12-381
library of drand/kyber-bls12381, as the informative annex of the draft
v0.15 describes it: the pinned Quicknet parameters, the release object,
the frame, the BLS verification of the release, the tlock stanza with H2,
H3 and H4, the age file of SEALED_CONTROL opened with its file key (HKDF,
header MAC and STREAM written out), the X25519 layers with age, and the
content of formats 1, 2 and 3. A test forbids importing this module, tlock
and drand. scripts/recovery_check.sh opens a time_only and a time_and_key
fixture of format 3 with it and compares what it recovers; scripts/check.sh
runs it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 day ago
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
want, err := os.ReadFile(filepath.Join(fixturesDir, rec.PlaintextFile))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if !bytes.Equal(res.content, want) {
|
|
|
|
|
t.Fatalf("content differs from %s", rec.PlaintextFile)
|
|
|
|
|
}
|
|
|
|
|
if res.format != rec.Format || uint64(len(res.content)) != rec.PayloadLength {
|
|
|
|
|
t.Fatalf("format %d, L %d; want %d, %d", res.format, len(res.content), rec.Format, rec.PayloadLength)
|
|
|
|
|
}
|
|
|
|
|
if res.format != 3 {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if len(res.body.files) != len(rec.Files) {
|
|
|
|
|
t.Fatalf("%d files, want %d", len(res.body.files), len(rec.Files))
|
|
|
|
|
}
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
if err := writeFiles(dir, res.body, io.Discard); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
for i, f := range rec.Files {
|
|
|
|
|
if res.body.files[i].path != f.Path {
|
|
|
|
|
t.Fatalf("file %d is %q, want %q", i, res.body.files[i].path, f.Path)
|
|
|
|
|
}
|
|
|
|
|
b, err := os.ReadFile(filepath.Join(dir, filepath.FromSlash(f.Path)))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
sum := sha256.Sum256(b)
|
|
|
|
|
if hex.EncodeToString(sum[:]) != f.SHA256 || uint64(len(b)) != f.Size {
|
|
|
|
|
t.Fatalf("%s: written file differs from the record", f.Path)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestRun(t *testing.T) {
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
out, bodyFile := filepath.Join(dir, "out"), filepath.Join(dir, "body")
|
|
|
|
|
args := []string{
|
|
|
|
|
"-dkc", filepath.Join(fixturesDir, "format3_time_and_key_portable.dkc"),
|
Spec v0.15 draft: remove the .dkr file
The author's decision of 7 October 2026. A release saved next to a capsule
cannot exist when the capsule is made, and once the date comes the capsule
can be opened: such a file only opens it again and does not cover the real
case, someone opening it decades later when drand is gone and nobody saved
anything. Long-term recovery rests instead on archives and cache services
that keep the releases of all rounds; a reader asks for its round and
verifies the signature against the pinned key.
Spec: the .dkr extension (section 20, back to v0.14), sections 1, 4, 8, 45,
47.1, 49, 50 (rewritten), 53, 62.1 (rule 28 removed, rule 26 reworded),
63, 70, 73, 74 (the datekeys.release .dkk extension dropped too), 76
(the v0.15 block, with the discarded design) and the annex 79. The
release object, the chain hash at step 10, step 9.c option B and the
archive format stay.
Code: decrypt -save-release and the command datekeys release are gone,
with writeRelease and their tests; decrypt -release FILE stays. The
release objects of testdata/releases are now <round>.cbor, and
TestVectorFilesAreCurrent fails on a file the generator no longer writes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
23 hours ago
|
|
|
"-release", filepath.Join(releasesDir, "1000.cbor"),
|
Recovery check: open fixtures with the annex and no DateKeys code
scripts/recovery is a program that opens a capsule with only the Go
standard library, golang.org/x/crypto, filippo.io/age and the BLS12-381
library of drand/kyber-bls12381, as the informative annex of the draft
v0.15 describes it: the pinned Quicknet parameters, the release object,
the frame, the BLS verification of the release, the tlock stanza with H2,
H3 and H4, the age file of SEALED_CONTROL opened with its file key (HKDF,
header MAC and STREAM written out), the X25519 layers with age, and the
content of formats 1, 2 and 3. A test forbids importing this module, tlock
and drand. scripts/recovery_check.sh opens a time_only and a time_and_key
fixture of format 3 with it and compares what it recovers; scripts/check.sh
runs it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 day ago
|
|
|
"-dkk", filepath.Join(fixturesDir, "format3_time_and_key_portable.dkk"),
|
|
|
|
|
"-out", out, "-body", bodyFile,
|
|
|
|
|
}
|
|
|
|
|
var log bytes.Buffer
|
|
|
|
|
if err := run(args, &log); err != nil {
|
|
|
|
|
t.Fatalf("%v\n%s", err, log.String())
|
|
|
|
|
}
|
|
|
|
|
got, _ := os.ReadFile(bodyFile)
|
|
|
|
|
want, _ := os.ReadFile(filepath.Join(fixturesDir, "format3_time_and_key_portable.plaintext"))
|
|
|
|
|
if !bytes.Equal(got, want) {
|
|
|
|
|
t.Fatal("-body differs from the plaintext fixture")
|
|
|
|
|
}
|
|
|
|
|
if _, err := os.Stat(filepath.Join(out, "secreto.txt")); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestBadReleases(t *testing.T) {
|
|
|
|
|
var single, other fixtureRecord
|
|
|
|
|
readJSON(t, filepath.Join(fixturesDir, "format3_single.json"), &single)
|
|
|
|
|
readJSON(t, filepath.Join(fixturesDir, "empty_payload.json"), &other) // round 1001
|
|
|
|
|
dkc, err := os.ReadFile(filepath.Join(fixturesDir, "format3_single.dkc"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
chain := unhex(t, quicknetChainHash)
|
|
|
|
|
wrongChain := bytes.Clone(chain)
|
|
|
|
|
wrongChain[0] ^= 1
|
|
|
|
|
sig, otherSig := unhex(t, single.Release.Signature), unhex(t, other.Release.Signature)
|
|
|
|
|
|
|
|
|
|
for _, tc := range []struct {
|
|
|
|
|
name, want string
|
Spec v0.15 draft: remove the .dkr file
The author's decision of 7 October 2026. A release saved next to a capsule
cannot exist when the capsule is made, and once the date comes the capsule
can be opened: such a file only opens it again and does not cover the real
case, someone opening it decades later when drand is gone and nobody saved
anything. Long-term recovery rests instead on archives and cache services
that keep the releases of all rounds; a reader asks for its round and
verifies the signature against the pinned key.
Spec: the .dkr extension (section 20, back to v0.14), sections 1, 4, 8, 45,
47.1, 49, 50 (rewritten), 53, 62.1 (rule 28 removed, rule 26 reworded),
63, 70, 73, 74 (the datekeys.release .dkk extension dropped too), 76
(the v0.15 block, with the discarded design) and the annex 79. The
release object, the chain hash at step 10, step 9.c option B and the
archive format stay.
Code: decrypt -save-release and the command datekeys release are gone,
with writeRelease and their tests; decrypt -release FILE stays. The
release objects of testdata/releases are now <round>.cbor, and
TestVectorFilesAreCurrent fails on a file the generator no longer writes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
23 hours ago
|
|
|
relObj []byte
|
Recovery check: open fixtures with the annex and no DateKeys code
scripts/recovery is a program that opens a capsule with only the Go
standard library, golang.org/x/crypto, filippo.io/age and the BLS12-381
library of drand/kyber-bls12381, as the informative annex of the draft
v0.15 describes it: the pinned Quicknet parameters, the release object,
the frame, the BLS verification of the release, the tlock stanza with H2,
H3 and H4, the age file of SEALED_CONTROL opened with its file key (HKDF,
header MAC and STREAM written out), the X25519 layers with age, and the
content of formats 1, 2 and 3. A test forbids importing this module, tlock
and drand. scripts/recovery_check.sh opens a time_only and a time_and_key
fixture of format 3 with it and compares what it recovers; scripts/check.sh
runs it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 day ago
|
|
|
}{
|
|
|
|
|
{"wrong chain_hash", "not Quicknet's", encodeRelease(wrongChain, 1000, sig)},
|
|
|
|
|
{"release of another round", "needs round 1000", encodeRelease(chain, other.Release.Round, otherSig)},
|
|
|
|
|
{"signature of another round", "does not verify", encodeRelease(chain, 1000, otherSig)},
|
|
|
|
|
{"wrong type tag", "type tag", bytes.Replace(encodeRelease(chain, 1000, sig), []byte("release"), []byte("relaxed"), 1)},
|
|
|
|
|
} {
|
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
v0.16: the key of words in the recovery annex, and a full last chunk
scripts/recovery opens a time_and_key capsule with the words of a key of
words (-words FILE), as the annex says in 79.7: the normalization without
tables for the text of the DateKeys lists (printable ASCII, the ASCII
spaces, á é í ó ú ü ñ and their capitals, and the marks U+0300 to U+036F),
and for any other text the full one, NFD, without the marks, simple lower
case and the spaces of 38.1, from UnicodeData.txt of Unicode 18.0.0
(-unicodedata FILE), checked by its SHA-256; then PBKDF2-HMAC-SHA256 of the
standard library. Its tests check both normalizations against every case
of wordkey.json and the two vectors of the annex.
Two fixtures of v0.16: format3_time_and_key_words, opened with the text of
the second vector of the annex, recorded in words_text with the identity it
gives; and format3_full_chunk, whose BODY and P are 65536 bytes, so that
PAYLOAD_AGE ends in a full STREAM chunk (79.5). recovery_check.sh opens
both. wordkey.json gains the text of the annex, also with its marks apart.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 hours ago
|
|
|
_, err := recoverCapsule(dkc, tc.relObj, credential{}, io.Discard)
|
Recovery check: open fixtures with the annex and no DateKeys code
scripts/recovery is a program that opens a capsule with only the Go
standard library, golang.org/x/crypto, filippo.io/age and the BLS12-381
library of drand/kyber-bls12381, as the informative annex of the draft
v0.15 describes it: the pinned Quicknet parameters, the release object,
the frame, the BLS verification of the release, the tlock stanza with H2,
H3 and H4, the age file of SEALED_CONTROL opened with its file key (HKDF,
header MAC and STREAM written out), the X25519 layers with age, and the
content of formats 1, 2 and 3. A test forbids importing this module, tlock
and drand. scripts/recovery_check.sh opens a time_only and a time_and_key
fixture of format 3 with it and compares what it recovers; scripts/check.sh
runs it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 day ago
|
|
|
if err == nil || !strings.Contains(err.Error(), tc.want) {
|
|
|
|
|
t.Fatalf("got %v, want an error with %q", err, tc.want)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestTlockVectors checks steps 10 and 11 value by value against
|
|
|
|
|
// testdata/vectors/tlock_steps.json and the GT serialization against
|
|
|
|
|
// tlock_ibe.json.
|
|
|
|
|
func TestTlockVectors(t *testing.T) {
|
|
|
|
|
var ibe struct {
|
|
|
|
|
Vectors []struct{ G1, G2, GT, H2 string }
|
|
|
|
|
}
|
|
|
|
|
readJSON(t, filepath.Join(vectorsDir, "tlock_ibe.json"), &ibe)
|
|
|
|
|
for _, v := range ibe.Vectors {
|
|
|
|
|
g1, g2 := bls.NullKyberG1(), bls.NullKyberG2()
|
|
|
|
|
if err := decodePoint(g1, unhex(t, v.G1), 48, "g1"); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if err := decodePoint(g2, unhex(t, v.G2), 96, "g2"); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if !g1.Equal(bls.NullKyberG1().Base()) || !g2.Equal(bls.NullKyberG2().Base()) {
|
|
|
|
|
t.Fatal("vector points are not the generators")
|
|
|
|
|
}
|
|
|
|
|
gt := suite.Pair(g1, g2)
|
|
|
|
|
raw, _ := gt.MarshalBinary()
|
|
|
|
|
if hex.EncodeToString(raw) != v.GT {
|
|
|
|
|
t.Fatal("GT serialization differs")
|
|
|
|
|
}
|
|
|
|
|
if h, _ := h2(gt); hex.EncodeToString(h) != v.H2 {
|
|
|
|
|
t.Fatalf("H2 = %x, want %s", h, v.H2)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var steps struct {
|
|
|
|
|
ChainHash string `json:"chain_hash"`
|
|
|
|
|
PublicKey string `json:"public_key"`
|
|
|
|
|
DST string `json:"dst"`
|
|
|
|
|
Vectors []struct {
|
|
|
|
|
Round uint64
|
|
|
|
|
Signature, Message string
|
|
|
|
|
HashToG1 string `json:"hash_to_g1"`
|
|
|
|
|
Body, Pairing, H2, Sigma, H4, R string
|
|
|
|
|
FileKey string `json:"file_key"`
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
readJSON(t, filepath.Join(vectorsDir, "tlock_steps.json"), &steps)
|
|
|
|
|
if steps.ChainHash != quicknetChainHash || steps.PublicKey != quicknetPublicKey || steps.DST != quicknetDST {
|
|
|
|
|
t.Fatal("pinned parameters differ from the vectors")
|
|
|
|
|
}
|
|
|
|
|
for _, v := range steps.Vectors {
|
|
|
|
|
if hex.EncodeToString(roundMessage(v.Round)) != v.Message {
|
|
|
|
|
t.Fatalf("round %d: M differs", v.Round)
|
|
|
|
|
}
|
|
|
|
|
hm, _ := hashToG1(roundMessage(v.Round)).MarshalBinary()
|
|
|
|
|
if hex.EncodeToString(hm) != v.HashToG1 {
|
|
|
|
|
t.Fatalf("round %d: H(M) differs", v.Round)
|
|
|
|
|
}
|
|
|
|
|
sig, err := verifyRelease(release{round: v.Round, signature: unhex(t, v.Signature)})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
body := unhex(t, v.Body)
|
|
|
|
|
u := bls.NullKyberG2()
|
|
|
|
|
if err := decodePoint(u, body[:96], 96, "U"); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
gt := suite.Pair(sig, u)
|
|
|
|
|
raw, _ := gt.MarshalBinary()
|
|
|
|
|
mask, _ := h2(gt)
|
|
|
|
|
sigma := xor(body[96:112], mask)
|
|
|
|
|
r, err := h3(sigma, xor(body[112:], h4(sigma)))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
fk, err := unwrapTlock(body, sig)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
for _, c := range []struct{ name, got, want string }{
|
|
|
|
|
{"pairing", hex.EncodeToString(raw), v.Pairing},
|
|
|
|
|
{"H2", hex.EncodeToString(mask), v.H2},
|
|
|
|
|
{"sigma", hex.EncodeToString(sigma), v.Sigma},
|
|
|
|
|
{"H4", hex.EncodeToString(h4(sigma)), v.H4},
|
|
|
|
|
{"r", hex.EncodeToString(r.FillBytes(make([]byte, 32))), v.R},
|
|
|
|
|
{"file key", hex.EncodeToString(fk), v.FileKey},
|
|
|
|
|
} {
|
|
|
|
|
if c.got != c.want {
|
|
|
|
|
t.Fatalf("round %d: %s = %s, want %s", v.Round, c.name, c.got, c.want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestPinnedRoundTime(t *testing.T) {
|
|
|
|
|
// format3_single.json: round 1000 unlocks at 2023-08-23T15:59:24Z.
|
|
|
|
|
if got := roundTime(1000).Format("2006-01-02T15:04:05Z"); got != "2023-08-23T15:59:24Z" {
|
|
|
|
|
t.Fatal(got)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestAgeSecretKey checks the Bech32 encoder against age's own.
|
|
|
|
|
func TestAgeSecretKey(t *testing.T) {
|
|
|
|
|
var rec struct {
|
|
|
|
|
AccessMaterial string `json:"access_material"`
|
|
|
|
|
}
|
|
|
|
|
readJSON(t, filepath.Join(fixturesDir, "format3_time_and_key_portable.dkk.json"), &rec)
|
|
|
|
|
key := ageSecretKey(unhex(t, rec.AccessMaterial))
|
|
|
|
|
got, err := age.ParseX25519Identity(key)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if got.String() != key {
|
|
|
|
|
t.Fatalf("age re-encodes %s as %s", key, got.String())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestStreamEdges(t *testing.T) {
|
|
|
|
|
if _, err := streamDecrypt(make([]byte, 32), nil); err == nil {
|
|
|
|
|
t.Fatal("an empty STREAM must fail")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestTampered flips one byte in each part of a capsule: the tlock U, the
|
|
|
|
|
// sealed age payload and PAYLOAD_AGE must each make the recovery fail.
|
|
|
|
|
func TestTampered(t *testing.T) {
|
|
|
|
|
var rec fixtureRecord
|
|
|
|
|
readJSON(t, filepath.Join(fixturesDir, "format3_single.json"), &rec)
|
|
|
|
|
dkc, err := os.ReadFile(filepath.Join(fixturesDir, "format3_single.dkc"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
Spec v0.15 draft: remove the .dkr file
The author's decision of 7 October 2026. A release saved next to a capsule
cannot exist when the capsule is made, and once the date comes the capsule
can be opened: such a file only opens it again and does not cover the real
case, someone opening it decades later when drand is gone and nobody saved
anything. Long-term recovery rests instead on archives and cache services
that keep the releases of all rounds; a reader asks for its round and
verifies the signature against the pinned key.
Spec: the .dkr extension (section 20, back to v0.14), sections 1, 4, 8, 45,
47.1, 49, 50 (rewritten), 53, 62.1 (rule 28 removed, rule 26 reworded),
63, 70, 73, 74 (the datekeys.release .dkk extension dropped too), 76
(the v0.15 block, with the discarded design) and the annex 79. The
release object, the chain hash at step 10, step 9.c option B and the
archive format stay.
Code: decrypt -save-release and the command datekeys release are gone,
with writeRelease and their tests; decrypt -release FILE stays. The
release objects of testdata/releases are now <round>.cbor, and
TestVectorFilesAreCurrent fails on a file the generator no longer writes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
23 hours ago
|
|
|
relObj := encodeRelease(unhex(t, quicknetChainHash), rec.Release.Round, unhex(t, rec.Release.Signature))
|
Recovery check: open fixtures with the annex and no DateKeys code
scripts/recovery is a program that opens a capsule with only the Go
standard library, golang.org/x/crypto, filippo.io/age and the BLS12-381
library of drand/kyber-bls12381, as the informative annex of the draft
v0.15 describes it: the pinned Quicknet parameters, the release object,
the frame, the BLS verification of the release, the tlock stanza with H2,
H3 and H4, the age file of SEALED_CONTROL opened with its file key (HKDF,
header MAC and STREAM written out), the X25519 layers with age, and the
content of formats 1, 2 and 3. A test forbids importing this module, tlock
and drand. scripts/recovery_check.sh opens a time_only and a time_and_key
fixture of format 3 with it and compares what it recovers; scripts/check.sh
runs it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 day ago
|
|
|
sealedEnd := 16 + 0x79 + 0x1ca // PUBLIC_HEADER_LEN and SEALED_CONTROL_LEN of its prelude
|
|
|
|
|
stanza := bytes.Index(dkc, []byte("-> tlock"))
|
|
|
|
|
body := stanza + bytes.IndexByte(dkc[stanza:], '\n') + 1
|
|
|
|
|
for _, tc := range []struct {
|
|
|
|
|
name string
|
|
|
|
|
at int
|
|
|
|
|
}{
|
|
|
|
|
{"tlock U", body + 10},
|
|
|
|
|
{"sealed control STREAM", sealedEnd - 1},
|
|
|
|
|
{"PAYLOAD_AGE STREAM", len(dkc) - 1},
|
|
|
|
|
} {
|
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
|
|
|
bad := bytes.Clone(dkc)
|
|
|
|
|
bad[tc.at] ^= 0x01
|
v0.16: the key of words in the recovery annex, and a full last chunk
scripts/recovery opens a time_and_key capsule with the words of a key of
words (-words FILE), as the annex says in 79.7: the normalization without
tables for the text of the DateKeys lists (printable ASCII, the ASCII
spaces, á é í ó ú ü ñ and their capitals, and the marks U+0300 to U+036F),
and for any other text the full one, NFD, without the marks, simple lower
case and the spaces of 38.1, from UnicodeData.txt of Unicode 18.0.0
(-unicodedata FILE), checked by its SHA-256; then PBKDF2-HMAC-SHA256 of the
standard library. Its tests check both normalizations against every case
of wordkey.json and the two vectors of the annex.
Two fixtures of v0.16: format3_time_and_key_words, opened with the text of
the second vector of the annex, recorded in words_text with the identity it
gives; and format3_full_chunk, whose BODY and P are 65536 bytes, so that
PAYLOAD_AGE ends in a full STREAM chunk (79.5). recovery_check.sh opens
both. wordkey.json gains the text of the annex, also with its marks apart.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 hours ago
|
|
|
if _, err := recoverCapsule(bad, relObj, credential{}, io.Discard); err == nil {
|
Recovery check: open fixtures with the annex and no DateKeys code
scripts/recovery is a program that opens a capsule with only the Go
standard library, golang.org/x/crypto, filippo.io/age and the BLS12-381
library of drand/kyber-bls12381, as the informative annex of the draft
v0.15 describes it: the pinned Quicknet parameters, the release object,
the frame, the BLS verification of the release, the tlock stanza with H2,
H3 and H4, the age file of SEALED_CONTROL opened with its file key (HKDF,
header MAC and STREAM written out), the X25519 layers with age, and the
content of formats 1, 2 and 3. A test forbids importing this module, tlock
and drand. scripts/recovery_check.sh opens a time_only and a time_and_key
fixture of format 3 with it and compares what it recovers; scripts/check.sh
runs it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 day ago
|
|
|
t.Fatal("a tampered capsule opened")
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestImports keeps the program independent of DateKeys and of the drand
|
|
|
|
|
// tools: only the standard library, age, kyber and x/crypto.
|
|
|
|
|
func TestImports(t *testing.T) {
|
|
|
|
|
f, err := parser.ParseFile(token.NewFileSet(), "main.go", nil, parser.ImportsOnly)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
var errs []error
|
|
|
|
|
for _, imp := range f.Imports {
|
|
|
|
|
p, _ := strconv.Unquote(imp.Path.Value)
|
|
|
|
|
for _, banned := range []string{"g.activething.com/go/DateKeys", "github.com/drand/tlock", "github.com/drand/drand"} {
|
|
|
|
|
if strings.HasPrefix(p, banned) {
|
|
|
|
|
errs = append(errs, errors.New("forbidden import "+p))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if err := errors.Join(errs...); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
}
|