From bbadb1b247e7e0a4869a50ad59aff9dd5ba9b72e Mon Sep 17 00:00:00 2001 From: Oleg Bespalov Date: Tue, 16 Apr 2024 19:04:19 +0200 Subject: [PATCH] jwk: ecdh & ecdsa support --- webcrypto/elliptic_curve.go | 252 ++++++++++++++---- webcrypto/key.go | 3 + webcrypto/tests/import_export/ec_importKey.js | 7 +- 3 files changed, 209 insertions(+), 53 deletions(-) diff --git a/webcrypto/elliptic_curve.go b/webcrypto/elliptic_curve.go index 1e98866..e0526b3 100644 --- a/webcrypto/elliptic_curve.go +++ b/webcrypto/elliptic_curve.go @@ -6,10 +6,13 @@ import ( "crypto/elliptic" "crypto/rand" "crypto/x509" + "encoding/json" "errors" + "fmt" "math/big" "github.com/dop251/goja" + "github.com/lestrrat-go/jwx/v2/jwk" ) // EcKeyAlgorithm is the algorithm for elliptic curve keys as defined in the [specification]. @@ -54,35 +57,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 +97,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 elliptic 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 +160,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 +179,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 @@ -330,7 +337,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 +361,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()) } @@ -375,33 +382,33 @@ func isValidEllipticCurve(curve EllipticCurveKind) bool { // 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 "P-256": return ecdh.P256(), nil - case EllipticCurveKindP384: + case "P-384": return ecdh.P384(), nil - case EllipticCurveKindP521: + case "P-521": 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 "P-256": return elliptic.P256(), nil - case EllipticCurveKindP384: + case "P-384": return elliptic.P384(), nil - case EllipticCurveKindP521: + case "P-521": return elliptic.P521(), nil default: return nil, errors.New("invalid elliptic curve") } } -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 +447,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 +597,148 @@ func (edsa *ECDSAParams) Verify(key CryptoKey, signature []byte, data []byte) (b return ecdsa.Verify(k, hasher.Sum(nil), r, s), nil } + +func exportECJWK(key *CryptoKey) (*JsonWebKey, error) { + var handle any + var err error + + switch k := key.handle.(type) { + case *ecdsa.PrivateKey, *ecdsa.PublicKey: + // do nothing + handle = key.handle + case *ecdh.PrivateKey: + handle, err = convertECDHtoECDSAKey(k) + if err != nil { + return nil, fmt.Errorf("failed to convert ECDH key to ECDSA key: %w", err) + } + case *ecdh.PublicKey: + handle, err = convertPublicECDHtoECDSA(k) + if err != nil { + return nil, fmt.Errorf("failed to convert ECDH key to ECDSA key: %w", err) + } + default: + return nil, errors.New("key's handle isn't an ECDSA key") + } + + sk, err := jwk.FromRaw(handle) + 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) + } + + return exported, 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("invalid elliptic curve") + } + + x, y := elliptic.Unmarshal(crv, k.Bytes()) + if x == nil { + return nil, errors.New("unable to import ECDSA public key data") + } + + return &ecdsa.PublicKey{ + Curve: crv, + X: x, + Y: y, + }, nil +} + +func importECDSAJWK(_ EllipticCurveKind, jsonKeyData []byte) (any, CryptoKeyType, error) { + jwkKey, err := jwk.ParseKey(jsonKeyData) + if err != nil { + return nil, UnknownCryptoKeyType, fmt.Errorf("failed to parse input as JWK key: %w", err) + } + + switch key := jwkKey.(type) { + case jwk.ECDSAPrivateKey: + crv, err := pickEllipticCurve(key.Crv().String()) + if err != nil { + return nil, UnknownCryptoKeyType, err + } + + return &ecdsa.PrivateKey{ + PublicKey: ecdsa.PublicKey{ + Curve: crv, + X: new(big.Int).SetBytes(key.X()), + Y: new(big.Int).SetBytes(key.Y()), + }, + D: new(big.Int).SetBytes(key.D()), + }, PrivateCryptoKeyType, nil + case jwk.ECDSAPublicKey: + crv, err := pickEllipticCurve(key.Crv().String()) + if err != nil { + return nil, UnknownCryptoKeyType, err + } + + return &ecdsa.PublicKey{ + Curve: crv, + X: new(big.Int).SetBytes(key.X()), + Y: new(big.Int).SetBytes(key.Y()), + }, PublicCryptoKeyType, nil + default: + return nil, UnknownCryptoKeyType, errors.New("input isn't a valid JWK ECDSA key") + } +} + +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];