diff --git a/common.go b/common.go index f497347..7ea06a0 100644 --- a/common.go +++ b/common.go @@ -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]. // diff --git a/common_test.go b/common_test.go index 0d04a2a..5d58639 100644 --- a/common_test.go +++ b/common_test.go @@ -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() diff --git a/constant.go b/constant.go index f020924..53b0c1d 100644 --- a/constant.go +++ b/constant.go @@ -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 diff --git a/eddsa.go b/eddsa.go new file mode 100644 index 0000000..c58ae94 --- /dev/null +++ b/eddsa.go @@ -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) +} diff --git a/eddsa_test.go b/eddsa_test.go new file mode 100644 index 0000000..1d821ae --- /dev/null +++ b/eddsa_test.go @@ -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()) + }) + }) +} diff --git a/registry.go b/registry.go index 8adb227..2f61d92 100644 --- a/registry.go +++ b/registry.go @@ -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, diff --git a/varsig_test.go b/varsig_test.go index 5d05ff7..3acad93 100644 --- a/varsig_test.go +++ b/varsig_test.go @@ -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)) {