Skip to content

Commit

Permalink
feat: add RSA key support (#14)
Browse files Browse the repository at this point in the history
This PR adds RSA key support. RSA keys are used on the browser since
browsers do not yet support ed25519 keys (or at least support is patchy
and not without bugs).

We'll likely only need RSA key _verification_ but for completeness this
PR includes signing functionality also.
  • Loading branch information
Alan Shaw authored Aug 16, 2024
1 parent d7e26c4 commit 6cf686f
Show file tree
Hide file tree
Showing 8 changed files with 312 additions and 1 deletion.
3 changes: 2 additions & 1 deletion did/did.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const KeyPrefix = "did:key:"

const DIDCore = 0x0d1d
const Ed25519 = 0xed
const RSA = 0x1205

var MethodOffset = varint.UvarintSize(uint64(DIDCore))

Expand Down Expand Up @@ -54,7 +55,7 @@ func Decode(bytes []byte) (DID, error) {
if err != nil {
return Undef, err
}
if code == Ed25519 {
if code == Ed25519 || code == RSA {
return DID{str: string(bytes), key: true}, nil
} else if code == DIDCore {
return DID{str: string(bytes)}, nil
Expand Down
35 changes: 35 additions & 0 deletions principal/multiformat/multiformat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package multiformat

import (
"bytes"
"fmt"

"github.com/multiformats/go-varint"
)

func TagWith(code uint64, bytes []byte) []byte {
offset := varint.UvarintSize(code)
tagged := make([]byte, len(bytes)+offset)
varint.PutUvarint(tagged, code)
copy(tagged[offset:], bytes)
return tagged
}

func UntagWith(code uint64, source []byte, offset int) ([]byte, error) {
b := source
if offset != 0 {
b = source[offset:]
}

tag, err := varint.ReadUvarint(bytes.NewReader(b))
if err != nil {
return nil, err
}

if tag != code {
return nil, fmt.Errorf("expected multiformat with 0x%x tag instead got 0x%x", code, tag)
}

size := varint.UvarintSize(code)
return b[size:], nil
}
25 changes: 25 additions & 0 deletions principal/multiformat/multiformat_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package multiformat

import (
"testing"

"github.com/storacha-network/go-ucanto/testing/helpers"
"github.com/stretchr/testify/require"
)

func TestTag(t *testing.T) {
t.Run("round trip", func(t *testing.T) {
b := []byte{1, 2, 3}
tb := TagWith(1, b)
utb := helpers.Must(UntagWith(1, tb, 0))
require.EqualValues(t, b, utb)
})

t.Run("incorrect tag", func(t *testing.T) {
b := []byte{1, 2, 3}
tb := TagWith(1, b)
_, err := UntagWith(2, tb, 0)
require.Error(t, err)
require.Equal(t, "expected multiformat with 0x2 tag instead got 0x1", err.Error())
})
}
120 changes: 120 additions & 0 deletions principal/rsa/signer/signer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package signer

import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"fmt"

"github.com/multiformats/go-multibase"
"github.com/storacha-network/go-ucanto/did"
"github.com/storacha-network/go-ucanto/principal"
"github.com/storacha-network/go-ucanto/principal/multiformat"
"github.com/storacha-network/go-ucanto/principal/rsa/verifier"
"github.com/storacha-network/go-ucanto/ucan/crypto/signature"
)

const Code = 0x1300
const Name = verifier.Name

const SignatureCode = verifier.SignatureCode
const SignatureAlgorithm = verifier.SignatureAlgorithm

const keySize = 2048

func Generate() (principal.Signer, error) {
priv, err := rsa.GenerateKey(rand.Reader, keySize)
if err != nil {
return nil, fmt.Errorf("generating RSA key: %s", err)
}

// Next we need to encode public key, because `RSAVerifier` uses it to
// for implementing the `DID()` method.
pubbytes := multiformat.TagWith(verifier.Code, x509.MarshalPKCS1PublicKey(&priv.PublicKey))

verif, err := verifier.Decode(pubbytes)
if err != nil {
return nil, fmt.Errorf("decoding public bytes: %s", err)
}

// Export key in Private Key Cryptography Standards (PKCS) format and extract
// the bytes corresponding to the private key, which we tag with RSA private
// key multiformat code. With both binary and actual key representation we
// create a RSASigner view.
prvbytes := multiformat.TagWith(Code, x509.MarshalPKCS1PrivateKey(priv))

return rsasigner{bytes: prvbytes, privKey: priv, verifier: verif}, nil
}

func Parse(str string) (principal.Signer, error) {
_, bytes, err := multibase.Decode(str)
if err != nil {
return nil, fmt.Errorf("decoding multibase string: %s", err)
}
return Decode(bytes)
}

func Format(signer principal.Signer) (string, error) {
return multibase.Encode(multibase.Base64pad, signer.Encode())
}

func Decode(b []byte) (principal.Signer, error) {
utb, err := multiformat.UntagWith(Code, b, 0)
if err != nil {
return nil, err
}

priv, err := x509.ParsePKCS1PrivateKey(utb)
if err != nil {
return nil, fmt.Errorf("parsing private key: %s", err)
}

pubbytes := multiformat.TagWith(verifier.Code, x509.MarshalPKCS1PublicKey(&priv.PublicKey))

verif, err := verifier.Decode(pubbytes)
if err != nil {
return nil, fmt.Errorf("decoding public bytes: %s", err)
}

return rsasigner{bytes: b, privKey: priv, verifier: verif}, nil
}

type rsasigner struct {
bytes []byte
privKey *rsa.PrivateKey
verifier principal.Verifier
}

func (s rsasigner) Code() uint64 {
return Code
}

func (s rsasigner) SignatureCode() uint64 {
return SignatureCode
}

func (s rsasigner) SignatureAlgorithm() string {
return SignatureAlgorithm
}

func (s rsasigner) Verifier() principal.Verifier {
return s.verifier
}

func (s rsasigner) DID() did.DID {
return s.verifier.DID()
}

func (s rsasigner) Encode() []byte {
return s.bytes
}

func (s rsasigner) Sign(msg []byte) signature.SignatureView {
hash := sha256.New()
hash.Write(msg)
digest := hash.Sum(nil)
sig, _ := rsa.SignPKCS1v15(nil, s.privKey, crypto.SHA256, digest)
return signature.NewSignatureView(signature.NewSignature(SignatureCode, sig))
}
42 changes: 42 additions & 0 deletions principal/rsa/signer/signer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package signer

import (
"fmt"
"testing"

"github.com/storacha-network/go-ucanto/testing/helpers"
"github.com/stretchr/testify/require"
)

func TestGenerateEncodeDecode(t *testing.T) {
s0 := helpers.Must(Generate())
fmt.Println(s0.DID().String())

s1 := helpers.Must(Decode(s0.Encode()))
fmt.Println(s1.DID().String())

require.Equal(t, s0.DID().String(), s1.DID().String())
}

func TestGenerateFormatParse(t *testing.T) {
s0 := helpers.Must(Generate())
fmt.Println(s0.DID().String())

str := helpers.Must(Format(s0))
fmt.Println(str)

s1 := helpers.Must(Parse(str))
fmt.Println(s1.DID().String())

require.Equal(t, s0.DID().String(), s1.DID().String())
}

func TestVerify(t *testing.T) {
s0 := helpers.Must(Generate())

msg := []byte("testy")
sig := s0.Sign(msg)

res := s0.Verifier().Verify(msg, sig)
require.Equal(t, true, res)
}
73 changes: 73 additions & 0 deletions principal/rsa/verifier/verifier.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package verifier

import (
"crypto"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"fmt"

"github.com/storacha-network/go-ucanto/did"
"github.com/storacha-network/go-ucanto/principal"
"github.com/storacha-network/go-ucanto/principal/multiformat"
"github.com/storacha-network/go-ucanto/ucan/crypto/signature"
)

const Code = 0x1205
const Name = "RSA"

const SignatureCode = signature.RS256
const SignatureAlgorithm = "RS256"

func Parse(str string) (principal.Verifier, error) {
did, err := did.Parse(str)
if err != nil {
return nil, fmt.Errorf("parsing DID: %s", err)
}
return Decode(did.Bytes())
}

func Decode(b []byte) (principal.Verifier, error) {
utb, err := multiformat.UntagWith(Code, b, 0)
if err != nil {
return nil, err
}

pub, err := x509.ParsePKCS1PublicKey(utb)
if err != nil {
return nil, fmt.Errorf("parsing public key: %s", err)
}

return rsaverifier{bytes: b, pubKey: pub}, nil
}

type rsaverifier struct {
bytes []byte
pubKey *rsa.PublicKey
}

func (v rsaverifier) Code() uint64 {
return Code
}

func (v rsaverifier) Verify(msg []byte, sig signature.Signature) bool {
if sig.Code() != signature.RS256 {
return false
}

hash := sha256.New()
hash.Write(msg)
digest := hash.Sum(nil)

err := rsa.VerifyPKCS1v15(v.pubKey, crypto.SHA256, digest, sig.Raw())
return err == nil
}

func (v rsaverifier) DID() did.DID {
id, _ := did.Decode(v.bytes)
return id
}

func (v rsaverifier) Encode() []byte {
return v.bytes
}
14 changes: 14 additions & 0 deletions principal/rsa/verifier/verifier_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package verifier

import "testing"

func TestParse(t *testing.T) {
str := "did:key:z4MXj1wBzi9jUstyNgxg2TNN9cNWH8BzcMa5iZ9DAUiLutvQPgBu3zE385tUsbd4oVfHwFb2afSmHpKG4x8JVzESNPSCri4fgztu9FdV3FArz2gByZ9E6zKk3snQKuRjfMJTf29b4BLwGu9j7BtJnhR7bWDWvNqo2YSAwEP8UXyV1W7Meiu96v4esmv2sBLug4vkMFDKXx8bdYZNJYGQQHYrqGXRStZZYGK9xiddMutKeopr1q9UKrczbFhWbdsHW587y4p4uVfwj8evGak6Gx7ADHyQPJc5jWmmUXTzZHJwTqEXDekFkQwkfR9ycxWKnSmPcN9mnimKmuD4LMMzZbodM8Ukgo7XGW8HbiUf3utjt6carBD4c"
v, err := Parse(str)
if err != nil {
t.Fatalf("parsing DID: %s", err)
}
if v.DID().String() != str {
t.Fatalf("expected %s to equal %s", v.DID().String(), str)
}
}
1 change: 1 addition & 0 deletions ucan/crypto/signature/signature.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
)

const EdDSA = 0xd0ed
const RS256 = 0xd01205

type Signature interface {
Code() uint64
Expand Down

0 comments on commit 6cf686f

Please sign in to comment.