You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
DateKeys/profile/point_test.go

113 lines
4.2 KiB

Spec v0.8.2 amendment: canonical point encoding; no library error text 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>
2 weeks ago
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)
}
}
}

Powered by TurnKey Linux.