github.com/lestrrat-go/jwx/v2@v2.0.21/internal/jose/jose.go (about)

     1  package jose
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"context"
     7  	"fmt"
     8  	"io"
     9  	"os/exec"
    10  	"strings"
    11  	"sync"
    12  	"testing"
    13  
    14  	"github.com/lestrrat-go/jwx/v2/internal/jwxtest"
    15  )
    16  
    17  var executablePath string
    18  var muExecutablePath sync.RWMutex
    19  
    20  func init() {
    21  	findExecutable()
    22  }
    23  
    24  func SetExecutable(path string) {
    25  	muExecutablePath.Lock()
    26  	defer muExecutablePath.Unlock()
    27  	executablePath = path
    28  }
    29  
    30  func findExecutable() {
    31  	p, err := exec.LookPath("jose")
    32  	if err == nil {
    33  		SetExecutable(p)
    34  	}
    35  }
    36  
    37  func ExecutablePath() string {
    38  	muExecutablePath.RLock()
    39  	defer muExecutablePath.RUnlock()
    40  
    41  	return executablePath
    42  }
    43  
    44  func Available() bool {
    45  	muExecutablePath.RLock()
    46  	defer muExecutablePath.RUnlock()
    47  
    48  	return executablePath != ""
    49  }
    50  
    51  func RunJoseCommand(ctx context.Context, t *testing.T, args []string, outw, errw io.Writer) error {
    52  	var errout bytes.Buffer
    53  	var capout bytes.Buffer
    54  
    55  	cmd := exec.CommandContext(ctx, ExecutablePath(), args...)
    56  	if outw == nil {
    57  		cmd.Stdout = &capout
    58  	} else {
    59  		cmd.Stdout = io.MultiWriter(outw, &capout)
    60  	}
    61  
    62  	if errw == nil {
    63  		cmd.Stderr = &errout
    64  	} else {
    65  		cmd.Stderr = io.MultiWriter(outw, &errout)
    66  	}
    67  
    68  	t.Logf("Executing `%s %s`\n", ExecutablePath(), strings.Join(args, " "))
    69  	if err := cmd.Run(); err != nil {
    70  		t.Logf(`failed to execute command: %s`, err)
    71  
    72  		if capout.Len() > 0 {
    73  			t.Logf("captured output: %s", capout.String())
    74  		}
    75  
    76  		if errout.Len() > 0 {
    77  			t.Logf("captured error: %s", errout.String())
    78  		}
    79  
    80  		return fmt.Errorf(`failed to execute command: %w`, err)
    81  	}
    82  
    83  	return nil
    84  }
    85  
    86  type AlgorithmSet struct {
    87  	data map[string]struct{}
    88  }
    89  
    90  func NewAlgorithmSet() *AlgorithmSet {
    91  	return &AlgorithmSet{
    92  		data: make(map[string]struct{}),
    93  	}
    94  }
    95  
    96  func (set *AlgorithmSet) Add(s string) {
    97  	set.data[s] = struct{}{}
    98  }
    99  
   100  func (set *AlgorithmSet) Has(s string) bool {
   101  	_, ok := set.data[s]
   102  	return ok
   103  }
   104  
   105  func Algorithms(ctx context.Context, t *testing.T) (*AlgorithmSet, error) {
   106  	var buf bytes.Buffer
   107  	if err := RunJoseCommand(ctx, t, []string{"alg"}, &buf, nil); err != nil {
   108  		return nil, fmt.Errorf(`failed to generate jose tool's supported algorithms: %w`, err)
   109  	}
   110  
   111  	set := NewAlgorithmSet()
   112  
   113  	scanner := bufio.NewScanner(&buf)
   114  	for scanner.Scan() {
   115  		alg := scanner.Text()
   116  		set.Add(alg)
   117  	}
   118  	return set, nil
   119  }
   120  
   121  // GenerateJwk creates a new key using the jose tool, and returns its filename and
   122  // a cleanup function.
   123  // The caller is responsible for calling the cleanup
   124  // function and make sure all resources are released
   125  func GenerateJwk(ctx context.Context, t *testing.T, template string) (string, func(), error) {
   126  	t.Helper()
   127  
   128  	file, cleanup, err := jwxtest.CreateTempFile("jwx-jose-key-*.jwk")
   129  	if err != nil {
   130  		return "", nil, fmt.Errorf(`failed to create temporary file: %w`, err)
   131  	}
   132  
   133  	if err := RunJoseCommand(ctx, t, []string{"jwk", "gen", "-i", template, "-o", file.Name()}, nil, nil); err != nil {
   134  		return "", nil, fmt.Errorf(`failed to generate key: %w`, err)
   135  	}
   136  
   137  	return file.Name(), cleanup, nil
   138  }
   139  
   140  // EncryptJwe creates an encrypted JWE message and returns its filename and
   141  // a cleanup function.
   142  // The caller is responsible for calling the cleanup
   143  // function and make sure all resources are released
   144  func EncryptJwe(ctx context.Context, t *testing.T, payload []byte, alg string, keyfile string, enc string, compact bool) (string, func(), error) {
   145  	t.Helper()
   146  
   147  	var arg string
   148  	if alg == "dir" {
   149  		arg = fmt.Sprintf(`{"protected":{"alg":"dir","enc":"%s"}}`, enc)
   150  	} else {
   151  		arg = fmt.Sprintf(`{"protected":{"enc":"%s"}}`, enc)
   152  	}
   153  
   154  	cmdargs := []string{"jwe", "enc", "-k", keyfile, "-i", arg}
   155  	if compact {
   156  		cmdargs = append(cmdargs, "-c")
   157  	}
   158  
   159  	var pfile string
   160  	if len(payload) > 0 {
   161  		fn, pcleanup, perr := jwxtest.WriteFile("jwx-jose-payload-*", bytes.NewReader(payload))
   162  		if perr != nil {
   163  			return "", nil, fmt.Errorf(`failed to write payload to file: %w`, perr)
   164  		}
   165  
   166  		cmdargs = append(cmdargs, "-I", fn)
   167  		pfile = fn
   168  		defer pcleanup()
   169  	}
   170  
   171  	ofile, ocleanup, oerr := jwxtest.CreateTempFile(`jwx-jose-key-*.jwe`)
   172  	if oerr != nil {
   173  		return "", nil, fmt.Errorf(`failed to create temporary file: %w`, oerr)
   174  	}
   175  
   176  	cmdargs = append(cmdargs, "-o", ofile.Name())
   177  
   178  	if err := RunJoseCommand(ctx, t, cmdargs, nil, nil); err != nil {
   179  		defer ocleanup()
   180  		if pfile != "" {
   181  			jwxtest.DumpFile(t, pfile)
   182  		}
   183  		jwxtest.DumpFile(t, keyfile)
   184  		return "", nil, fmt.Errorf(`failed to encrypt message: %w`, err)
   185  	}
   186  
   187  	return ofile.Name(), ocleanup, nil
   188  }
   189  
   190  func DecryptJwe(ctx context.Context, t *testing.T, cfile, kfile string) ([]byte, error) {
   191  	t.Helper()
   192  
   193  	cmdargs := []string{"jwe", "dec", "-i", cfile, "-k", kfile}
   194  	var output bytes.Buffer
   195  	if err := RunJoseCommand(ctx, t, cmdargs, &output, nil); err != nil {
   196  		jwxtest.DumpFile(t, cfile)
   197  		jwxtest.DumpFile(t, kfile)
   198  
   199  		return nil, fmt.Errorf(`failed to decrypt message: %w`, err)
   200  	}
   201  
   202  	return output.Bytes(), nil
   203  }
   204  
   205  func FmtJwe(ctx context.Context, t *testing.T, data []byte) ([]byte, error) {
   206  	t.Helper()
   207  
   208  	fn, pcleanup, perr := jwxtest.WriteFile("jwx-jose-fmt-data-*", bytes.NewReader(data))
   209  	if perr != nil {
   210  		return nil, fmt.Errorf(`failed to write data to file: %w`, perr)
   211  	}
   212  	defer pcleanup()
   213  
   214  	cmdargs := []string{"jwe", "fmt", "-i", fn}
   215  
   216  	var output bytes.Buffer
   217  	if err := RunJoseCommand(ctx, t, cmdargs, &output, nil); err != nil {
   218  		jwxtest.DumpFile(t, fn)
   219  
   220  		return nil, fmt.Errorf(`failed to format JWE message: %w`, err)
   221  	}
   222  
   223  	return output.Bytes(), nil
   224  }
   225  
   226  // SignJws signs a message and returns its filename and
   227  // a cleanup function.
   228  // The caller is responsible for calling the cleanup
   229  // function and make sure all resources are released
   230  func SignJws(ctx context.Context, t *testing.T, payload []byte, keyfile string, compact bool) (string, func(), error) {
   231  	t.Helper()
   232  
   233  	cmdargs := []string{"jws", "sig", "-k", keyfile}
   234  	if compact {
   235  		cmdargs = append(cmdargs, "-c")
   236  	}
   237  
   238  	var pfile string
   239  	if len(payload) > 0 {
   240  		fn, pcleanup, perr := jwxtest.WriteFile("jwx-jose-payload-*", bytes.NewReader(payload))
   241  		if perr != nil {
   242  			return "", nil, fmt.Errorf(`failed to write payload to file: %w`, perr)
   243  		}
   244  
   245  		cmdargs = append(cmdargs, "-I", fn)
   246  		pfile = fn
   247  		defer pcleanup()
   248  	}
   249  
   250  	ofile, ocleanup, oerr := jwxtest.CreateTempFile(`jwx-jose-sig-*.jws`)
   251  	if oerr != nil {
   252  		return "", nil, fmt.Errorf(`failed to create temporary file: %w`, oerr)
   253  	}
   254  
   255  	cmdargs = append(cmdargs, "-o", ofile.Name())
   256  
   257  	if err := RunJoseCommand(ctx, t, cmdargs, nil, nil); err != nil {
   258  		defer ocleanup()
   259  		if pfile != "" {
   260  			jwxtest.DumpFile(t, pfile)
   261  		}
   262  		jwxtest.DumpFile(t, keyfile)
   263  		return "", nil, fmt.Errorf(`failed to sign message: %w`, err)
   264  	}
   265  
   266  	return ofile.Name(), ocleanup, nil
   267  }
   268  
   269  func VerifyJws(ctx context.Context, t *testing.T, cfile, kfile string) ([]byte, error) {
   270  	t.Helper()
   271  
   272  	cmdargs := []string{"jws", "ver", "-i", cfile, "-k", kfile, "-O-"}
   273  	var output bytes.Buffer
   274  	if err := RunJoseCommand(ctx, t, cmdargs, &output, nil); err != nil {
   275  		jwxtest.DumpFile(t, cfile)
   276  		jwxtest.DumpFile(t, kfile)
   277  
   278  		return nil, fmt.Errorf(`failed to decrypt message: %w`, err)
   279  	}
   280  
   281  	return output.Bytes(), nil
   282  }