diff --git a/examples/import_export/import-export-jwk-ecdsa.js b/examples/import_export/import-export-jwk-ecdsa.js new file mode 100644 index 0000000..6554657 --- /dev/null +++ b/examples/import_export/import-export-jwk-ecdsa.js @@ -0,0 +1,28 @@ +import { crypto } from "k6/x/webcrypto"; + + export default async function () { + const jwk = { + kty: "EC", + crv: "P-521", + x: "AVb0efjfHiCn_8BM5CDD4VSuJRmWvuQvA0uE1Bt0PzTkXzEbgTqc3sjNpZu7vTHUYLMpJSHnwbci5WZ8A9svrnU_", + y: "AVAXNs_iRzlDINjkr8L9ObWpMxBhuB4iQSgrnheJGCK1t54FL0WXtZZD_Tk3nFG9USXE9IvD8CXOPNNpUyhsyzj7", + d: "APQIdYNoupMPMPdq4FT-XNLOf9osn3am1DbPddZsRAv-YzHHwXKhJHgZPIJRSHvJEmP6UCF_hf9jb1nNVG46tIO0", + }; + + console.log("static key: " + JSON.stringify(jwk)); + + + const importedKey = await crypto.subtle.importKey( + "jwk", + jwk, + { name: "ECDSA", namedCurve: "P-256" }, + true, + ["sign"] + ); + + console.log("imported: " + JSON.stringify(importedKey)); + + const exportedAgain = await crypto.subtle.exportKey("jwk", importedKey); + + console.log("exported again: " + JSON.stringify(exportedAgain)); +} \ No newline at end of file diff --git a/go.mod b/go.mod index 9b2db9e..ad937d2 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.19 require ( github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d github.com/google/uuid v1.3.1 - github.com/lestrrat-go/jwx/v2 v2.0.21 github.com/stretchr/testify v1.9.0 go.k6.io/k6 v0.49.0 gopkg.in/guregu/null.v3 v3.3.0 @@ -14,22 +13,15 @@ require ( require ( github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/dlclark/regexp2 v1.9.0 // indirect github.com/fatih/color v1.16.0 // indirect github.com/go-logr/logr v1.3.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-sourcemap/sourcemap v2.1.4-0.20211119122758-180fcef48034+incompatible // indirect - github.com/goccy/go-json v0.10.2 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/pprof v0.0.0-20230728192033-2ba5b33183c6 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/lestrrat-go/blackmagic v1.0.2 // indirect - github.com/lestrrat-go/httpcc v1.0.1 // indirect - github.com/lestrrat-go/httprc v1.0.5 // indirect - github.com/lestrrat-go/iter v1.0.2 // indirect - github.com/lestrrat-go/option v1.0.1 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -37,7 +29,6 @@ require ( github.com/onsi/ginkgo v1.16.5 // indirect github.com/onsi/gomega v1.20.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/segmentio/asm v1.2.0 // indirect github.com/serenize/snaker v0.0.0-20201027110005-a7ad2135616e // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/afero v1.1.2 // indirect diff --git a/go.sum b/go.sum index 349f4ed..5ffc81b 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,6 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.9.0 h1:pTK/l/3qYIKaRXuHnEnIf7Y5NxfRPfpb7dis6/gdlVI= @@ -33,8 +31,6 @@ github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5Nq github.com/go-sourcemap/sourcemap v2.1.4-0.20211119122758-180fcef48034+incompatible h1:bopx7t9jyUNX1ebhr0G4gtQWmUOgwQRI0QsYhdYLgkU= github.com/go-sourcemap/sourcemap v2.1.4-0.20211119122758-180fcef48034+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= @@ -72,18 +68,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= -github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= -github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= -github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= -github.com/lestrrat-go/httprc v1.0.5 h1:bsTfiH8xaKOJPrg1R+E3iE/AWZr/x0Phj9PBTG/OLUk= -github.com/lestrrat-go/httprc v1.0.5/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= -github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= -github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= -github.com/lestrrat-go/jwx/v2 v2.0.21 h1:jAPKupy4uHgrHFEdjVjNkUgoBKtVDgrQPB/h55FHrR0= -github.com/lestrrat-go/jwx/v2 v2.0.21/go.mod h1:09mLW8zto6bWL9GbwnqAli+ArLf+5M33QLQPDggkUWM= -github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= -github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -110,8 +94,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= -github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/serenize/snaker v0.0.0-20201027110005-a7ad2135616e h1:zWKUYT07mGmVBH+9UgnHXd/ekCK99C8EbDSAt5qsjXE= github.com/serenize/snaker v0.0.0-20201027110005-a7ad2135616e/go.mod h1:Yow6lPLSAXx2ifx470yD/nUe22Dv5vBvxK/UK9UUTVs= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= @@ -120,9 +102,7 @@ github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/webcrypto/base64.go b/webcrypto/base64.go new file mode 100644 index 0000000..58d8c21 --- /dev/null +++ b/webcrypto/base64.go @@ -0,0 +1,11 @@ +package webcrypto + +import "encoding/base64" + +func base64URLEncode(data []byte) string { + return base64.RawURLEncoding.EncodeToString(data) +} + +func base64URLDecode(data string) ([]byte, error) { + return base64.RawURLEncoding.DecodeString(data) +} diff --git a/webcrypto/elliptic_curve.go b/webcrypto/elliptic_curve.go index 1e98866..b4e1e19 100644 --- a/webcrypto/elliptic_curve.go +++ b/webcrypto/elliptic_curve.go @@ -7,11 +7,18 @@ import ( "crypto/rand" "crypto/x509" "errors" + "fmt" "math/big" "github.com/dop251/goja" ) +const ( + p256Canonical = "P-256" + p384Canonical = "P-384" + p521Canonical = "P-521" +) + // EcKeyAlgorithm is the algorithm for elliptic curve keys as defined in the [specification]. // // [specification]: https://www.w3.org/TR/WebCryptoAPI/#EcKeyAlgorithm-dictionary @@ -54,35 +61,30 @@ func (e *EcKeyImportParams) ImportKey( keyData []byte, _ []CryptoKeyUsage, ) (*CryptoKey, error) { - var keyType CryptoKeyType - var handle any - - var importFn func(curve EllipticCurveKind, keyData []byte) (any, error) + var importFn func(curve EllipticCurveKind, keyData []byte) (any, CryptoKeyType, error) switch { case e.Algorithm.Name == ECDH && format == Pkcs8KeyFormat: importFn = importECDHPrivateKey - keyType = PrivateCryptoKeyType case e.Algorithm.Name == ECDH && format == RawKeyFormat: importFn = importECDHPublicKey - keyType = PublicCryptoKeyType case e.Algorithm.Name == ECDSA && format == Pkcs8KeyFormat: importFn = importECDSAPrivateKey - keyType = PrivateCryptoKeyType case e.Algorithm.Name == ECDSA && format == RawKeyFormat: importFn = importECDSAPublicKey - keyType = PublicCryptoKeyType case e.Algorithm.Name == ECDH && format == SpkiKeyFormat: importFn = importECDHSPKIPublicKey - keyType = PublicCryptoKeyType case e.Algorithm.Name == ECDSA && format == SpkiKeyFormat: importFn = importECDSASPKIPublicKey - keyType = PublicCryptoKeyType + case e.Algorithm.Name == ECDSA && format == JwkKeyFormat: + importFn = importECDSAJWK + case e.Algorithm.Name == ECDH && format == JwkKeyFormat: + importFn = importECDHJWK default: return nil, NewError(NotSupportedError, unsupportedKeyFormatErrorMsg+" "+format+" for algorithm "+e.Algorithm.Name) } - handle, err := importFn(e.NamedCurve, keyData) + handle, keyType, err := importFn(e.NamedCurve, keyData) if err != nil { return nil, err } @@ -99,48 +101,53 @@ func (e *EcKeyImportParams) ImportKey( }, nil } -func importECDHPublicKey(curve EllipticCurveKind, keyData []byte) (any, error) { - c, err := pickECDHCurve(curve) +func importECDHPublicKey(curve EllipticCurveKind, keyData []byte) (any, CryptoKeyType, error) { + c, err := pickECDHCurve(curve.String()) if err != nil { - return nil, NewError(NotSupportedError, "invalid elliptic curve "+string(curve)) + return nil, UnknownCryptoKeyType, NewError(NotSupportedError, "invalid ECDH curve "+string(curve)) } handle, err := c.NewPublicKey(keyData) if err != nil { - return nil, NewError(DataError, "unable to import ECDH public key data: "+err.Error()) + return nil, UnknownCryptoKeyType, NewError(DataError, "unable to import ECDH public key data: "+err.Error()) } - return handle, nil + return handle, PublicCryptoKeyType, nil } -func importECDHSPKIPublicKey(_ EllipticCurveKind, keyData []byte) (any, error) { +func importECDHSPKIPublicKey(_ EllipticCurveKind, keyData []byte) (any, CryptoKeyType, error) { pk, err := x509.ParsePKIXPublicKey(keyData) if err != nil { - return nil, NewError(DataError, "unable to import ECDH public key data: "+err.Error()) + return nil, UnknownCryptoKeyType, NewError(DataError, "unable to import ECDH public key data: "+err.Error()) } ecdsaKey, ok := pk.(*ecdsa.PublicKey) if !ok { - return nil, NewError(DataError, "a public key is not an ECDSA key") + return nil, UnknownCryptoKeyType, NewError(DataError, "a public key is not an ECDSA key") } // try to restore the ECDH key - return ecdsaKey.ECDH() + key, err := ecdsaKey.ECDH() + if err != nil { + return nil, UnknownCryptoKeyType, NewError(DataError, "unable to import key data: "+err.Error()) + } + + return key, PublicCryptoKeyType, nil } -func importECDSASPKIPublicKey(_ EllipticCurveKind, keyData []byte) (any, error) { +func importECDSASPKIPublicKey(_ EllipticCurveKind, keyData []byte) (any, CryptoKeyType, error) { pk, err := x509.ParsePKIXPublicKey(keyData) if err != nil { - return nil, NewError(DataError, "unable to import ECDH public key data: "+err.Error()) + return nil, UnknownCryptoKeyType, NewError(DataError, "unable to import ECDH public key data: "+err.Error()) } ecdsaKey, ok := pk.(*ecdsa.PublicKey) if !ok { - return nil, NewError(DataError, "a public key is not an ECDSA key") + return nil, UnknownCryptoKeyType, NewError(DataError, "a public key is not an ECDSA key") } // try to restore the ECDH key - return ecdsaKey, nil + return ecdsaKey, PublicCryptoKeyType, nil } // EllipticCurveKind represents the kind of elliptic curve that is being used. @@ -157,6 +164,10 @@ const ( EllipticCurveKindP521 EllipticCurveKind = "P-521" ) +func (k EllipticCurveKind) String() string { + return string(k) +} + // IsEllipticCurve returns true if the given string is a valid EllipticCurveKind, // false otherwise. func IsEllipticCurve(name string) bool { @@ -172,57 +183,57 @@ func IsEllipticCurve(name string) bool { } } -func importECDHPrivateKey(_ EllipticCurveKind, keyData []byte) (any, error) { +func importECDHPrivateKey(_ EllipticCurveKind, keyData []byte) (any, CryptoKeyType, error) { parsedKey, err := x509.ParsePKCS8PrivateKey(keyData) if err != nil { - return nil, NewError(DataError, "unable to import ECDH private key data: "+err.Error()) + return nil, UnknownCryptoKeyType, NewError(DataError, "unable to import ECDH private key data: "+err.Error()) } // check if the key is an ECDSA key ecdsaKey, ok := parsedKey.(*ecdsa.PrivateKey) if !ok { - return nil, NewError(DataError, "a private key is not an ECDSA key") + return nil, UnknownCryptoKeyType, NewError(DataError, "a private key is not an ECDSA key") } // try to restore the ECDH key handle, err := ecdsaKey.ECDH() if err != nil { - return nil, NewError(DataError, "unable to import key data: "+err.Error()) + return nil, UnknownCryptoKeyType, NewError(DataError, "unable to import key data: "+err.Error()) } - return handle, nil + return handle, PrivateCryptoKeyType, nil } -func importECDSAPrivateKey(_ EllipticCurveKind, keyData []byte) (any, error) { +func importECDSAPrivateKey(_ EllipticCurveKind, keyData []byte) (any, CryptoKeyType, error) { parsedKey, err := x509.ParsePKCS8PrivateKey(keyData) if err != nil { - return nil, NewError(DataError, "unable to import ECDSA private key data: "+err.Error()) + return nil, UnknownCryptoKeyType, NewError(DataError, "unable to import ECDSA private key data: "+err.Error()) } ecdsaKey, ok := parsedKey.(*ecdsa.PrivateKey) if !ok { - return nil, NewError(DataError, "a private key is not an ECDSA key") + return nil, UnknownCryptoKeyType, NewError(DataError, "a private key is not an ECDSA key") } - return ecdsaKey, nil + return ecdsaKey, PrivateCryptoKeyType, nil } -func importECDSAPublicKey(curve EllipticCurveKind, keyData []byte) (any, error) { - c, err := pickEllipticCurve(curve) +func importECDSAPublicKey(curve EllipticCurveKind, keyData []byte) (any, CryptoKeyType, error) { + c, err := pickEllipticCurve(curve.String()) if err != nil { - return nil, NewError(NotSupportedError, "invalid elliptic curve "+string(curve)) + return nil, UnknownCryptoKeyType, NewError(NotSupportedError, "invalid elliptic curve "+string(curve)) } x, y := elliptic.Unmarshal(c, keyData) if x == nil { - return nil, NewError(DataError, "unable to import ECDSA public key data") + return nil, UnknownCryptoKeyType, NewError(DataError, "unable to import ECDSA public key data") } return &ecdsa.PublicKey{ Curve: c, X: x, Y: y, - }, nil + }, PublicCryptoKeyType, nil } // ECKeyGenParams represents the object that should be passed as the algorithm @@ -276,7 +287,7 @@ func (ecgp *ECKeyGenParams) GenerateKey( } if !isValidEllipticCurve(ecgp.NamedCurve) { - return nil, NewError(NotSupportedError, "invalid elliptic curve "+string(ecgp.NamedCurve)) + return nil, NewError(NotSupportedError, "elliptic curve "+string(ecgp.NamedCurve)+" is not supported") } if len(keyUsages) == 0 { @@ -330,7 +341,7 @@ func generateECDHKeyPair(curve EllipticCurveKind, keyUsages []CryptoKeyUsage) (a } } - c, err := pickECDHCurve(curve) + c, err := pickECDHCurve(curve.String()) if err != nil { return nil, nil, NewError(NotSupportedError, err.Error()) } @@ -354,7 +365,7 @@ func generateECDSAKeyPair(curve EllipticCurveKind, keyUsages []CryptoKeyUsage) ( } } - c, err := pickEllipticCurve(curve) + c, err := pickEllipticCurve(curve.String()) if err != nil { return nil, nil, NewError(NotSupportedError, err.Error()) } @@ -372,36 +383,33 @@ func isValidEllipticCurve(curve EllipticCurveKind) bool { return curve == EllipticCurveKindP256 || curve == EllipticCurveKindP384 || curve == EllipticCurveKindP521 } -// pickECDHCurve returns the elliptic curve that corresponds to the given -// EllipticCurveKind. -// If the curve is not supported, an error is returned. -func pickECDHCurve(k EllipticCurveKind) (ecdh.Curve, error) { +func pickECDHCurve(k string) (ecdh.Curve, error) { switch k { - case EllipticCurveKindP256: + case p256Canonical: return ecdh.P256(), nil - case EllipticCurveKindP384: + case p384Canonical: return ecdh.P384(), nil - case EllipticCurveKindP521: + case p521Canonical: return ecdh.P521(), nil default: return nil, errors.New("invalid ECDH curve") } } -func pickEllipticCurve(k EllipticCurveKind) (elliptic.Curve, error) { +func pickEllipticCurve(k string) (elliptic.Curve, error) { switch k { - case EllipticCurveKindP256: + case p256Canonical: return elliptic.P256(), nil - case EllipticCurveKindP384: + case p384Canonical: return elliptic.P384(), nil - case EllipticCurveKindP521: + case p521Canonical: return elliptic.P521(), nil default: - return nil, errors.New("invalid elliptic curve") + return nil, errors.New("invalid elliptic curve " + k) } } -func exportECKey(alg string, ck *CryptoKey, format KeyFormat) ([]byte, error) { +func exportECKey(alg string, ck *CryptoKey, format KeyFormat) (interface{}, error) { if ck.handle == nil { return nil, NewError(OperationError, "key data is not accessible") } @@ -440,6 +448,8 @@ func exportECKey(alg string, ck *CryptoKey, format KeyFormat) ([]byte, error) { } return bytes, nil + case JwkKeyFormat: + return exportECJWK(ck) default: return nil, NewError(NotSupportedError, unsupportedKeyFormatErrorMsg+" "+format) } @@ -588,3 +598,40 @@ func (edsa *ECDSAParams) Verify(key CryptoKey, signature []byte, data []byte) (b return ecdsa.Verify(k, hasher.Sum(nil), r, s), nil } + +func convertECDHtoECDSAKey(k *ecdh.PrivateKey) (*ecdsa.PrivateKey, error) { + pk, err := convertPublicECDHtoECDSA(k.PublicKey()) + if err != nil { + return nil, err + } + + return &ecdsa.PrivateKey{ + PublicKey: *pk, + D: new(big.Int).SetBytes(k.Bytes()), + }, nil +} + +func convertPublicECDHtoECDSA(k *ecdh.PublicKey) (*ecdsa.PublicKey, error) { + var crv elliptic.Curve + switch k.Curve() { + case ecdh.P256(): + crv = elliptic.P256() + case ecdh.P384(): + crv = elliptic.P384() + case ecdh.P521(): + crv = elliptic.P521() + default: + return nil, errors.New("curve not supported for converting to ECDSA key") + } + + x, y := elliptic.Unmarshal(crv, k.Bytes()) + if x == nil { + return nil, fmt.Errorf("unable to convert ECDH public key to ECDSA public key, curve: %s", crv.Params().Name) + } + + return &ecdsa.PublicKey{ + Curve: crv, + X: x, + Y: y, + }, nil +} diff --git a/webcrypto/jwk.go b/webcrypto/jwk.go index 4252fc0..3bb666b 100644 --- a/webcrypto/jwk.go +++ b/webcrypto/jwk.go @@ -1,11 +1,21 @@ package webcrypto import ( + "crypto/ecdh" + "crypto/ecdsa" + "crypto/elliptic" "encoding/json" "errors" "fmt" + "math/big" +) + +const ( + // JWKECKeyType represents the elliptic curve key type. + JWKECKeyType = "EC" - "github.com/lestrrat-go/jwx/v2/jwk" + // JWKOctKeyType represents the symmetric key type. + JWKOctKeyType = "oct" ) // JsonWebKey represents a JSON Web Key (JsonWebKey) key. @@ -16,20 +26,42 @@ func (jwk *JsonWebKey) Set(key string, value interface{}) { (*jwk)[key] = value } +// symmetricJWK represents a symmetric JWK key. +// It is used to unmarshal symmetric keys from JWK format. +type symmetricJWK struct { + Kty string `json:"kty"` + K string `json:"k"` +} + +func (jwk *symmetricJWK) validate() error { + if jwk.Kty != JWKOctKeyType { + return fmt.Errorf("invalid key type: %s", jwk.Kty) + } + + if jwk.K == "" { + return errors.New("key (k) is required") + } + + return nil +} + // extractSymmetricJWK extracts the symmetric key from a given JWK key (JSON data). func extractSymmetricJWK(jsonKeyData []byte) ([]byte, error) { - key, err := jwk.ParseKey(jsonKeyData) - if err != nil { - return nil, fmt.Errorf("failed to parse input as JWK key: %w", err) + sk := symmetricJWK{} + if err := json.Unmarshal(jsonKeyData, &sk); err != nil { + return nil, fmt.Errorf("failed to parse symmetric JWK: %w", err) } - // check if the key is a symmetric key - sk, ok := key.(jwk.SymmetricKey) - if !ok { - return nil, errors.New("input isn't a valid JWK symmetric key") + if err := sk.validate(); err != nil { + return nil, fmt.Errorf("invalid symmetric JWK: %w", err) } - return sk.Octets(), nil + skBytes, err := base64URLDecode(sk.K) + if err != nil { + return nil, fmt.Errorf("failed to decode symmetric key: %w", err) + } + + return skBytes, nil } // exportSymmetricJWK exports a symmetric key as a map of JWK key parameters. @@ -39,25 +71,11 @@ func exportSymmetricJWK(key *CryptoKey) (*JsonWebKey, error) { return nil, errors.New("key's handle isn't a byte slice") } - sk, err := jwk.FromRaw(rawKey) - if err != nil { - return nil, fmt.Errorf("failed to create JWK key: %w", err) - } - - // we do marshal and unmarshal to get the map of JWK key parameters - // where all standard parameters are present, a proper marshaling is done - m, err := json.Marshal(sk) - if err != nil { - return nil, fmt.Errorf("failed to marshal JWK key: %w", err) - } - // wrap result into the object that is expected to be returned exported := &JsonWebKey{} - err = json.Unmarshal(m, exported) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal JWK key: %w", err) - } + exported.Set("k", base64URLEncode(rawKey)) + exported.Set("kty", JWKOctKeyType) exported.Set("ext", key.Extractable) exported.Set("key_ops", key.Usages) @@ -89,3 +107,186 @@ func extractAlg(inAlg any, keyLen int) (string, error) { return "", fmt.Errorf("unsupported algorithm: %v", inAlg) } } + +// ecJWK represents an EC JWK key. +// It is used to unmarshal ECDSA and ECDH keys to and from JWK format. +type ecJWK struct { + // Key type + Kty string `json:"kty"` + // Canonical Curve + Crv string `json:"crv"` + // X coordinate + X string `json:"x"` + // Y coordinate + Y string `json:"y"` + // Private scalar + D string `json:"d"` +} + +func (jwk *ecJWK) validate() error { + if jwk.Kty != JWKECKeyType { + return fmt.Errorf("invalid key type: %s", jwk.Kty) + } + + if jwk.Crv == "" { + return errors.New("curve is required") + } + + if jwk.X == "" { + return errors.New("coordinate X is required") + } + + if jwk.Y == "" { + return errors.New("coordinate Y is required") + } + + return nil +} + +// encodeCurveBigInt encodes the private scalar D of an ECDSA key with padding. +func encodeCurveBigInt(data *big.Int, curveBits int) string { + // Determine the expected byte length for the curve + byteLength := curveBits / 8 + // Add one more byte if the bits are not a multiple of 8 + if curveBits%8 != 0 { + byteLength++ + } + + dBytes := data.Bytes() + dPadded := padLeft(dBytes, byteLength) // Pad if necessary + + return base64URLEncode(dPadded) +} + +// padLeft pads the byte slice with zeros to the left to ensure it has a specific length. +func padLeft(bytes []byte, size int) []byte { + padding := make([]byte, size-len(bytes)) + return append(padding, bytes...) //nolint:makezero // we need to pad with zeros +} + +func exportECJWK(key *CryptoKey) (interface{}, error) { + exported := &JsonWebKey{} + exported.Set("kty", JWKECKeyType) + + var x, y, d *big.Int + var curveParams *elliptic.CurveParams + + switch k := key.handle.(type) { + case *ecdsa.PrivateKey: + x = k.X + y = k.Y + d = k.D + curveParams = k.Curve.Params() + case *ecdsa.PublicKey: + x = k.X + y = k.Y + curveParams = k.Curve.Params() + case *ecdh.PrivateKey: + ecdsaKey, err := convertECDHtoECDSAKey(k) + if err != nil { + return nil, fmt.Errorf("failed to convert ECDH key to ECDSA key: %w", err) + } + + x = ecdsaKey.X + y = ecdsaKey.Y + d = ecdsaKey.D + curveParams = ecdsaKey.Curve.Params() + case *ecdh.PublicKey: + ecdsaKey, err := convertPublicECDHtoECDSA(k) + if err != nil { + return nil, fmt.Errorf("failed to convert ECDH key to ECDSA key: %w", err) + } + + x = ecdsaKey.X + y = ecdsaKey.Y + curveParams = ecdsaKey.Curve.Params() + default: + return nil, errors.New("key's handle isn't an ECDSA/ECDH public/private key") + } + + exported.Set("crv", curveParams.Name) + curveBits := curveParams.BitSize + + exported.Set("x", base64URLEncode(x.Bytes())) + exported.Set("y", base64URLEncode(y.Bytes())) + + if d != nil { + exported.Set("d", encodeCurveBigInt(d, curveBits)) + } + + return exported, nil +} + +func importECDSAJWK(_ EllipticCurveKind, jsonKeyData []byte) (any, CryptoKeyType, error) { + var jwkKey ecJWK + if err := json.Unmarshal(jsonKeyData, &jwkKey); err != nil { + return nil, UnknownCryptoKeyType, fmt.Errorf("failed to parse input as EC JWK key: %w", err) + } + + if err := jwkKey.validate(); err != nil { + return nil, UnknownCryptoKeyType, fmt.Errorf("invalid EC JWK key: %w", err) + } + + crv, err := pickEllipticCurve(jwkKey.Crv) + if err != nil { + return nil, UnknownCryptoKeyType, fmt.Errorf("failed to parse elliptic curve: %w", err) + } + + x, err := base64URLDecode(jwkKey.X) + if err != nil { + return nil, UnknownCryptoKeyType, fmt.Errorf("failed to decode X coordinate: %w", err) + } + + y, err := base64URLDecode(jwkKey.Y) + if err != nil { + return nil, UnknownCryptoKeyType, fmt.Errorf("failed to decode Y coordinate: %w", err) + } + + pk := &ecdsa.PublicKey{ + Curve: crv, + X: new(big.Int).SetBytes(x), + Y: new(big.Int).SetBytes(y), + } + + // if the key is a public key, return it + if jwkKey.D == "" { + return pk, PublicCryptoKeyType, nil + } + + d, err := base64URLDecode(jwkKey.D) + if err != nil { + return nil, UnknownCryptoKeyType, fmt.Errorf("failed to decode D: %w", err) + } + + return &ecdsa.PrivateKey{ + PublicKey: *pk, + D: new(big.Int).SetBytes(d), + }, PrivateCryptoKeyType, nil +} + +func importECDHJWK(_ EllipticCurveKind, jsonKeyData []byte) (any, CryptoKeyType, error) { + // first we do try to parse the key as ECDSA key + key, _, err := importECDSAJWK(EllipticCurveKindP256, jsonKeyData) + if err != nil { + return nil, UnknownCryptoKeyType, fmt.Errorf("failed to parse input as ECDH key: %w", err) + } + + switch key := key.(type) { + case *ecdsa.PrivateKey: + ecdhKey, err := key.ECDH() + if err != nil { + return nil, UnknownCryptoKeyType, fmt.Errorf("failed to convert ECDSA key to ECDH key: %w", err) + } + + return ecdhKey, PrivateCryptoKeyType, nil + case *ecdsa.PublicKey: + ecdhKey, err := key.ECDH() + if err != nil { + return nil, UnknownCryptoKeyType, fmt.Errorf("failed to convert ECDSA key to ECDH key: %w", err) + } + + return ecdhKey, PublicCryptoKeyType, nil + default: + return nil, UnknownCryptoKeyType, errors.New("input isn't a valid ECDH key") + } +} diff --git a/webcrypto/key.go b/webcrypto/key.go index 2b19c43..6acb150 100644 --- a/webcrypto/key.go +++ b/webcrypto/key.go @@ -109,6 +109,9 @@ func (ck *CryptoKey) ContainsUsage(usage CryptoKeyUsage) bool { type CryptoKeyType = string const ( + // UnknownCryptoKeyType that we set when we don't know the type of the key. + UnknownCryptoKeyType CryptoKeyType = "unknown" + // SecretCryptoKeyType carries the information that a key is a secret key // to use with a symmetric algorithm. SecretCryptoKeyType CryptoKeyType = "secret" diff --git a/webcrypto/tests/import_export/ec_importKey.js b/webcrypto/tests/import_export/ec_importKey.js index 50a631e..88fa524 100644 --- a/webcrypto/tests/import_export/ec_importKey.js +++ b/webcrypto/tests/import_export/ec_importKey.js @@ -79,8 +79,8 @@ testVectors.forEach(function(vector) { // Test public keys first [[]].forEach(function(usages) { // Only valid usages argument is empty array // TODO: return back formats after implementing them - // 'spki', 'spki_compressed', 'jwk', 'raw_compressed' - ['raw', 'spki'].forEach(function(format) { + // 'spki', 'spki_compressed', 'raw_compressed' + ['raw', 'spki', 'jwk'].forEach(function(format) { var algorithm = {name: vector.name, namedCurve: curve}; var data = keyData[curve]; if (format === "jwk") { // Not all fields used for public keys @@ -93,9 +93,8 @@ testVectors.forEach(function(vector) { }); // Next, test private keys - // TODO: return back 'jwk' once it supported allValidUsages(vector.privateUsages, []).forEach(function(usages) { - ['pkcs8'].forEach(function(format) { + ['pkcs8', 'jwk'].forEach(function(format) { var algorithm = {name: vector.name, namedCurve: curve}; var data = keyData[curve];