niko_trust/pkg/address/pubkey.go
Niko Marmeladkov 3bf13fa488 Public SDK packages, proxy-aware rate limits, service login recipe
- internal/{address,identity,protocol,tce,transport,verify} -> pkg/ so
  external Go projects can import the verified core; invariant tests
  updated for the new paths
- Config.TrustProxy: key rate limiting by X-Forwarded-For when the relay
  sits behind a reverse proxy (off by default, header never trusted
  otherwise)
- examples/service + examples/approve: complete passwordless login round
  trip (mint request -> wallet approves -> local verify), run live in CI
- docs/SERVICE-GUIDE.md: the integration recipe
2026-08-26 12:49:54 +03:00

125 lines
4.5 KiB
Go

package address
import (
"bytes"
"crypto/ed25519"
"errors"
"filippo.io/edwards25519"
)
// Public key validation.
//
// Go's crypto/ed25519.Verify follows RFC 8032 and deliberately performs no
// checks on the public key beyond its length. That is correct for RFC 8032,
// but it is not sufficient for a protocol in which an attacker chooses the
// public key, because several classes of key make signatures meaningless:
//
// - The all-zero key encodes a point of order 4. The all-zero signature
// verifies against that key for *every* message. An attacker who
// registered such a key would own an identity whose signature verifies on
// any claim or approval anyone cares to construct, which is a direct
// break of INV-1.
//
// - The identity element (y = 1) behaves the same way.
//
// - Small-order and mixed-order points admit signatures that verify under
// more than one public key, destroying the "who said this" property that
// the entire protocol rests on.
//
// - Non-canonical encodings (field elements >= p) give two distinct byte
// strings that denote the same curve point, hence two distinct addresses
// for one identity, which violates INV-8.
//
// The protocol therefore requires every public key entering the system to be a
// canonically encoded point in the prime-order subgroup. The check is
// performed once, at address construction, so that every Address in the
// program has already been validated and no later code has to remember to do
// it.
var (
// ErrKeySize is returned when a key is not 32 bytes.
ErrKeySize = errors.New("address: public key must be 32 bytes")
// ErrKeyNotOnCurve is returned when the key does not decode to a valid
// Edwards25519 point.
ErrKeyNotOnCurve = errors.New("address: public key is not a valid curve point")
// ErrKeyNonCanonical is returned when the key is a valid point encoded in
// a non-canonical way (a field element that is not fully reduced).
ErrKeyNonCanonical = errors.New("address: public key encoding is non-canonical")
// ErrKeySmallOrder is returned when the key lies in the small-order
// torsion subgroup, for which signatures are forgeable or ambiguous.
ErrKeySmallOrder = errors.New("address: public key has small order")
)
// ValidatePubKey reports whether pub is usable as a trust identity key.
//
// It returns nil only for a 32-byte, canonically encoded Edwards25519 point
// that is not annihilated by multiplication by the cofactor. Honest keys
// produced by ed25519.GenerateKey always satisfy this.
func ValidatePubKey(pub []byte) error {
if len(pub) != ed25519.PublicKeySize {
return ErrKeySize
}
p, err := new(edwards25519.Point).SetBytes(pub)
if err != nil {
// SetBytes rejects non-canonical field encodings and points that are
// not on the curve. Both are fatal, but they are distinguishable by
// re-encoding below only for the on-curve case, so report the generic
// curve error unless we can say something more precise.
if isNonCanonicalEncoding(pub) {
return ErrKeyNonCanonical
}
return ErrKeyNotOnCurve
}
// A canonical point re-encodes to exactly the bytes it came from. Any
// difference means the input was an alternative spelling of this point.
if !bytes.Equal(p.Bytes(), pub) {
return ErrKeyNonCanonical
}
// Multiplying by the cofactor (8) maps every small-order point to the
// identity. A key in the prime-order subgroup never maps to the identity,
// because the group has prime order and the key is not itself the
// identity.
if new(edwards25519.Point).MultByCofactor(p).Equal(edwards25519.NewIdentityPoint()) == 1 {
return ErrKeySmallOrder
}
return nil
}
// isNonCanonicalEncoding reports whether the y coordinate encoded in pub is
// greater than or equal to the field prime p = 2^255 - 19. Such an encoding is
// rejected outright; it exists only to produce a second byte string for a
// point that already has a canonical encoding.
func isNonCanonicalEncoding(pub []byte) bool {
if len(pub) != ed25519.PublicKeySize {
return false
}
// Little-endian comparison against p, ignoring the sign bit in the MSB.
var y [32]byte
copy(y[:], pub)
y[31] &= 0x7f
// p = 2^255 - 19 little-endian.
var prime = [32]byte{
0xed, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f,
}
for i := 31; i >= 0; i-- {
if y[i] < prime[i] {
return false
}
if y[i] > prime[i] {
return true
}
}
// Exactly equal to p is also non-canonical.
return true
}