Arithmetic & Commitments
The core package — modular arithmetic over big.Int with explicit moduli, constant-time comparison, hash-to-field, Fiat–Shamir, safe primes, and the HMAC commitment scheme.
core is the one package in this library that does not use the curve abstraction. It works directly on math/big integers with an explicit modulus, and it exists because a handful of constructions — Paillier, the legacy sharing/v1 and dkg/gennaro layers, the older threshold ECDSA code — need integer arithmetic in a group whose order is not a curve order.
Reach for core when you are implementing something over the integers mod m for an m you chose yourself, need a byte-level HMAC commitment, or need RFC-shaped hash-to-field. Do not reach for it to do scalar arithmetic on a curve — curve.Scalar is faster, constant-time-oriented, and cannot silently escape its field.
Modular arithmetic
Every helper takes the modulus as its last argument and returns (*big.Int, error). The error is not decoration: it is how the package refuses nil inputs instead of panicking.
Add(x, y, m)?(*big.Int, error)
z = x + y mod m. If m is nil the result is the unbounded integer sum. Errors only if x or y is nil.
(*big.Int, error)Mul(x, y, m)?(*big.Int, error)
z = x * y mod m. If m is nil the result is the unbounded product. Errors only if x or y is nil.
(*big.Int, error)Exp(x, y, m)?(*big.Int, error)
z = x^y mod m. Thin wrapper over big.Int.Exp; a nil m means no reduction. Errors only if x or y is nil.
(*big.Int, error)Neg(x, m)?(*big.Int, error)
z = -x mod m, reduced into [0, m). m is required — nil m is an error.
(*big.Int, error)Inv(x, m)?(*big.Int, error)
y such that x*y = 1 mod m. Errors if x is not invertible mod m ('cannot compute the multiplicative inverse').
(*big.Int, error)Rand(m)?(*big.Int, error)
Cryptographically secure random integer strictly in the range 1 < r < m. Rejection-samples until r > 1.
(*big.Int, error)In(x, m)?error
Membership test: nil if 0 <= x < m, otherwise internal.ErrZmMembership.
errorAnyNil(values ...)?bool
true if any argument is nil. Used as the guard clause in every function above.
boolPackage-level integer constants are provided so you are not allocating them in loops: core.Zero, core.One, core.Two.
package main
import (
"fmt"
"math/big"
"github.com/sonr-io/crypto/core"
)
func main() {
m, _ := new(big.Int).SetString(
"208351617316091241234326746312124448251235562226470491514186331217050270460481", 10)
a, err := core.Rand(m)
if err != nil {
panic(err)
}
b, err := core.Rand(m)
if err != nil {
panic(err)
}
ab, _ := core.Mul(a, b, m)
aInv, err := core.Inv(a, m)
if err != nil {
panic(err) // a shares a factor with m
}
// (a*b) * a^-1 == b
back, _ := core.Mul(ab, aInv, m)
fmt.Println(core.ConstantTimeEq(back, b)) // true
fmt.Println(core.In(ab, m) == nil) // true
}
Constant-time comparison
func ConstantTimeEqByte(a, b *big.Int) byte // 0x1 if equal, 0x0 otherwise
func ConstantTimeEq(a, b *big.Int) bool // ConstantTimeEqByte(a, b) == 1
Both compare a.Bytes() against b.Bytes() via crypto/subtle.ConstantTimeCompare and compare Sign(). Two nil arguments compare equal; one nil compares unequal.
Hashing and hash-to-field
| Function | Signature | Purpose |
|---|---|---|
Hash |
Hash(msg []byte, curve elliptic.Curve) (*big.Int, error) |
Hash-to-field: one field element for the given curve. |
ExpandMessageXmd |
ExpandMessageXmd(f func() hash.Hash, msg, DST []byte, lenInBytes int) ([]byte, error) |
expand_message_xmd from the CFRG hash-to-curve draft, §5.4.1. |
I2OSP |
I2OSP(b, n int) []byte |
Integer-to-octet-string, n bytes, big-endian. |
OS2IP |
OS2IP(os []byte) *big.Int |
Octet-string-to-integer. |
FiatShamir |
FiatShamir(values ...*big.Int) ([]byte, error) |
Iterated HKDF challenge derivation; 32-byte output. |
ComputeHMAC |
ComputeHMAC(f func() hash.Hash, msg, k []byte) ([]byte, error) |
HMAC with an explicit hash constructor. |
Size |
const Size = sha256.Size |
32 — the width of commitments and nonces in this package. |
HashField |
struct{ Order, Characteristic, ExtensionDegree *big.Int } |
Describes the field F_p^k for the curve being hashed to. |
Params |
struct{ F *HashField; SecurityParameter int; Hash func() hash.Hash; L int } |
Per-curve hash-to-field parameters. |
Hash — curve support and its fixed DST
Hash looks up a Params for the curve, then runs expand_message_xmd and reduces to one field element. Supported curves and their parameters, read from getParams:
Curve (Params().Name) |
Security parameter | Hash | L (bytes) |
|---|---|---|---|
secp256k1 (btcec) |
128 | SHA-256 | 48 |
P-256 |
128 | SHA-256 | 48 |
P-384 / secp384r1 |
192 | SHA3-384 | 72 |
P-521 / secp521r1 |
256 | SHA-512 | 98 |
Bls12381G1 |
128 | SHA-256 | 48 |
ed25519 |
128 | SHA-256 | 48 |
Any other curve returns unsupported curve: <name>.
// Custom DST, correct expansion, your own reduction.
okm, err := core.ExpandMessageXmd(sha256.New, msg, []byte("MYPROTO-V01-CS01"), 48)
if err != nil {
return nil, err
}
e := new(big.Int).Mod(core.OS2IP(okm), fieldCharacteristic)
ExpandMessageXmd errors only when ceil(lenInBytes / hashSize) > 255. It takes the hash constructor, not a hash.Hash, and it will nil-dereference if you pass nil — there is no guard.
FiatShamir
Derives a 32-byte challenge from a sequence of integers. The construction is an iterated HKDF-SHA256: for each value, okm_i = HKDF(f_i || value_i || okm_{i-1}), where f_i is a 32-byte prefix whose leading byte decrements per iteration (0xFF, then 0xFE, …). The source cites Signal’s X3DH and XEdDSA notes as the design source. info is the fixed string Coinbase tECDSA 1.0; the salt is 32 zero bytes.
challenge, err := core.FiatShamir(commitment, publicKey, nonce)
Safe primes
func GenerateSafePrime(bits uint) (*big.Int, error)
Returns a prime p = 2q + 1 where q is also prime (a Sophie Germain prime), with p of the requested bit length. bits must be at least 3. The implementation picks a bits-1-bit prime q, computes 2q + 1, and retries until ProbablyPrime accepts it with max(bits/16, 8) Miller–Rabin rounds.
Commitments
core ships one commitment scheme, and it is a hash commitment — not Pedersen, not polynomial. It commits to bytes, not to a group element.
type Commitment []byte // 32 bytes: HMAC-SHA256(key = nonce, msg)
type Witness struct {
Msg []byte
// unexported: r [32]byte, the random nonce
}
func Commit(msg []byte) (Commitment, *Witness, error)
func Open(c Commitment, d Witness) (bool, error)
Commit draws a 32-byte nonce from crypto/rand and returns HMAC-SHA256(msg, key = nonce) as the commitment, with the nonce hidden inside the Witness. Open recomputes the HMAC from d.Msg and the witness nonce and compares against c with subtle.ConstantTimeCompare.
package main
import (
"encoding/json"
"fmt"
"github.com/sonr-io/crypto/core"
)
func main() {
// Committer: publish c, keep w secret until the reveal phase.
c, w, err := core.Commit([]byte("bid: 42"))
if err != nil {
panic(err)
}
fmt.Println(len(c) == core.Size) // true, 32 bytes
// Witness marshals to JSON (msg + nonce) so it can be sent on reveal.
wire, _ := json.Marshal(w)
// Verifier: after receiving the witness.
var got core.Witness
if err := json.Unmarshal(wire, &got); err != nil {
panic(err)
}
ok, err := core.Open(c, got)
if err != nil {
panic(err)
}
fmt.Println(ok) // true
}
Properties as implemented. Hiding rests on HMAC-SHA256 being a PRF under the fresh 32-byte random key — the commitment is a PRF evaluation keyed by a secret nonce, so it reveals nothing about msg to anyone without the nonce. Binding rests on collision resistance: to open the same 32-byte commitment to a different message you would need HMAC(msg', k') == HMAC(msg, k).
The internal package
go doc github.com/sonr-io/crypto/internal lists a handful of tempting helpers:
func B10(s string) *big.Int
func BigInt2Ed25519Point(y *big.Int) (*edwards25519.Point, error)
func BigInt2Ed25519Scalar(x *big.Int) (*edwards25519.Scalar, error)
func ByteSub(b []byte)
func CalcFieldSize(curve elliptic.Curve) int
func Hash(info []byte, values ...[]byte) ([]byte, error)
func ReverseScalarBytes(inBytes []byte) []byte
plus the sentinel errors this library returns from core: ErrNotOnCurve, ErrPointsDistinctCurves, ErrZmMembership, ErrResidueOne, ErrNCannotBeZero, ErrNilArguments, ErrZeroValue, ErrInvalidRound, ErrIncorrectCount, ErrInvalidJson. There are also two vendored Ed25519 helper packages, internal/ed25519/edwards25519 and internal/ed25519/extra25519 (the latter with PrivateKeyToCurve25519, PublicKeyToCurve25519, HashToEdwards, RepresentativeToPublicKey, ScalarBaseMult).
internal.ReverseScalarBytes and internal.CalcFieldSize are the two you will most want and most miss; both are two lines and trivially reimplemented ((curve.Params().BitSize + 7) / 8 for the latter).