Amendment of the unreleased v0.8.2, recorded in §76 with its case: the second implementation's phase-2 research found that tlock-js over @noble/curves 1.9.7 accepts U re-encoded as c0 + p and a signature x + p and returns the same file key, while the reference rejects both (noble 1.9.7 differed from kilic on 5,615 of 41,686 encodings), and the spec did not say which encodings are valid. - §12.2 defines the canonical encoding of a BLS12-381 point (drand's compressed ZCash form) and requires decoders to reject every other byte string; §12.1 applies it to public_key. - §63 step 10 applies it to the release signature (ERR_RELEASE_INVALID) and step 11 defines the tlock stanza body U || V || W (96 + 16 + 16 bytes for Quicknet) with a canonical, non-infinity U (ERR_INTEGRITY). - §64 gains ten mutations, exported to mutations.json (65 cases). The signature x + p case uses published Quicknet round 1004, the first after 1000 whose x allows x + p < 2^381. The reference already gave every stated code and step. Errors no longer copy text from tlock, kyber, age, drand or kyber-bls12381. kyber's IBE error carried the candidate plaintext and r, and with one bit of W flipped the message disclosed the real tlock file key with that bit flipped. Every such place now uses a fixed reason with its normative sentinel; TestTlockFailureDiagnosticsCarryNoSecrets fails with the old wrapping. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>v0.8.2
parent
692cf87db1
commit
f6f2e9f55f
@ -0,0 +1,134 @@
|
|||||||
|
package capsule_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/drand/drand/v2/common"
|
||||||
|
"github.com/drand/tlock"
|
||||||
|
|
||||||
|
datekeys "g.activething.com/go/DateKeys"
|
||||||
|
"g.activething.com/go/DateKeys/agewrap"
|
||||||
|
"g.activething.com/go/DateKeys/capsule"
|
||||||
|
"g.activething.com/go/DateKeys/internal/testkit"
|
||||||
|
"g.activething.com/go/DateKeys/profile"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Spec §63 step 11: when the IBE check of the tlock stanza fails, kyber
|
||||||
|
// reports in its error the candidate plaintext, W xor H4(sigma), and r. A
|
||||||
|
// third party who edits W learns FK_TIME from the candidate and the edit, so
|
||||||
|
// neither the error of Open nor the details of its checks may carry them:
|
||||||
|
// the text of tlock and kyber is never copied.
|
||||||
|
func TestTlockFailureDiagnosticsCarryNoSecrets(t *testing.T) {
|
||||||
|
e, err := testkit.NewMutationEnv(fixtureDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
f := e.TimeOnly
|
||||||
|
fk, err := f.TimeFileKey()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// W ends the stanza body: flipping its last bit flips the last bit of
|
||||||
|
// the candidate plaintext, sigma being unchanged.
|
||||||
|
in, err := f.WithTlockBody(func(b []byte) ([]byte, error) {
|
||||||
|
c := bytes.Clone(b)
|
||||||
|
c[len(c)-1] ^= 1
|
||||||
|
return c, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
candidate := bytes.Clone(fk)
|
||||||
|
candidate[len(candidate)-1] ^= 1
|
||||||
|
secrets := map[string]string{
|
||||||
|
"FK_TIME": string(fk),
|
||||||
|
"FK_TIME in hex": hex.EncodeToString(fk),
|
||||||
|
"candidate plaintext": string(candidate),
|
||||||
|
"candidate plaintext in hex": hex.EncodeToString(candidate),
|
||||||
|
}
|
||||||
|
|
||||||
|
// What tlock reports for this body: kyber's error, with the candidate
|
||||||
|
// and r as kyber prints it.
|
||||||
|
parts, err := testkit.Split(in.DKC)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
stanzas, err := agewrap.Stanzas(bytes.NewReader(parts.Sealed))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
p := profile.Quicknet()
|
||||||
|
scheme, err := p.DrandScheme()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
key := scheme.KeyGroup.Point()
|
||||||
|
if err := key.UnmarshalBinary(p.PublicKey); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ct, err := tlock.BytesToCiphertext(*scheme, stanzas[0].Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_, tlockErr := tlock.TimeUnlock(*scheme, key, common.Beacon{Round: f.Published.Round, Signature: f.Published.Signature}, ct)
|
||||||
|
if tlockErr == nil {
|
||||||
|
t.Fatal("tlock accepts the edited W")
|
||||||
|
}
|
||||||
|
if msg := tlockErr.Error(); strings.Contains(msg, string(candidate)) {
|
||||||
|
if i := strings.LastIndex(msg, ", r "); i >= 0 {
|
||||||
|
r := msg[i+len(", r "):]
|
||||||
|
secrets["r as kyber prints it"] = r
|
||||||
|
if b, err := hex.DecodeString(r); err == nil {
|
||||||
|
secrets["r"] = string(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
t.Logf("tlock no longer reports the candidate plaintext: %q", msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
opened, err := capsule.Open(context.Background(), io.Discard, bytes.NewReader(in.DKC), capsule.OpenOptions{
|
||||||
|
Registry: testkit.Registry(), Source: testkit.NewSource(f.Published), Now: testkit.Fixed(f.Unlock),
|
||||||
|
})
|
||||||
|
checks := opened.Inspection.Checks
|
||||||
|
if last := checks[len(checks)-1]; !errors.Is(err, datekeys.ErrIntegrity) || last.Step != 11 || last.Error != "ERR_INTEGRITY" {
|
||||||
|
t.Fatalf("got %v at step %d, want ERR_INTEGRITY at step 11", err, last.Step)
|
||||||
|
}
|
||||||
|
texts := []string{err.Error()}
|
||||||
|
for _, c := range checks {
|
||||||
|
texts = append(texts, c.Detail)
|
||||||
|
}
|
||||||
|
for name, s := range secrets {
|
||||||
|
for _, text := range texts {
|
||||||
|
if strings.Contains(text, s) {
|
||||||
|
t.Errorf("the %s is in %q", name, text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The failures of age get fixed reasons, but a failure of the caller's
|
||||||
|
// writer at step 17 is not one of age: it keeps its own text, and the code
|
||||||
|
// it always had.
|
||||||
|
func TestPlaintextWriterFailureKeepsItsText(t *testing.T) {
|
||||||
|
e, err := testkit.NewMutationEnv(fixtureDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
f := e.TimeOnly
|
||||||
|
opened, err := capsule.Open(context.Background(), failingWriter{}, bytes.NewReader(f.DKC), capsule.OpenOptions{
|
||||||
|
Registry: testkit.Registry(), Source: testkit.NewSource(f.Published), Now: testkit.Fixed(f.Unlock),
|
||||||
|
})
|
||||||
|
checks := opened.Inspection.Checks
|
||||||
|
if last := checks[len(checks)-1]; !errors.Is(err, datekeys.ErrIntegrity) || last.Step != 17 {
|
||||||
|
t.Fatalf("got %v at step %d, want ERR_INTEGRITY at step 17", err, last.Step)
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "disk full") || strings.Contains(err.Error(), "STREAM") {
|
||||||
|
t.Fatalf("the error of the writer is not reported as such: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,117 @@
|
|||||||
|
package capsule_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/hex"
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"filippo.io/age"
|
||||||
|
|
||||||
|
"g.activething.com/go/DateKeys/agewrap"
|
||||||
|
"g.activething.com/go/DateKeys/internal/testkit"
|
||||||
|
"g.activething.com/go/DateKeys/profile"
|
||||||
|
)
|
||||||
|
|
||||||
|
// reducingIdentity models a reader whose decoder reduces coordinates modulo
|
||||||
|
// p: it reduces c0 of U before the strict tlock identity sees the stanza.
|
||||||
|
type reducingIdentity struct{ id *agewrap.TimeIdentity }
|
||||||
|
|
||||||
|
func (r reducingIdentity) Unwrap(stanzas []*age.Stanza) ([]byte, error) {
|
||||||
|
s := *stanzas[0]
|
||||||
|
s.Body = testkit.ReduceCoordinate(s.Body, testkit.CoordinateLen)
|
||||||
|
return r.id.Unwrap([]*age.Stanza{&s})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spec §12.2, §64: the point mutations of the exported corpus differ from a
|
||||||
|
// capsule that opens only in the encoding of one point. A reader that
|
||||||
|
// reduces coordinates modulo p opens the capsules with c0 + p in U and with
|
||||||
|
// x + p in the signature, which the reference rejects
|
||||||
|
// (TestExportedMutationCorpus), and every edited tlock body keeps a valid
|
||||||
|
// header MAC, so that only the rules of the body reject it.
|
||||||
|
func TestPointMutationsChangeOnlyTheEncoding(t *testing.T) {
|
||||||
|
var f testkit.MutationFile
|
||||||
|
if err := testkit.ReadJSON(mutationsFile, &f); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
cases := map[string]*testkit.MutationCase{}
|
||||||
|
for i := range f.Cases {
|
||||||
|
cases[f.Cases[i].Name] = &f.Cases[i]
|
||||||
|
}
|
||||||
|
input := func(name string) *testkit.MutationInput {
|
||||||
|
t.Helper()
|
||||||
|
c := cases[name]
|
||||||
|
if c == nil {
|
||||||
|
t.Fatalf("no case %q", name)
|
||||||
|
}
|
||||||
|
in, err := c.Input(fixtureDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return in
|
||||||
|
}
|
||||||
|
|
||||||
|
// The frozen capsule of round XPlusPRound opens with the signature
|
||||||
|
// reduced modulo p, the published one.
|
||||||
|
in := input("release signature re-encoded with x + p")
|
||||||
|
reduced := testkit.ReduceCoordinate(in.Release.Signature, 0)
|
||||||
|
if !bytes.Equal(reduced, testkit.Release(testkit.XPlusPRound).Signature) {
|
||||||
|
t.Fatalf("x + p reduces to %x", reduced)
|
||||||
|
}
|
||||||
|
in.Release.Signature = reduced
|
||||||
|
if v, err := in.Open(); err != nil || v.Err != nil {
|
||||||
|
t.Fatalf("with the published signature: %v %v", err, v.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The tlock bodies: the header MAC of OUTER_TIME_AGE verifies with
|
||||||
|
// FK_TIME, and OUTER_TIME_AGE decrypts to the CONTROL_CBOR of the fixture.
|
||||||
|
e, err := testkit.NewMutationEnv(fixtureDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
fk, err := e.TimeOnly.TimeFileKey()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
control, err := hex.DecodeString(e.TimeOnly.ControlCBOR)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
sealed := func(in *testkit.MutationInput, id age.Identity) []byte {
|
||||||
|
t.Helper()
|
||||||
|
parts, err := testkit.Split(in.DKC)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
r, err := age.Decrypt(bytes.NewReader(parts.Sealed), id)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
out, err := io.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
for _, name := range []string{
|
||||||
|
"tlock stanza U re-encoded with c0 + p",
|
||||||
|
"tlock stanza U is the point at infinity",
|
||||||
|
"tlock stanza U with the infinity flag and a payload",
|
||||||
|
"tlock stanza body of 127 bytes",
|
||||||
|
"tlock stanza body of 129 bytes",
|
||||||
|
"negated release signature and U re-encoded with c0 + p",
|
||||||
|
} {
|
||||||
|
if got := sealed(input(name), age.NewInjectedFileKeyIdentity(fk)); !bytes.Equal(got, control) {
|
||||||
|
t.Fatalf("%s: OUTER_TIME_AGE does not decrypt to the fixture's CONTROL_CBOR", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// With c0 reduced modulo p, U is the U of the fixture again.
|
||||||
|
in = input("tlock stanza U re-encoded with c0 + p")
|
||||||
|
id, err := agewrap.NewTimeIdentity(profile.Quicknet(), in.Release.Round, *in.Release)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := sealed(in, reducingIdentity{id}); !bytes.Equal(got, control) {
|
||||||
|
t.Fatal("a reader that reduces c0 does not open OUTER_TIME_AGE")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,99 @@
|
|||||||
|
package testkit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"math/big"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Re-encodings of BLS12-381 points in the compressed form of drand (spec
|
||||||
|
// §12.2), for the mutations and tests of the canonical point encoding. They
|
||||||
|
// work on the bytes alone: a flag byte whose low five bits start a
|
||||||
|
// big-endian coordinate, and coordinates of 48 bytes, x in G1 and c1 then c0
|
||||||
|
// in G2.
|
||||||
|
|
||||||
|
// FieldModulus is p, the modulus of the base field of BLS12-381.
|
||||||
|
var FieldModulus, _ = new(big.Int).SetString("1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab", 16)
|
||||||
|
|
||||||
|
// Flags of the first byte of a compressed point (spec §12.2).
|
||||||
|
const (
|
||||||
|
FlagCompressed = 0x80
|
||||||
|
FlagInfinity = 0x40
|
||||||
|
FlagSort = 0x20
|
||||||
|
flagMask = FlagCompressed | FlagInfinity | FlagSort
|
||||||
|
)
|
||||||
|
|
||||||
|
// CoordinateLen is the size of a coordinate of Fp, and of a compressed point
|
||||||
|
// of G1; a compressed point of G2 takes two.
|
||||||
|
const CoordinateLen = 48
|
||||||
|
|
||||||
|
// AddModulus returns enc with p added to the 48-byte coordinate at byte
|
||||||
|
// offset at: 0 for x in G1 and for c1 in G2, 48 for c0 in G2. The flags are
|
||||||
|
// kept. The result reduces modulo p to the coordinate of enc, so a decoder
|
||||||
|
// that reduces coordinates reads the point of enc, but it is not a canonical
|
||||||
|
// encoding (spec §12.2). It fails when the sum does not fit in the bits of
|
||||||
|
// the coordinate: at offset 0, the 381 bits below the flags.
|
||||||
|
func AddModulus(enc []byte, at int) ([]byte, error) {
|
||||||
|
if at < 0 || at%CoordinateLen != 0 || at+CoordinateLen > len(enc) {
|
||||||
|
return nil, errors.New("testkit: no coordinate at that offset")
|
||||||
|
}
|
||||||
|
c := bytes.Clone(enc[at : at+CoordinateLen])
|
||||||
|
bits := 8 * CoordinateLen
|
||||||
|
if at == 0 {
|
||||||
|
c[0] &^= flagMask
|
||||||
|
bits -= 3
|
||||||
|
}
|
||||||
|
sum := new(big.Int).Add(new(big.Int).SetBytes(c), FieldModulus)
|
||||||
|
if sum.BitLen() > bits {
|
||||||
|
return nil, errors.New("testkit: the coordinate plus p does not fit")
|
||||||
|
}
|
||||||
|
out := bytes.Clone(enc)
|
||||||
|
sum.FillBytes(out[at : at+CoordinateLen])
|
||||||
|
if at == 0 {
|
||||||
|
out[0] |= enc[0] & flagMask
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReduceCoordinate returns enc with the 48-byte coordinate at byte offset at
|
||||||
|
// reduced modulo p, the flags kept: what a decoder that reduces coordinates
|
||||||
|
// reads, and the inverse of AddModulus.
|
||||||
|
func ReduceCoordinate(enc []byte, at int) []byte {
|
||||||
|
out := bytes.Clone(enc)
|
||||||
|
c := out[at : at+CoordinateLen]
|
||||||
|
flags := byte(0)
|
||||||
|
if at == 0 {
|
||||||
|
flags = c[0] & flagMask
|
||||||
|
c[0] &^= flagMask
|
||||||
|
}
|
||||||
|
new(big.Int).Mod(new(big.Int).SetBytes(c), FieldModulus).FillBytes(c)
|
||||||
|
c[0] |= flags
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Negated returns the encoding of the negated point: the same x, the sort
|
||||||
|
// flag flipped (spec §12.2). It is canonical when enc is the canonical
|
||||||
|
// encoding of a point other than the point at infinity.
|
||||||
|
func Negated(enc []byte) []byte {
|
||||||
|
out := bytes.Clone(enc)
|
||||||
|
out[0] ^= FlagSort
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// InfinityWithPayload returns enc with the flags of the point at infinity,
|
||||||
|
// compression and infinity without sort, over its own coordinate bits: an
|
||||||
|
// encoding of the point at infinity with a payload, which spec §12.2
|
||||||
|
// forbids.
|
||||||
|
func InfinityWithPayload(enc []byte) []byte {
|
||||||
|
out := bytes.Clone(enc)
|
||||||
|
out[0] = out[0]&^flagMask | FlagCompressed | FlagInfinity
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Infinity returns the canonical encoding of the point at infinity in n
|
||||||
|
// bytes, 48 for G1 and 96 for G2: 0xc0, then zeros (spec §12.2).
|
||||||
|
func Infinity(n int) []byte {
|
||||||
|
out := make([]byte, n)
|
||||||
|
out[0] = FlagCompressed | FlagInfinity
|
||||||
|
return out
|
||||||
|
}
|
||||||
@ -0,0 +1,112 @@
|
|||||||
|
package profile_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/drand/drand/v2/crypto"
|
||||||
|
|
||||||
|
datekeys "g.activething.com/go/DateKeys"
|
||||||
|
"g.activething.com/go/DateKeys/internal/testkit"
|
||||||
|
"g.activething.com/go/DateKeys/profile"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Spec §12.2: the decoders of drand, through which the reference reads public
|
||||||
|
// keys, release signatures and the U of a tlock stanza, accept exactly the
|
||||||
|
// canonical encodings. The reference relies on them for the rule, so this
|
||||||
|
// test fails if a dependency update makes them lenient.
|
||||||
|
func TestDrandPointDecodersAreCanonical(t *testing.T) {
|
||||||
|
s, err := crypto.SchemeFromName(crypto.SigsOnG1ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
g1 := func(b []byte) error { return s.SigGroup.Point().UnmarshalBinary(b) }
|
||||||
|
g2 := func(b []byte) error { return s.KeyGroup.Point().UnmarshalBinary(b) }
|
||||||
|
must := func(b []byte, err error) []byte {
|
||||||
|
t.Helper()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
cleared := func(b []byte, flags byte) []byte {
|
||||||
|
c := bytes.Clone(b)
|
||||||
|
c[0] &^= flags
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
key, sig := profile.Quicknet().PublicKey, testkit.Release(1000).Signature
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
decode func([]byte) error
|
||||||
|
in []byte
|
||||||
|
ok bool
|
||||||
|
}{
|
||||||
|
{"G1: a release signature", g1, sig, true},
|
||||||
|
{"G1: the point at infinity", g1, testkit.Infinity(48), true},
|
||||||
|
{"G2: the Quicknet public key", g2, key, true},
|
||||||
|
{"G2: the point at infinity", g2, testkit.Infinity(96), true},
|
||||||
|
{"G1: x + p", g1, must(testkit.AddModulus(testkit.Release(testkit.XPlusPRound).Signature, 0)), false},
|
||||||
|
{"G2: c1 + p", g2, must(testkit.AddModulus(key, 0)), false},
|
||||||
|
{"G2: c0 + p", g2, must(testkit.AddModulus(key, testkit.CoordinateLen)), false},
|
||||||
|
{"G1: infinity flag and a payload", g1, testkit.InfinityWithPayload(sig), false},
|
||||||
|
{"G2: infinity flag and a payload", g2, testkit.InfinityWithPayload(key), false},
|
||||||
|
{"G1: the point at infinity with the sort flag", g1, testkit.Negated(testkit.Infinity(48)), false},
|
||||||
|
{"G2: the point at infinity with the sort flag", g2, testkit.Negated(testkit.Infinity(96)), false},
|
||||||
|
{"G1: the point at infinity without the compression flag", g1, cleared(testkit.Infinity(48), testkit.FlagCompressed), false},
|
||||||
|
{"G1: compression flag cleared", g1, cleared(sig, testkit.FlagCompressed), false},
|
||||||
|
{"G2: compression flag cleared", g2, cleared(key, testkit.FlagCompressed), false},
|
||||||
|
{"G1: 96 bytes, the length of the uncompressed form", g1, append(bytes.Clone(sig), sig...), false},
|
||||||
|
{"G2: 192 bytes, the length of the uncompressed form", g2, append(bytes.Clone(key), key...), false},
|
||||||
|
{"G1: 47 bytes", g1, sig[:47], false},
|
||||||
|
{"G2: 97 bytes", g2, append(bytes.Clone(key), 0), false},
|
||||||
|
{"G1: a point of the curve outside the prime-order subgroup", g1, offSubgroupG1(t), false},
|
||||||
|
} {
|
||||||
|
if err := tc.decode(tc.in); (err == nil) != tc.ok {
|
||||||
|
t.Errorf("%s: decoded %v, want %v (%v)", tc.name, err == nil, tc.ok, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spec §12.1, §12.2: the public key of a profile is the canonical encoding of
|
||||||
|
// a point of the key group other than the point at infinity. Each profile
|
||||||
|
// below is otherwise valid, its chain hash computed over the exact bytes of
|
||||||
|
// its key, and is refused whether it is validated or decoded.
|
||||||
|
func TestPublicKeyEncodingIsCanonical(t *testing.T) {
|
||||||
|
key := profile.Quicknet().PublicKey
|
||||||
|
c1PlusP, err := testkit.AddModulus(key, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
c0PlusP, err := testkit.AddModulus(key, testkit.CoordinateLen)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
uncompressed := bytes.Clone(key)
|
||||||
|
uncompressed[0] &^= testkit.FlagCompressed
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
key []byte
|
||||||
|
}{
|
||||||
|
{"c1 + p", c1PlusP},
|
||||||
|
{"c0 + p", c0PlusP},
|
||||||
|
{"the point at infinity", testkit.Infinity(96)},
|
||||||
|
{"infinity flag and a payload", testkit.InfinityWithPayload(key)},
|
||||||
|
{"compression flag cleared", uncompressed},
|
||||||
|
{"192 bytes, the length of the uncompressed form", append(bytes.Clone(key), key...)},
|
||||||
|
} {
|
||||||
|
p := profile.Quicknet()
|
||||||
|
p.PublicKey = tc.key
|
||||||
|
copy(p.ChainHash[:], chainHashFormula(p))
|
||||||
|
if err := p.Validate(); !errors.Is(err, datekeys.ErrUnknownProfile) {
|
||||||
|
t.Errorf("%s: Validate: %v", tc.name, err)
|
||||||
|
}
|
||||||
|
b, err := p.CanonicalCBOR()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := profile.Decode(b); !errors.Is(err, datekeys.ErrUnknownProfile) {
|
||||||
|
t.Errorf("%s: Decode: %v", tc.name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in new issue