feat(rsa): adds RSA varsig implementation and "common" algorithms

This commit is contained in:
Steve Moyer
2025-07-07 11:25:58 -04:00
parent f9f39d363f
commit 8b710b7e23
5 changed files with 282 additions and 11 deletions

25
common.go Normal file
View File

@@ -0,0 +1,25 @@
package varsig
// RS256 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 RS256(keyLength uint64, payloadEncoding PayloadEncoding, opts ...Option) (*RSAVarsig, error) {
return NewRSAVarsig(HashAlgorithmSHA256, keyLength, payloadEncoding, opts...)
}
// RS384 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 RS384(keyLength uint64, payloadEncoding PayloadEncoding, opts ...Option) (*RSAVarsig, error) {
return NewRSAVarsig(HashAlgorithmSHA384, keyLength, payloadEncoding, opts...)
}
// RS512 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 RS512(keyLength uint64, payloadEncoding PayloadEncoding, opts ...Option) (*RSAVarsig, error) {
return NewRSAVarsig(HashAlgorithmSHA512, keyLength, payloadEncoding, opts...)
}

40
common_test.go Normal file
View File

@@ -0,0 +1,40 @@
package varsig_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/selesy/go-varsig"
)
func TestRS256(t *testing.T) {
t.Parallel()
in := mustVarsig[varsig.RSAVarsig](t)(varsig.RS256(0x100, varsig.PayloadEncodingDAGCBOR))
out := roundTrip(t, in, "NAGFJBKAAnE")
assertRSAEqual(t, in, out)
}
func TestRS384(t *testing.T) {
t.Parallel()
in := mustVarsig[varsig.RSAVarsig](t)(varsig.RS384(0x100, varsig.PayloadEncodingDAGCBOR))
out := roundTrip(t, in, "NAGFJCCAAnE")
assertRSAEqual(t, in, out)
}
func TestRS512(t *testing.T) {
t.Parallel()
in := mustVarsig[varsig.RSAVarsig](t)(varsig.RS512(0x100, varsig.PayloadEncodingDAGCBOR))
out := roundTrip(t, in, "NAGFJBOAAnE")
assertRSAEqual(t, in, out)
}
func assertRSAEqual(t *testing.T, in, out *varsig.RSAVarsig) {
t.Helper()
assert.Equal(t, in.HashAlgorithm(), out.HashAlgorithm())
assert.Equal(t, in.KeyLength(), out.KeyLength())
}

94
rsa.go Normal file
View File

@@ -0,0 +1,94 @@
package varsig
import (
"bytes"
"encoding/binary"
"github.com/multiformats/go-multicodec"
)
const SignAlgorithmRSA = SignAlgorithm(multicodec.RsaPub)
var _ Varsig = (*RSAVarsig)(nil)
// RSAVarsig is a varsig that encodes the parameters required to describe
// and RSA signature.
type RSAVarsig struct {
varsig[RSAVarsig]
hashAlg HashAlgorithm
sigLen uint64
}
// NewRSAVarsig creates and validates an RSA varsig with the provided
// parameters.
func NewRSAVarsig(hashAlgorithm HashAlgorithm, keyLength uint64, payloadEncoding PayloadEncoding, opts ...Option) (*RSAVarsig, error) {
options := newOptions(opts...)
var (
vers = Version1
sig = []byte{}
)
if options.ForceVersion0() {
vers = Version0
sig = options.Signature()
}
v := &RSAVarsig{
varsig: varsig[RSAVarsig]{
vers: vers,
signAlg: SignAlgorithmRSA,
payEnc: payloadEncoding,
sig: sig,
},
hashAlg: hashAlgorithm,
sigLen: keyLength,
}
return v.validateSig(v, v.sigLen)
}
// Encode returns the encoded byte formation of the RSAVarsig.
func (v RSAVarsig) Encode() []byte {
buf := v.encode()
buf = binary.AppendUvarint(buf, uint64(v.hashAlg))
buf = binary.AppendUvarint(buf, v.sigLen)
buf = binary.AppendUvarint(buf, uint64(v.payEnc))
buf = append(buf, v.Signature()...)
return buf
}
// HashAlgorithm returns the hash algorithm used to has the payload content.
func (v *RSAVarsig) HashAlgorithm() HashAlgorithm {
return v.hashAlg
}
// KeyLength returns the length of the RSA key used to sign the payload
// content.
func (v *RSAVarsig) KeyLength() uint64 {
return v.sigLen
}
func decodeRSA(r *bytes.Reader, vers Version, signAlg SignAlgorithm) (Varsig, error) {
hashAlg, err := DecodeHashAlgorithm(r)
if err != nil {
return nil, err
}
sigLen, err := binary.ReadUvarint(r)
if err != nil {
return nil, err
}
vs := &RSAVarsig{
varsig: varsig[RSAVarsig]{
vers: vers,
signAlg: signAlg,
},
hashAlg: HashAlgorithm(hashAlg),
sigLen: sigLen,
}
return vs.decodePayEncAndSig(r, vs, sigLen)
}

96
rsa_test.go Normal file
View File

@@ -0,0 +1,96 @@
package varsig_test
import (
"encoding/base64"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/selesy/go-varsig"
)
func TestRSAVarsig(t *testing.T) {
t.Parallel()
const keyLen = 0x100
// This test uses the same RSA configuration as below but for varsig
// >= v1
example, err := base64.RawStdEncoding.DecodeString("NAGFJBKAAnE")
require.NoError(t, err)
t.Run("Decode", func(t *testing.T) {
t.Parallel()
vs, err := varsig.Decode(example)
require.NoError(t, err)
rsaVs, ok := vs.(*varsig.RSAVarsig)
require.True(t, ok)
assert.Equal(t, varsig.Version1, rsaVs.Version())
assert.Equal(t, varsig.SignAlgorithmRSA, rsaVs.SignatureAlgorithm())
assert.Equal(t, varsig.HashAlgorithmSHA256, rsaVs.HashAlgorithm())
assert.Equal(t, varsig.PayloadEncodingDAGCBOR, rsaVs.PayloadEncoding())
assert.Equal(t, uint64(keyLen), rsaVs.KeyLength())
assert.Len(t, rsaVs.Signature(), 0)
})
t.Run("Encode", func(t *testing.T) {
t.Parallel()
rsaVarsig, err := varsig.NewRSAVarsig(
varsig.HashAlgorithmSHA256,
keyLen,
varsig.PayloadEncodingDAGCBOR,
)
require.NoError(t, err)
assert.Equal(t, example, rsaVarsig.Encode())
t.Log(base64.RawStdEncoding.EncodeToString(rsaVarsig.Encode()))
})
}
func TestUCANExample(t *testing.T) {
t.Parallel()
const keyLen = 0x100
// This test is the value shown in the UCAN v1.0.0 example, which is
// an RSA varsig < v1 encoded as RS256 with a key length of 0x100
// bytes and DAG-CBOR payload encoding.
example, err := base64.RawStdEncoding.DecodeString("NIUkEoACcQ")
require.NoError(t, err)
t.Run("Decode", func(t *testing.T) {
t.Parallel()
vs, err := varsig.Decode(example)
require.ErrorIs(t, err, varsig.ErrMissingSignature)
rsaVs, ok := vs.(*varsig.RSAVarsig)
require.True(t, ok)
assert.Equal(t, varsig.Version0, rsaVs.Version())
assert.Equal(t, varsig.SignAlgorithmRSA, rsaVs.SignatureAlgorithm())
assert.Equal(t, varsig.HashAlgorithmSHA256, rsaVs.HashAlgorithm())
assert.Equal(t, varsig.PayloadEncodingDAGCBOR, rsaVs.PayloadEncoding())
assert.Equal(t, uint64(keyLen), rsaVs.KeyLength())
assert.Len(t, rsaVs.Signature(), 0)
})
t.Run("Encode", func(t *testing.T) {
t.Parallel()
rsaVarsig, err := varsig.NewRSAVarsig(
varsig.HashAlgorithmSHA256,
keyLen,
varsig.PayloadEncodingDAGCBOR,
varsig.WithForceVersion0([]byte{}),
)
require.ErrorIs(t, err, varsig.ErrMissingSignature)
assert.Equal(t, example, rsaVarsig.Encode())
})
}

View File

@@ -1,7 +1,9 @@
package varsig_test
import (
"encoding/base64"
"encoding/hex"
"errors"
"io"
"testing"
@@ -157,17 +159,31 @@ func TestDecode(t *testing.T) {
})
}
// func TestReadUvarint(t *testing.T) {
// t.Parallel()
func mustVarsig[T varsig.Varsig](t *testing.T) func(*T, error) *T {
t.Helper()
// var r io.ByteReader = &bytes.Reader{}
return func(v *T, err error) *T {
if err != nil && ((*v).Version() != varsig.Version0 || !errors.Is(err, varsig.ErrMissingSignature)) {
t.Error(err)
}
// u, err := binary.ReadUvarint(r)
// require.ErrorIs(t, err, io.EOF)
// assert.Equal(t, uint64(0), u)
return v
}
}
// var buf []byte
// buf = binary.AppendUvarint(buf, 0x100)
// t.Log("0x100 varint:", hex.EncodeToString(buf))
// t.Fail()
// }
func roundTrip[T varsig.Varsig](t *testing.T, in T, expEncHex string) T {
data := in.Encode()
assert.Equal(t, expEncHex, base64.RawStdEncoding.EncodeToString(data))
out, err := varsig.Decode(in.Encode())
if err != nil && (out.Version() != varsig.Version0 || !errors.Is(err, varsig.ErrMissingSignature)) {
t.Fail()
}
assert.Equal(t, in.Version(), out.Version())
assert.Equal(t, in.SignatureAlgorithm(), out.SignatureAlgorithm())
assert.Equal(t, in.PayloadEncoding(), out.PayloadEncoding())
assert.Equal(t, in.Signature(), out.Signature())
return out.(T)
}