Skip to content

Commit d12291b

Browse files
authored
Merge pull request #2 from libsv/jsonenvelope
2 parents b0b0c71 + 2afd08f commit d12291b

File tree

2 files changed

+271
-0
lines changed

2 files changed

+271
-0
lines changed

envelope/jsonenvelope.go

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Package envelope supports the JSON Envelope Spec
2+
// It can be found here https://github.com/bitcoin-sv-specs/brfc-misc/tree/master/jsonenvelope
3+
//
4+
// Standard for serialising a JSON document in order to have consistency when
5+
// ECDSA signing the document.
6+
// Any changes to a document being signed and verified, however minor they may be,
7+
// will cause the signature verification to fail since the document will be converted into a string
8+
// before being (hashed and then) signed. With JSON documents, the format permits changes to be made
9+
// without compromising the validity of the format (eg. extra spaces, carriage returns, etc.).
10+
package envelope
11+
12+
import (
13+
"crypto/sha256"
14+
"encoding/base64"
15+
"encoding/hex"
16+
"encoding/json"
17+
"fmt"
18+
"strings"
19+
20+
"github.com/libsv/go-bk/bec"
21+
)
22+
23+
// JSONEnvelope defines an envelop contain option signature and public key to use to
24+
// verify the signature and payload.
25+
// If no Signature or PublicKey are provided we do not validate the payload.
26+
// The payload is usually an escaped JSON string.
27+
type JSONEnvelope struct {
28+
Payload string `json:"payload"`
29+
Signature *string `json:"signature"`
30+
PublicKey *string `json:"publicKey"`
31+
Encoding string `json:"encoding"`
32+
MimeType string `json:"mimetype"`
33+
}
34+
35+
// IsValid will check that a JSONEnvelope is valid by using the PublicKey and Signature to
36+
// validate the payload. If the payload differs from the signature, false is returned.
37+
// If the signature or public key are invalid an error is returned.
38+
func (j *JSONEnvelope) IsValid() (bool, error) {
39+
// if no sig or pk we don't try to verify
40+
if j.Signature == nil && j.PublicKey == nil {
41+
return true, nil
42+
}
43+
// parse and validate public key
44+
pub, err := hex.DecodeString(*j.PublicKey)
45+
if err != nil {
46+
return false, fmt.Errorf("failed to decode json envelope publicKey %w", err)
47+
}
48+
verifyPubKey, err := bec.ParsePubKey(pub, bec.S256())
49+
if err != nil {
50+
return false, fmt.Errorf("failed to parse json envelope publicKey %w", err)
51+
}
52+
53+
// parse and validate signature
54+
signature, err := hex.DecodeString(*j.Signature)
55+
if err != nil {
56+
return false, fmt.Errorf("failed to decode json envelope signature %w", err)
57+
}
58+
sig, err := bec.ParseSignature(signature, bec.S256())
59+
if err != nil {
60+
return false, fmt.Errorf("failed to parse json envelope signature %w", err)
61+
}
62+
var verifyHash [32]byte
63+
switch j.MimeType {
64+
case "application/json":
65+
verifyHash = sha256.Sum256([]byte(strings.Replace(j.Payload, `\`, "", -1)))
66+
case "base64":
67+
bb, err := base64.StdEncoding.DecodeString(j.Payload)
68+
if err != nil {
69+
return false, fmt.Errorf("failed to parse base64 payload %w", err)
70+
}
71+
verifyHash = sha256.Sum256(bb)
72+
default:
73+
verifyHash = sha256.Sum256([]byte(j.Payload))
74+
}
75+
return sig.Verify(verifyHash[:], verifyPubKey), nil
76+
}
77+
78+
// NewJSONEnvelope will create and return a new JSONEnvelope with the provided
79+
// payload serialised and signed.
80+
func NewJSONEnvelope(payload interface{}) (*JSONEnvelope, error) {
81+
privateKey, err := bec.NewPrivateKey(bec.S256())
82+
if err != nil {
83+
return nil, fmt.Errorf("failed to generate new private key %w", err)
84+
}
85+
publicKey := privateKey.PubKey()
86+
87+
pl, err := json.Marshal(payload)
88+
if err != nil {
89+
return nil, fmt.Errorf("failed to encode payload %w", err)
90+
}
91+
hash := sha256.Sum256(pl)
92+
signature, err := privateKey.Sign(hash[:])
93+
if err != nil {
94+
return nil, fmt.Errorf("failed to create signature %w", err)
95+
}
96+
signatureHex := hex.EncodeToString(signature.Serialise())
97+
publicKeyHex := hex.EncodeToString(publicKey.SerialiseCompressed())
98+
99+
return &JSONEnvelope{
100+
Payload: string(pl),
101+
Signature: &signatureHex,
102+
PublicKey: &publicKeyHex,
103+
Encoding: "UTF-8",
104+
MimeType: "application/json",
105+
}, nil
106+
}

envelope/jsonenvelope_test.go

+165
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
package envelope
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
func TestJSONEnvelope_IsValid(t *testing.T) {
12+
t.Parallel()
13+
tests := map[string]struct {
14+
env *JSONEnvelope
15+
expValid bool
16+
err error
17+
}{
18+
"valid envelope should return true": {
19+
env: &JSONEnvelope{
20+
Payload: `{\"name\":\"simon\",\"colour\":\"blue\"}`,
21+
Signature: strToPtr("30450221008209b19ffe2182d859ce36fdeff5ded4b3f70ad77e0e8715238a539db97c1282022043b1a5b260271b7c833ca7c37d1490f21b7bd029dbb8970570c7fdc3df5c93ab"),
22+
PublicKey: strToPtr("02b01c0c23ff7ff35f774e6d3b3491a123afb6c98965054e024d2320f7dbd25d8a"),
23+
Encoding: "UTF-8",
24+
MimeType: "application/json",
25+
},
26+
expValid: true,
27+
err: nil,
28+
}, "JSON envelope should be valid when checked with correct sig and pk": {
29+
env: &JSONEnvelope{
30+
Payload: `{\"Test\":\"abc123\",\"Name\":\"4567890\",\"Thing\":\"%$oddchars££$-\"}`,
31+
Signature: strToPtr("3045022100b2b3000353b1acaf6e0190a44fc26b0b43830e5aa8d1232813c928d003697c010220294796e63da19d238b29f9cb17e2f31f728ef77a41bfd0f5e355f99f347ff4bf"),
32+
PublicKey: strToPtr("0394890eeb9888e68cb953d56c598ab0aaa6789e20522cc8b937353694799d7ab1"),
33+
Encoding: "UTF-8",
34+
MimeType: "application/json",
35+
},
36+
expValid: true,
37+
err: nil,
38+
}, "JSON envelope invalid signature should fail": {
39+
env: &JSONEnvelope{
40+
Payload: `{\"Test\":\"abc123\",\"Name\":\"4567890\",\"Thing\":\"%$oddchars££$-\"}`,
41+
Signature: strToPtr("3045022100b2b3000353b1acaf6e0190a44fc26b0b43830e5aaaaaa32813c928d003697c010220294796e63da19d238b29f9cb17e2f31f728ef77a41bfd0f5e355f99f347ff4bf"),
42+
PublicKey: strToPtr("0394890eeb9888e68cb953d56c598ab0aaa6789e20522cc8b937353694799d7ab1"),
43+
Encoding: "UTF-8",
44+
MimeType: "application/json",
45+
},
46+
expValid: false,
47+
err: nil,
48+
}, "JSON envelope invalid public key should fail": {
49+
env: &JSONEnvelope{
50+
Payload: `{\"Test\":\"abc123\",\"Name\":\"4567890\",\"Thing\":\"%$oddchars££$-\"}`,
51+
Signature: strToPtr("3045022100b2b3000353b1acaf6e0190a44fc26b0b43830e5aa8d1232813c928d003697c010220294796e63da19d238b29f9cb17e2f31f728ef77a41bfd0f5e355f99f347ff4bf"),
52+
PublicKey: strToPtr("0394890eeb9888e68cb953d56c598ab0aaa6789e20522cc8b937353694799d9999"),
53+
Encoding: "UTF-8",
54+
MimeType: "application/json",
55+
},
56+
expValid: false,
57+
err: nil,
58+
}, "JSON envelope signature not hex should return error": {
59+
env: &JSONEnvelope{
60+
Payload: `{\"Test\":\"abc123\",\"Name\":\"4567890\",\"Thing\":\"%$oddchars££$-\"}`,
61+
Signature: strToPtr("3045022100b2b3000353b1aZZf6e0190a44fc26b0b43830e5aa8d1232813c928d003697c010220294796e63da19d238b29f9cb17e2f31f728ef77a41bfd0f5e355f99f347ff4bf"),
62+
PublicKey: strToPtr("0394890eeb9888e68cb953d56c598ab0aaa6789e20522cc8b937353694799d9999"),
63+
Encoding: "UTF-8",
64+
MimeType: "application/json",
65+
},
66+
expValid: false,
67+
err: errors.New("failed to decode json envelope signature encoding/hex: invalid byte: U+005A 'Z'"),
68+
}, "JSON envelope public key not hex should return error": {
69+
env: &JSONEnvelope{
70+
Payload: `{\"Test\":\"abc123\",\"Name\":\"4567890\",\"Thing\":\"%$oddchars££$-\"}`,
71+
Signature: strToPtr("3045022100b2b3000353b1acaf6e0190a44fc26b0b43830e5aa8d1232813c928d003697c010220294796e63da19d238b29f9cb17e2f31f728ef77a41bfd0f5e355f99f347ff4bf"),
72+
PublicKey: strToPtr("0394890eeb9888e68cb953d56c598ab0aaa6789e20522cc8b937353694799d99ZZ"),
73+
Encoding: "UTF-8",
74+
MimeType: "application/json",
75+
},
76+
expValid: false,
77+
err: errors.New("failed to decode json envelope publicKey encoding/hex: invalid byte: U+005A 'Z'"),
78+
}, "JSON envelope signature invalid signature prefix should fail": {
79+
env: &JSONEnvelope{
80+
Payload: `{\"Test\":\"abc123\",\"Name\":\"4567890\",\"Thing\":\"%$oddchars££$-\"}`,
81+
Signature: strToPtr("9945022100b2b3000353b1acaf6e0190a44fc26b0b43830e5aa8d1232813c928d003697c010220294796e63da19d238b29f9cb17e2f31f728ef77a41bfd0f5e355f99f347ff4bf"),
82+
PublicKey: strToPtr("0394890eeb9888e68cb953d56c598ab0aaa6789e20522cc8b937353694799d7ab1"),
83+
Encoding: "UTF-8",
84+
MimeType: "application/json",
85+
},
86+
expValid: false,
87+
err: errors.New("failed to parse json envelope signature malformed signature: no header magic"),
88+
}, "JSON envelope public key invalid prefix should fail": {
89+
env: &JSONEnvelope{
90+
Payload: `{\"Test\":\"abc123\",\"Name\":\"4567890\",\"Thing\":\"%$oddchars££$-\"}`,
91+
Signature: strToPtr("3045022100b2b3000353b1acaf6e0190a44fc26b0b43830e5aa8d1232813c928d003697c010220294796e63da19d238b29f9cb17e2f31f728ef77a41bfd0f5e355f99f347ff4bf"),
92+
PublicKey: strToPtr("9994890eeb9888e68cb953d56c598ab0aaa6789e20522cc8b937353694799d7ab1"),
93+
Encoding: "UTF-8",
94+
MimeType: "application/json",
95+
},
96+
expValid: false,
97+
err: errors.New("failed to parse json envelope publicKey invalid magic in compressed pubkey string: 153"),
98+
}, "JSON envelope no sigs should not validate and return valid": {
99+
env: &JSONEnvelope{
100+
Payload: `{\"Test\":\"abc123\",\"Name\":\"4567890\",\"Thing\":\"%$oddchars££$-\"}`,
101+
Encoding: "UTF-8",
102+
MimeType: "application/json",
103+
},
104+
expValid: true,
105+
err: nil,
106+
},
107+
}
108+
for name, test := range tests {
109+
t.Run(name, func(t *testing.T) {
110+
isValid, err := test.env.IsValid()
111+
if test.err != nil {
112+
assert.Error(t, err)
113+
assert.False(t, isValid)
114+
assert.EqualError(t, err, test.err.Error())
115+
return
116+
}
117+
assert.NoError(t, err)
118+
assert.Equal(t, test.expValid, isValid)
119+
})
120+
}
121+
}
122+
123+
func TestNewJSONEnvelope_signAndCheckValid(t *testing.T) {
124+
t.Parallel()
125+
tests := map[string]struct {
126+
payload interface{}
127+
err error
128+
}{
129+
"successful run should return no errors": {
130+
payload: struct {
131+
Test string
132+
Name string
133+
Thing string
134+
}{
135+
Test: "abc123",
136+
Name: "4567890",
137+
Thing: "%$oddchars££$-",
138+
},
139+
err: nil,
140+
},
141+
}
142+
for name, test := range tests {
143+
t.Run(name, func(t *testing.T) {
144+
// create env and check valid
145+
env, err := NewJSONEnvelope(test.payload)
146+
assert.NoError(t, err)
147+
val, valErr := env.IsValid()
148+
assert.NoError(t, valErr)
149+
assert.True(t, val)
150+
// convert to json then decode and retry validation
151+
bb, err := json.Marshal(env)
152+
assert.NoError(t, err)
153+
var unmarshalledEnv *JSONEnvelope
154+
assert.NoError(t, json.Unmarshal(bb, &unmarshalledEnv))
155+
// is valid?
156+
val, valErr = unmarshalledEnv.IsValid()
157+
assert.NoError(t, valErr)
158+
assert.True(t, val)
159+
})
160+
}
161+
}
162+
163+
func strToPtr(s string) *string {
164+
return &s
165+
}

0 commit comments

Comments
 (0)