Skip to content

Commit 8de5106

Browse files
committed
feat(#2): implementation fixes, readme updates
1 parent f3c3505 commit 8de5106

File tree

6 files changed

+231
-36
lines changed

6 files changed

+231
-36
lines changed

README.md

+29-1
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,33 @@ func main() {
6262
// generate a new ECDSA key
6363
key, err := webcrypto.Subtle().GenerateKey(
6464
&ecdsa.Algorithm{
65-
NamedCurve: "P-256"
65+
NamedCurve: "P-256",
6666
}, true, webcrypto.Sign, webcrypto.Verify)
6767
if err != nil {
6868
panic(err)
6969
}
70+
71+
ckp := key.(webcrypto.CryptoKeyPair)
72+
73+
// sign some data with the private key
74+
sig, err := webcrypto.Subtle().Sign(&ecdsa.Algorithm{
75+
Hash: "SHA-256",
76+
}, ckp.PrivateKey(), []byte("test"))
77+
if err != nil {
78+
panic(err)
79+
}
80+
81+
// verify the signature with the public key
82+
ok, err := webcrypto.Subtle().Verify(&ecdsa.Algorithm{
83+
Hash: "SHA-256",
84+
}, ckp.PublicKey(), sig, []byte("test"))
85+
if err != nil {
86+
panic(err)
87+
}
88+
89+
if !ok {
90+
// didn't verify - do something
91+
}
7092
}
7193
```
7294

@@ -85,6 +107,12 @@ import (
85107
func main() {
86108
// Generate a new key. A *hmac.CryptoKey is returned which implements webcrypto.CryptoKey
87109
key, err := webcrypto.Subtle().GenerateKey(
110+
&Algorithm{
111+
Name: "ECDSA",
112+
Params: ecdsa.KeyGenParams{
113+
114+
}
115+
}
88116
&hmac.Algorithm{
89117
KeyGenParams: &hmac.KeyGenParams{
90118
Hash: "SHA-256",

algorithm.go

+6
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
package webcrypto
1616

17+
import "fmt"
18+
1719
var algorithms = map[string]func() SubtleCrypto{}
1820

1921
// Algorithm implements the Algorithm dictionary type as specified at
@@ -33,6 +35,10 @@ type KeyAlgorithm interface {
3335
// RegisterAlgorithm will register SubtleCrypto implementations referenced by the algorithm
3436
// name provided. When fn gets called, it should return a NEW instance of the implementation.
3537
func RegisterAlgorithm(name string, fn func() SubtleCrypto) {
38+
_, ok := algorithms[name]
39+
if ok {
40+
panic(fmt.Sprintf("%s algorithm already registered", name))
41+
}
3642
algorithms[name] = fn
3743
}
3844

algorithms/ecdsa/ecdsa.go

+25-20
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
package ecdsa
1818

1919
import (
20-
"crypto"
2120
"crypto/ecdsa"
2221
"crypto/elliptic"
2322
"crypto/rand"
@@ -74,8 +73,6 @@ func (c *CryptoKey) Usages() []webcrypto.KeyUsage {
7473
return c.usages
7574
}
7675

77-
// KeyGenParams represents the parameters available for key generation as specified at
78-
// §23.4 https://www.w3.org/TR/WebCryptoAPI/#dfn-EcKeyGenParams
7976
type Algorithm struct {
8077
Hash string
8178
NamedCurve string
@@ -172,6 +169,9 @@ func (s *subtleCrypto) WrapKey(format webcrypto.KeyFormat, key webcrypto.CryptoK
172169
}
173170

174171
func exportKey(format webcrypto.KeyFormat, key webcrypto.CryptoKey) (any, error) {
172+
if !key.Extractable() {
173+
return nil, webcrypto.NewError(webcrypto.ErrInvalidAccessError, "key not extractable")
174+
}
175175
ckp, ok := key.(*CryptoKey)
176176
if !ok {
177177
return nil, webcrypto.NewError(webcrypto.ErrDataError, "key must be *ecdsa.CryptoKey")
@@ -201,15 +201,15 @@ func exportKeyJwk(key *CryptoKey) (*webcrypto.JsonWebKey, error) {
201201
jwk := &webcrypto.JsonWebKey{
202202
Kty: "EC",
203203
Ext: key.ext,
204-
KeyOps: key.usages,
205-
Use: "sig",
206204
Crv: key.alg.namedCurve,
207-
X: util.Encoding().EncodeToString(key.pub.Y.Bytes()),
205+
KeyOps: []webcrypto.KeyUsage{webcrypto.Verify},
206+
X: util.Encoding().EncodeToString(key.pub.X.Bytes()),
208207
Y: util.Encoding().EncodeToString(key.pub.Y.Bytes()),
209208
}
210209

211210
if key.isPrivate {
212211
jwk.D = util.Encoding().EncodeToString(key.priv.D.Bytes())
212+
jwk.KeyOps = []webcrypto.KeyUsage{webcrypto.Sign}
213213
}
214214

215215
return jwk, nil
@@ -361,14 +361,6 @@ func importKeyJwk(keyData *webcrypto.JsonWebKey, algorithm *Algorithm, extractab
361361
return nil, webcrypto.NewError(webcrypto.ErrDataError, "invalid kty")
362362
}
363363

364-
// If usages is non-empty and the "use" field of jwk is present and is
365-
// not a case-sensitive string match to "sig", then throw a DataError.
366-
if len(keyUsages) > 0 {
367-
if keyData.Use != "sig" {
368-
return nil, webcrypto.NewError(webcrypto.ErrDataError, "invalid use")
369-
}
370-
}
371-
372364
// the 'crv' in the jwk must match the named curve in the provided algorithm
373365
if keyData.Crv != algorithm.NamedCurve {
374366
return nil, webcrypto.NewError(webcrypto.ErrDataError, "crv mismatch")
@@ -434,9 +426,13 @@ func importKeyJwk(keyData *webcrypto.JsonWebKey, algorithm *Algorithm, extractab
434426
// If the "key_ops" field of jwk is present, and is invalid according to the requirements
435427
// of JSON Web Key or does not contain all of the specified usages values, then throw
436428
// a DataError.
437-
if len(keyData.KeyOps) > 0 {
438-
if err := util.AreUsagesValid(keyUsages, keyData.KeyOps); err != nil {
439-
return nil, err
429+
if ck.isPrivate {
430+
if len(keyData.KeyOps) != 1 || keyData.KeyOps[0] != webcrypto.Sign {
431+
return nil, webcrypto.NewError(webcrypto.ErrSyntaxError, "invalid key use")
432+
}
433+
} else {
434+
if len(keyData.KeyOps) != 1 || keyData.KeyOps[0] != webcrypto.Verify {
435+
return nil, webcrypto.NewError(webcrypto.ErrSyntaxError, "invalid key use")
440436
}
441437
}
442438

@@ -450,6 +446,9 @@ func importKeyJwk(keyData *webcrypto.JsonWebKey, algorithm *Algorithm, extractab
450446
}
451447

452448
func sign(algorithm webcrypto.Algorithm, key webcrypto.CryptoKey, data []byte) ([]byte, error) {
449+
if key.Type() != webcrypto.Private {
450+
return nil, webcrypto.NewError(webcrypto.ErrInvalidAccessError, "key must be an *ecdsa.CryptoKey private key")
451+
}
453452
// ensure its the correct algorithm
454453
alg, err := getAlgorithm(algorithm)
455454
if err != nil {
@@ -478,15 +477,19 @@ func sign(algorithm webcrypto.Algorithm, key webcrypto.CryptoKey, data []byte) (
478477

479478
digest := hash.Sum(nil)
480479

481-
b, err := pk.priv.Sign(rand.Reader, digest, crypto.SHA256)
480+
// We concat both r and s - https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/sign#ecdsa
481+
r, s, err := ecdsa.Sign(rand.Reader, pk.priv, digest)
482482
if err != nil {
483483
return nil, webcrypto.NewError(webcrypto.ErrOperationError, fmt.Sprintf("failed to sign: %s", err.Error()))
484484
}
485485

486-
return b, nil
486+
return append(r.Bytes(), s.Bytes()...), nil
487487
}
488488

489489
func verify(algorithm webcrypto.Algorithm, key webcrypto.CryptoKey, signature []byte, data []byte) (bool, error) {
490+
if key.Type() != webcrypto.Public {
491+
return false, webcrypto.NewError(webcrypto.ErrInvalidAccessError, "key must be an *ecdsa.CryptoKey public key")
492+
}
490493
// ensure its the correct algorithm
491494
alg, err := getAlgorithm(algorithm)
492495
if err != nil {
@@ -512,7 +515,9 @@ func verify(algorithm webcrypto.Algorithm, key webcrypto.CryptoKey, signature []
512515

513516
digest := hash.Sum(nil)
514517

515-
return ecdsa.VerifyASN1(pk.pub, digest, signature), nil
518+
r := signature[0:32]
519+
s := signature[32:64]
520+
return ecdsa.Verify(pk.pub, digest, big.NewInt(0).SetBytes(r), big.NewInt(0).SetBytes(s)), nil
516521
}
517522

518523
func getAlgorithm(a webcrypto.Algorithm) (*Algorithm, error) {

algorithms/ecdsa/ecdsa_test.go

+144-14
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@
1717
package ecdsa
1818

1919
import (
20+
"encoding/base64"
21+
"encoding/json"
2022
"errors"
2123
"fmt"
24+
"os"
2225
"testing"
2326

2427
"github.com/armortal/webcrypto-go"
@@ -125,47 +128,174 @@ func Test_SignAndVerify(t *testing.T) {
125128
}
126129
ckp := k.(webcrypto.CryptoKeyPair)
127130

131+
signAndVerify(t, ckp.PrivateKey(), ckp.PublicKey(), "SHA-1", []byte("Hello, world!"))
132+
signAndVerify(t, ckp.PrivateKey(), ckp.PublicKey(), "SHA-256", []byte("Hello, world!"))
133+
signAndVerify(t, ckp.PrivateKey(), ckp.PublicKey(), "SHA-384", []byte("Hello, world!"))
134+
signAndVerify(t, ckp.PrivateKey(), ckp.PublicKey(), "SHA-512", []byte("Hello, world!"))
135+
}
136+
137+
func signAndVerify(t *testing.T, priv webcrypto.CryptoKey, pub webcrypto.CryptoKey, hashFn string, data []byte) {
128138
b, err := subtle.Sign(&Algorithm{
129-
Hash: "SHA-256",
130-
}, ckp.PrivateKey(), []byte("Hello, World!"))
139+
Hash: hashFn,
140+
}, priv, data)
131141
if err != nil {
132142
t.Error(err)
133143
}
134144

135145
ok, err := subtle.Verify(&Algorithm{
136-
Hash: "SHA-256",
137-
}, ckp.PublicKey(), b, []byte("Hello, World!"))
146+
Hash: hashFn,
147+
}, pub, b, data)
148+
if err != nil {
149+
t.Error(err)
150+
}
151+
if !ok {
152+
t.Error("sig mismatch")
153+
}
154+
155+
// test inputting and public into sign() and private key into verify()
156+
_, err = subtle.Sign(&Algorithm{
157+
Hash: hashFn,
158+
}, pub, data)
159+
if err == nil {
160+
t.Error("public key should not be allowed in sign()")
161+
}
162+
163+
ok, err = subtle.Verify(&Algorithm{
164+
Hash: hashFn,
165+
}, priv, b, data)
166+
if err == nil {
167+
t.Error("private key should not be allowed in verify()")
168+
}
169+
170+
if ok {
171+
t.Error("false should have been returned")
172+
}
173+
}
174+
175+
func Test_testData(t *testing.T) {
176+
b, err := os.ReadFile("testdata/data.json")
177+
if err != nil {
178+
t.Error(err)
179+
}
180+
var m map[string]any
181+
if err := json.Unmarshal(b, &m); err != nil {
182+
t.Error(err)
183+
}
184+
185+
b, err = json.Marshal(m["publicKey"])
186+
if err != nil {
187+
t.Error(err)
188+
}
189+
190+
var jwk webcrypto.JsonWebKey
191+
if err := json.Unmarshal(b, &jwk); err != nil {
192+
t.Error(err)
193+
}
194+
195+
k, err := subtle.ImportKey(webcrypto.Jwk, &jwk, &Algorithm{NamedCurve: P256}, true, webcrypto.Verify)
138196
if err != nil {
139197
t.Error(err)
140198
}
141199

200+
sig, err := base64.StdEncoding.DecodeString(m["signature"].(string))
201+
if err != nil {
202+
t.Error(err)
203+
}
204+
205+
ok, err := subtle.Verify(&Algorithm{
206+
Hash: m["hash"].(string),
207+
}, k, sig, []byte("test"))
208+
if err != nil {
209+
t.Error(err)
210+
}
142211
if !ok {
143-
t.FailNow()
212+
t.Error("verify failed")
144213
}
145-
fmt.Printf("%x", b)
146214
}
147215

148-
func Test_ExportAndImportKey(t *testing.T) {
216+
func Test_ExportAndImportJsonWebKey(t *testing.T) {
149217
k, err := subtle.GenerateKey(&Algorithm{
150218
NamedCurve: P256,
151-
}, false, webcrypto.Sign)
219+
}, true, webcrypto.Sign)
152220
if err != nil {
153221
t.Error(err)
154222
}
155223

156-
jwk, err := subtle.ExportKey(webcrypto.Jwk, k.(webcrypto.CryptoKeyPair).PrivateKey())
224+
// lets sign a message that we'll verify after importing
225+
data := []byte("Hello, world!")
226+
sig, err := subtle.Sign(&Algorithm{
227+
Hash: "SHA-256",
228+
}, k.(webcrypto.CryptoKeyPair).PrivateKey(), data)
157229
if err != nil {
158230
t.Error(err)
159231
}
160232

161-
// b, err := json.Marshal(jwk.(*webcrypto.JsonWebKey))
162-
// if err != nil {
163-
// t.Error(err)
164-
// }
233+
// export the private key and verify the jwk
234+
priv, err := subtle.ExportKey(webcrypto.Jwk, k.(webcrypto.CryptoKeyPair).PrivateKey())
235+
if err != nil {
236+
t.Error(err)
237+
}
238+
239+
jwk := priv.(*webcrypto.JsonWebKey)
240+
if jwk.Crv != "P-256" {
241+
t.Error("invalid crv")
242+
}
243+
if jwk.Kty != "EC" {
244+
t.Error("invalid kty")
245+
}
246+
if jwk.Y == "" || jwk.X == "" || jwk.D == "" {
247+
t.Error("invalid y|x|d")
248+
}
249+
if len(jwk.KeyOps) != 1 || jwk.KeyOps[0] != webcrypto.Sign {
250+
t.Error("invalid key_ops")
251+
}
252+
if !jwk.Ext {
253+
t.Error("invalid ext")
254+
}
165255

166-
_, err = subtle.ImportKey(webcrypto.Jwk, jwk, &Algorithm{NamedCurve: P256}, false, webcrypto.Sign)
256+
// export the public key and verify the jwk
257+
pub, err := subtle.ExportKey(webcrypto.Jwk, k.(webcrypto.CryptoKeyPair).PublicKey())
167258
if err != nil {
168259
t.Error(err)
169260
}
170261

262+
jwk = pub.(*webcrypto.JsonWebKey)
263+
if jwk.Crv != "P-256" {
264+
t.Error("invalid crv")
265+
}
266+
if jwk.Kty != "EC" {
267+
t.Error("invalid kty")
268+
}
269+
if jwk.Y == "" || jwk.X == "" {
270+
t.Error("invalid x|y")
271+
}
272+
if len(jwk.KeyOps) != 1 || jwk.KeyOps[0] != webcrypto.Verify {
273+
t.Error("invalid key_ops")
274+
}
275+
if !jwk.Ext {
276+
t.Error("invalid ext")
277+
}
278+
279+
// import the key
280+
imp, err := subtle.ImportKey(webcrypto.Jwk, jwk, &Algorithm{NamedCurve: P256}, true, webcrypto.Verify)
281+
if err != nil {
282+
t.Error(err)
283+
}
284+
285+
ok, err := subtle.Verify(&Algorithm{
286+
Hash: "SHA-256",
287+
}, imp, sig, data)
288+
if err != nil {
289+
t.Error(err)
290+
}
291+
292+
if !ok {
293+
t.Error("verify failed")
294+
}
295+
296+
// export the key and verify the jwk
297+
_, err = subtle.ExportKey(webcrypto.PKCS8, k.(webcrypto.CryptoKeyPair).PrivateKey())
298+
if err != nil {
299+
t.Error(err)
300+
}
171301
}

0 commit comments

Comments
 (0)