feat(eddsa): add support for EdDSA varsigs - and common Ed25519 and Ed448 algorithms

This commit is contained in:
Steve Moyer
2025-07-07 15:42:11 -04:00
parent 8b710b7e23
commit 03bcfb7b16
7 changed files with 225 additions and 10 deletions

View File

@@ -1,5 +1,21 @@
package varsig
// Ed25519 produces a varsig that describes the associated algorithm defined
// by the [IANA JOSE specification].
//
// [IANA JOSE specidication]: https://www.iana.org/assignments/jose/jose.xhtml#web-signature-encryption-algorithms
func Ed25519(payloadEncoding PayloadEncoding, opts ...Option) (*EdDSAVarsig, error) {
return NewEdDSAVarsig(CurveEd25519, HashAlgorithmSHA512, payloadEncoding, opts...)
}
// Ed448 produces a varsig that describes the associated algorithm defined
// by the [IANA JOSE specification].
//
// [IANA JOSE specidication]: https://www.iana.org/assignments/jose/jose.xhtml#web-signature-encryption-algorithms
func Ed448(payloadEncoding PayloadEncoding, opts ...Option) (*EdDSAVarsig, error) {
return NewEdDSAVarsig(CurveEd448, HashAlgorithmShake256, payloadEncoding, opts...)
}
// RS256 produces a varsig that describes the associated algorithm defined
// by the [IANA JOSE specification].
//

View File

@@ -8,11 +8,27 @@ import (
"github.com/selesy/go-varsig"
)
func TestEd25519(t *testing.T) {
t.Parallel()
in := mustVarsig[varsig.EdDSAVarsig](t)(varsig.Ed25519(varsig.PayloadEncodingDAGCBOR))
out := roundTrip(t, in, "3401ed01ed011371")
assertEdDSAEqual(t, in, out)
}
func TestEd448(t *testing.T) {
t.Parallel()
in := mustVarsig[varsig.EdDSAVarsig](t)(varsig.Ed448(varsig.PayloadEncodingDAGCBOR))
out := roundTrip(t, in, "3401ed0183241971")
assertEdDSAEqual(t, in, out)
}
func TestRS256(t *testing.T) {
t.Parallel()
in := mustVarsig[varsig.RSAVarsig](t)(varsig.RS256(0x100, varsig.PayloadEncodingDAGCBOR))
out := roundTrip(t, in, "NAGFJBKAAnE")
out := roundTrip(t, in, "3401852412800271")
assertRSAEqual(t, in, out)
}
@@ -20,7 +36,7 @@ func TestRS384(t *testing.T) {
t.Parallel()
in := mustVarsig[varsig.RSAVarsig](t)(varsig.RS384(0x100, varsig.PayloadEncodingDAGCBOR))
out := roundTrip(t, in, "NAGFJCCAAnE")
out := roundTrip(t, in, "3401852420800271")
assertRSAEqual(t, in, out)
}
@@ -28,10 +44,17 @@ func TestRS512(t *testing.T) {
t.Parallel()
in := mustVarsig[varsig.RSAVarsig](t)(varsig.RS512(0x100, varsig.PayloadEncodingDAGCBOR))
out := roundTrip(t, in, "NAGFJBOAAnE")
out := roundTrip(t, in, "3401852413800271")
assertRSAEqual(t, in, out)
}
func assertEdDSAEqual(t *testing.T, in, out *varsig.EdDSAVarsig) {
t.Helper()
assert.Equal(t, in.Curve(), out.Curve())
assert.Equal(t, in.HashAlgorithm(), out.HashAlgorithm())
}
func assertRSAEqual(t *testing.T, in, out *varsig.RSAVarsig) {
t.Helper()

View File

@@ -14,9 +14,10 @@ type HashAlgorithm uint64
const (
HashAlgorithmUnspecified HashAlgorithm = 0x00
HashAlgorithmSHA256 HashAlgorithm = HashAlgorithm(multicodec.Sha2_256)
HashAlgorithmSHA384 HashAlgorithm = HashAlgorithm(multicodec.Sha2_384)
HashAlgorithmSHA512 HashAlgorithm = HashAlgorithm(multicodec.Sha2_512)
HashAlgorithmSHA256 = HashAlgorithm(multicodec.Sha2_256)
HashAlgorithmSHA384 = HashAlgorithm(multicodec.Sha2_384)
HashAlgorithmSHA512 = HashAlgorithm(multicodec.Sha2_512)
HashAlgorithmShake256 = HashAlgorithm(multicodec.Shake256)
)
// DecodeHashAlgorithm reads and validates the expected hash algorithm
@@ -34,8 +35,9 @@ func DecodeHashAlgorithm(r *bytes.Reader) (HashAlgorithm, error) {
HashAlgorithmSHA256: {},
HashAlgorithmSHA384: {},
HashAlgorithmSHA512: {},
HashAlgorithmShake256: {},
}[h]; !ok {
return HashAlgorithmUnspecified, ErrUnknownHashAlgorithm
return HashAlgorithmUnspecified, fmt.Errorf("%w: %x", ErrUnknownHashAlgorithm, h)
}
return h, nil

116
eddsa.go Normal file
View File

@@ -0,0 +1,116 @@
package varsig
import (
"bytes"
"crypto/ed25519"
"encoding/binary"
"github.com/multiformats/go-multicodec"
)
const (
SignAlgorithmEdDSA = SignAlgorithm(multicodec.Ed25519Pub)
SignAlgorithmEd25519 = SignAlgorithm(multicodec.Ed25519Pub)
SignAlgorithmEd448 = SignAlgorithm(multicodec.Ed448Pub)
)
type EdDSACurve uint64
const (
CurveEd25519 = EdDSACurve(multicodec.Ed25519Pub)
CurveEd448 = EdDSACurve(multicodec.Ed448Pub)
)
var _ Varsig = (*EdDSAVarsig)(nil)
type EdDSAVarsig struct {
varsig[EdDSAVarsig]
curve EdDSACurve
hashAlg HashAlgorithm
}
func NewEdDSAVarsig(curve EdDSACurve, hashAlgorithm HashAlgorithm, payloadEncoding PayloadEncoding, opts ...Option) (*EdDSAVarsig, error) {
options := newOptions(opts...)
var (
vers = Version1
signAlg = SignAlgorithmEdDSA
sig = []byte{}
)
if options.ForceVersion0() {
vers = Version0
signAlg = SignAlgorithm(curve)
sig = options.Signature()
}
v := &EdDSAVarsig{
varsig: varsig[EdDSAVarsig]{
vers: vers,
signAlg: signAlg,
payEnc: payloadEncoding,
sig: sig,
},
curve: curve,
hashAlg: hashAlgorithm,
}
return v.validateSig(v, ed25519.PrivateKeySize)
}
func (v *EdDSAVarsig) Curve() EdDSACurve {
return v.curve
}
func (v *EdDSAVarsig) HashAlgorithm() HashAlgorithm {
return v.hashAlg
}
func (v EdDSAVarsig) Encode() []byte {
buf := v.encode()
if v.vers != Version0 {
buf = binary.AppendUvarint(buf, uint64(v.curve))
}
buf = binary.AppendUvarint(buf, uint64(v.hashAlg))
buf = binary.AppendUvarint(buf, uint64(v.payEnc))
buf = append(buf, v.Signature()...)
return buf
}
func decodeEd25519(r *bytes.Reader, vers Version, signAlg SignAlgorithm) (Varsig, error) {
curve := uint64(signAlg)
if vers != Version0 {
u, err := binary.ReadUvarint(r)
if err != nil {
return nil, err // TODO: wrap error?
}
curve = u
}
hashAlg, err := binary.ReadUvarint(r)
if err != nil {
return nil, err // TODO: wrap error?
}
v := &EdDSAVarsig{
varsig: varsig[EdDSAVarsig]{
vers: vers,
signAlg: signAlg,
},
curve: EdDSACurve(curve),
hashAlg: HashAlgorithm(hashAlg),
}
return v.decodePayEncAndSig(r, v, ed25519.PrivateKeySize)
}
// TODO: remove this when parseEd25519 is added to the DefaultRegistry.
func Junk() {
_, _ = decodeEd25519(nil, 0, 0)
}

58
eddsa_test.go Normal file
View File

@@ -0,0 +1,58 @@
package varsig_test
import (
"encoding/hex"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/selesy/go-varsig"
)
func TestDecodeEd25519(t *testing.T) {
t.Parallel()
t.Run("passes - section 3 example - v0", func(t *testing.T) {
// Original: 34ed01 1371ae3784f03f9ee1163382fa6efa73b0c31ecf58c899c836709303ba4621d1e6df20e09aaa568914290b7ea124f5b38e70b9b69c7de0d216880eac885edd41c302
// Corrected: 34ed011371ae3784f03f9ee1163382fa6efa73b0c31ecf58c899c836709303ba4621d1e6df20e09aaa568914290b7ea124f5b38e70b9b69c7de0d216880eac885edd41c302")
hdr, err := hex.DecodeString("34ed011371")
require.NoError(t, err)
sig, err := hex.DecodeString("ae3784f03f9ee1163382fa6efa73b0c31ecf58c899c836709303ba4621d1e6df20e09aaa568914290b7ea124f5b38e70b9b69c7de0d216880eac885edd41c302")
require.NoError(t, err)
require.Len(t, sig, 64)
t.Run("Decode", func(t *testing.T) {
t.Parallel()
v, err := varsig.Decode(append(hdr, sig...))
require.NoError(t, err)
require.NotNil(t, v)
assert.Equal(t, varsig.Version0, v.Version())
assert.Equal(t, varsig.SignAlgorithmEd25519, v.SignatureAlgorithm())
assert.Equal(t, varsig.PayloadEncodingDAGCBOR, v.PayloadEncoding())
assert.Len(t, v.Signature(), 64)
impl, ok := v.(*varsig.EdDSAVarsig)
require.True(t, ok)
assert.Equal(t, varsig.CurveEd25519, impl.Curve())
assert.Equal(t, varsig.HashAlgorithmSHA512, impl.HashAlgorithm())
})
t.Run("Encode", func(t *testing.T) {
t.Parallel()
v, err := varsig.NewEdDSAVarsig(
varsig.CurveEd25519,
varsig.HashAlgorithmSHA512,
varsig.PayloadEncodingDAGCBOR,
varsig.WithForceVersion0(sig),
)
require.NoError(t, err)
require.NotNil(t, v)
assert.Equal(t, append(hdr, sig...), v.Encode())
})
})
}

View File

@@ -26,7 +26,8 @@ type Registry map[SignAlgorithm]DecodeFunc
func DefaultRegistry() Registry {
return map[SignAlgorithm]DecodeFunc{
SignAlgorithmRSA: decodeRSA,
SignAlgorithmEd25519: notYetImplementedVarsigDecoder,
SignAlgorithmEdDSA: decodeEd25519,
SignAlgorithmEd448: decodeEd25519,
SignAlgorithmECDSAP256: notYetImplementedVarsigDecoder,
SignAlgorithmECDSASecp256k1: notYetImplementedVarsigDecoder,
SignAlgorithmECDSAP521: notYetImplementedVarsigDecoder,

View File

@@ -1,7 +1,6 @@
package varsig_test
import (
"encoding/base64"
"encoding/hex"
"errors"
"io"
@@ -173,7 +172,7 @@ func mustVarsig[T varsig.Varsig](t *testing.T) func(*T, error) *T {
func roundTrip[T varsig.Varsig](t *testing.T, in T, expEncHex string) T {
data := in.Encode()
assert.Equal(t, expEncHex, base64.RawStdEncoding.EncodeToString(data))
assert.Equal(t, expEncHex, hex.EncodeToString(data))
out, err := varsig.Decode(in.Encode())
if err != nil && (out.Version() != varsig.Version0 || !errors.Is(err, varsig.ErrMissingSignature)) {