Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Support of RSA #85

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ check-linter-version:

## test: Executes any tests.
test:
go test -race -timeout 30s ./...
go test -race -timeout 60s ./...

## lint: Runs the linters.
lint: linter-config check-linter-version
Expand Down
16 changes: 16 additions & 0 deletions examples/generateKey/generateKey-RSA-PSS.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { crypto } from "k6/x/webcrypto";

export default async function () {
const key = await crypto.subtle.generateKey(
{
name: "RSA-PSS",
modulusLength: 2048, // Can be 1024, 2048, or 4096
publicExponent: new Uint8Array([1, 0, 1]), // 24-bit representation of 65537
hash: {name: "SHA-1"}, // Could be "SHA-1", "SHA-256", "SHA-384", or "SHA-512"
},
true,
["sign", "verify"] // Key usages
);

console.log(JSON.stringify(key));
}
16 changes: 16 additions & 0 deletions examples/generateKey/generateKey-RSASSA-PKCS1-v1_5.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { crypto } from "k6/x/webcrypto";

export default async function () {
const key = await crypto.subtle.generateKey(
{
name: "RSASSA-PKCS1-v1_5",
modulusLength: 1024, // Can be 1024, 2048, or 4096
publicExponent: new Uint8Array([1]), // 24-bit representation of 65537
hash: {name: "SHA-256"}, // Could be "SHA-1", "SHA-256", "SHA-384", or "SHA-512"
},
true,
["sign", "verify"] // Key usages
);

console.log(JSON.stringify(key));
}
31 changes: 31 additions & 0 deletions examples/import_export/export-rsa-spki.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { crypto } from "k6/x/webcrypto";

export default async function () {
const generatedKeyPair = await await crypto.subtle.generateKey(
{
name: "RSASSA-PKCS1-v1_5",
modulusLength: 1024,
publicExponent: new Uint8Array([1]),
hash: {name: "SHA-256"},
},
true,
["sign", "verify"] // Key usages
);

const exportedPrivateKey = await crypto.subtle.exportKey(
"pkcs8",
generatedKeyPair.privateKey
);
console.log("exported private key: " + printArrayBuffer(exportedPrivateKey));

const exportedPublicKey = await crypto.subtle.exportKey(
"spki",
generatedKeyPair.publicKey
);
console.log("exported public key: " + printArrayBuffer(exportedPublicKey));
}

const printArrayBuffer = (buffer) => {
let view = new Uint8Array(buffer);
return Array.from(view);
};
37 changes: 30 additions & 7 deletions webcrypto/algorithm.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package webcrypto

import (
"fmt"
"reflect"
"strings"

Expand Down Expand Up @@ -145,16 +146,30 @@ func normalizeAlgorithm(rt *sobek.Runtime, v sobek.Value, op AlgorithmIdentifier
return Algorithm{}, NewError(SyntaxError, "algorithm cannot be interpreted as a string or an object")
}

algorithm.Name = normalizeAlgorithmName(algorithm.Name)

if !isRegisteredAlgorithm(algorithm.Name, op) {
return Algorithm{}, NewError(
NotSupportedError,
fmt.Sprintf("algorithm %q doesn't support (in implementation) operation %q", algorithm.Name, op),
)
}

return algorithm, nil
}

func normalizeAlgorithmName(name string) string {
// Algorithm identifiers are always upper cased.
// A registered algorithm provided in lower case format, should
// be considered valid.
algorithm.Name = strings.ToUpper(algorithm.Name)
name = strings.ToUpper(name)

if !isRegisteredAlgorithm(algorithm.Name, op) {
return Algorithm{}, NewError(NotSupportedError, "unsupported algorithm: "+algorithm.Name)
// exception is made for RSASSA-PKCS1-v1_5
if name == strings.ToUpper(RSASsaPkcs1v15) {
return RSASsaPkcs1v15
}

return algorithm, nil
return name
}

// isRegisteredAlgorithm returns true if the given algorithm name is registered
Expand All @@ -171,13 +186,17 @@ func isRegisteredAlgorithm(algorithmName string, forOperation string) bool {
return isAesAlgorithm(algorithmName) ||
isHashAlgorithm(algorithmName) ||
algorithmName == HMAC ||
isEllipticCurve(algorithmName)
isEllipticCurve(algorithmName) ||
isRSAAlgorithm(algorithmName)
case OperationIdentifierExportKey, OperationIdentifierImportKey:
return isAesAlgorithm(algorithmName) || algorithmName == HMAC || isEllipticCurve(algorithmName)
return isAesAlgorithm(algorithmName) ||
algorithmName == HMAC ||
isEllipticCurve(algorithmName) ||
isRSAAlgorithm(algorithmName)
case OperationIdentifierEncrypt, OperationIdentifierDecrypt:
return isAesAlgorithm(algorithmName)
case OperationIdentifierSign, OperationIdentifierVerify:
return algorithmName == HMAC || algorithmName == ECDSA
return algorithmName == HMAC || algorithmName == ECDSA || algorithmName == RSAPss
default:
return false
}
Expand All @@ -191,6 +210,10 @@ func isHashAlgorithm(algorithmName string) bool {
return algorithmName == SHA1 || algorithmName == SHA256 || algorithmName == SHA384 || algorithmName == SHA512
}

func isRSAAlgorithm(algorithmName string) bool {
return algorithmName == RSASsaPkcs1v15 || algorithmName == RSAPss || algorithmName == RSAOaep
}

// hasAlg an internal interface that helps us to identify
// if a given object has an algorithm method.
type hasAlg interface {
Expand Down
3 changes: 3 additions & 0 deletions webcrypto/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ const (
// QuotaExceededError is the error thrown if the byteLength of a typedArray
// exceeds 65,536.
QuotaExceededError = "QuotaExceededError"

// NotImplemented means that we have not implemented the feature yet.
NotImplemented = "NotImplemented"
)

// Error represents a custom error emitted by the
Expand Down
135 changes: 135 additions & 0 deletions webcrypto/jwk.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"crypto/ecdh"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rsa"
"encoding/json"
"errors"
"fmt"
Expand Down Expand Up @@ -290,3 +291,137 @@ func importECDHJWK(_ EllipticCurveKind, jsonKeyData []byte) (any, CryptoKeyType,
return nil, UnknownCryptoKeyType, errors.New("input isn't a valid ECDH key")
}
}

type rsaJWK struct {
Kty string `json:"kty"` // Key Type
N string `json:"n"` // Modulus
E string `json:"e"` // Exponent
D string `json:"d,omitempty"` // Private exponent
P string `json:"p,omitempty"` // First prime factor
Q string `json:"q,omitempty"` // Second prime factor
Dp string `json:"dp,omitempty"` // Exponent1
Dq string `json:"dq,omitempty"` // Exponent2
Qi string `json:"qi,omitempty"` // Coefficient
}

func (jwk *rsaJWK) validate() error {
if jwk.Kty != "RSA" {
return fmt.Errorf("invalid key type: %s", jwk.Kty)
}

if jwk.N == "" {
return errors.New("modulus (n) is required")
}

if jwk.E == "" {
return errors.New("exponent (e) is required")
}

// TODO: validate the rest of the fields

return nil
}

func importRSAJWK(jsonKeyData []byte) (any, CryptoKeyType, error) {
var jwk rsaJWK
if err := json.Unmarshal(jsonKeyData, &jwk); err != nil {
return nil, UnknownCryptoKeyType, fmt.Errorf("failed to parse input as RSA JWK key: %w", err)
}

if err := jwk.validate(); err != nil {
return nil, UnknownCryptoKeyType, fmt.Errorf("invalid RSA JWK key: %w", err)
}

// Decode the various key components
nBytes, err := base64URLDecode(jwk.N)
if err != nil {
return nil, UnknownCryptoKeyType, fmt.Errorf("failed to decode modulus: %w", err)
}
eBytes, err := base64URLDecode(jwk.E)
if err != nil {
return nil, UnknownCryptoKeyType, fmt.Errorf("failed to decode exponent: %w", err)
}

// Convert exponent to an integer
eInt := new(big.Int).SetBytes(eBytes).Int64()

pubKey := rsa.PublicKey{
N: new(big.Int).SetBytes(nBytes),
E: int(eInt),
}

// If the private exponent is missing, return the public key
if jwk.D == "" {
return pubKey, PublicCryptoKeyType, nil
}

dBytes, err := base64URLDecode(jwk.D)
if err != nil {
return nil, UnknownCryptoKeyType, fmt.Errorf("failed to decode private exponent: %w", err)
}
pBytes, err := base64URLDecode(jwk.P)
if err != nil {
return nil, UnknownCryptoKeyType, fmt.Errorf("failed to decode first prime factor: %w", err)
}
qBytes, err := base64URLDecode(jwk.Q)
if err != nil {
return nil, UnknownCryptoKeyType, fmt.Errorf("failed to decode second prime factor: %w", err)
}
dpBytes, err := base64URLDecode(jwk.Dp)
if err != nil {
return nil, UnknownCryptoKeyType, fmt.Errorf("failed to decode first exponent: %w", err)
}
dqBytes, err := base64URLDecode(jwk.Dq)
if err != nil {
return nil, UnknownCryptoKeyType, fmt.Errorf("failed to decode second exponent: %w", err)
}
qiBytes, err := base64URLDecode(jwk.Qi)
if err != nil {
return nil, UnknownCryptoKeyType, fmt.Errorf("failed to decode coefficient: %w", err)
}

privKey := &rsa.PrivateKey{
PublicKey: pubKey,
D: new(big.Int).SetBytes(dBytes),
Primes: []*big.Int{
new(big.Int).SetBytes(pBytes),
new(big.Int).SetBytes(qBytes),
},
Precomputed: rsa.PrecomputedValues{
Dp: new(big.Int).SetBytes(dpBytes),
Dq: new(big.Int).SetBytes(dqBytes),
Qinv: new(big.Int).SetBytes(qiBytes),
},
}

err = privKey.Validate()
if err != nil {
return nil, UnknownCryptoKeyType, fmt.Errorf("failed to validate private key: %w", err)
}

return privKey, PrivateCryptoKeyType, nil
}

func exportRSAJWK(key *CryptoKey) (interface{}, error) {
exported := &JsonWebKey{}
exported.Set("kty", "RSA")

switch rsaKey := key.handle.(type) {
case *rsa.PrivateKey:
exported.Set("n", base64URLEncode(rsaKey.N.Bytes()))
exported.Set("e", base64URLEncode(big.NewInt(int64(rsaKey.E)).Bytes()))
exported.Set("d", base64URLEncode(rsaKey.D.Bytes()))
exported.Set("p", base64URLEncode(rsaKey.Primes[0].Bytes()))
exported.Set("q", base64URLEncode(rsaKey.Primes[1].Bytes()))
exported.Set("dp", base64URLEncode(rsaKey.Precomputed.Dp.Bytes()))
exported.Set("dq", base64URLEncode(rsaKey.Precomputed.Dq.Bytes()))
exported.Set("qi", base64URLEncode(rsaKey.Precomputed.Qinv.Bytes()))
case rsa.PublicKey:
exported.Set("n", base64URLEncode(rsaKey.N.Bytes()))
exported.Set("e", base64URLEncode(big.NewInt(int64(rsaKey.E)).Bytes()))
default:
return nil, fmt.Errorf("key's handle isn't an RSA public/private key, got: %T", key.handle)
}

return exported, nil
}
6 changes: 5 additions & 1 deletion webcrypto/key.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,10 @@ func newKeyGenerator(rt *sobek.Runtime, normalized Algorithm, params sobek.Value
kg, err = newHMACKeyGenParams(rt, normalized, params)
case ECDH, ECDSA:
kg, err = newECKeyGenParams(rt, normalized, params)
case RSASsaPkcs1v15, RSAPss, RSAOaep:
kg, err = newRsaHashedKeyGenParams(rt, normalized, params)
default:
return nil, errors.New("key generation not implemented for algorithm " + normalized.Name)
return nil, NewError(NotImplemented, "unsupported algorithm for key generation: "+normalized.Name)
}

if err != nil {
Expand All @@ -216,6 +218,8 @@ func newKeyImporter(rt *sobek.Runtime, normalized Algorithm, params sobek.Value)
ki, err = newHMACImportParams(rt, normalized, params)
case ECDH, ECDSA:
ki, err = newEcKeyImportParams(rt, normalized, params)
case RSASsaPkcs1v15, RSAPss, RSAOaep:
ki, err = newRsaHashedImportParams(rt, normalized, params)
default:
return nil, errors.New("key import not implemented for algorithm " + normalized.Name)
}
Expand Down
13 changes: 4 additions & 9 deletions webcrypto/params.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,7 @@ type PBKDF2Params struct {
// RSAHashedKeyGenParams represents the object that should be passed as the algorithm
// parameter into `SubtleCrypto.GenerateKey`, when generating an RSA key pair.
type RSAHashedKeyGenParams struct {
// Name should be set to AlgorithmKindRsassPkcs1v15,
// AlgorithmKindRsaPss, or AlgorithmKindRsaOaep.
Name AlgorithmIdentifier
Algorithm

// ModulusLength holds (a Number) the length of the RSA modulus, in bits.
// This should be at least 2048. Some organizations are now recommending
Expand All @@ -105,23 +103,20 @@ type RSAHashedKeyGenParams struct {
// Hash represents the name of the digest function to use. You can
// use any of the following: DigestKindSha256, DigestKindSha384,
// or DigestKindSha512.
Hash string
Hash any
}

// RSAHashedImportParams represents the object that should be passed as the
// algorithm parameter into `SubtleCrypto.ImportKey` or `SubtleCrypto.UnwrapKey`, when
// importing any RSA-based key pair: that is, when the algorithm is identified as any
// of RSASSA-PKCS1-v1_5, RSA-PSS, or RSA-OAEP.
type RSAHashedImportParams struct {
// Name should be set to AlgorithmKindRsassPkcs1v15,
// AlgorithmKindRsaPss, or AlgorithmKindRsaOaep depending
// on the algorithm you want to use.
Name string
Algorithm

// Hash represents the name of the digest function to use.
// Note that although you can technically pass SHA-1 here, this is strongly
// discouraged as it is considered vulnerable.
Hash AlgorithmIdentifier
Hash any
}

// RSAOaepParams represents the object that should be passed as the algorithm parameter
Expand Down
Loading
Loading