package jose import ( "bytes" "context" "fmt" "io" "os/exec" "strings" "sync" "testing" "github.com/lestrrat-go/jwx/internal/jwxtest" "github.com/pkg/errors" ) var executablePath string var muExecutablePath sync.RWMutex func init() { findExecutable() } func SetExecutable(path string) { muExecutablePath.Lock() defer muExecutablePath.Unlock() executablePath = path } func findExecutable() { p, err := exec.LookPath("jose") if err == nil { SetExecutable(p) } } func ExecutablePath() string { muExecutablePath.RLock() defer muExecutablePath.RUnlock() return executablePath } func Available() bool { muExecutablePath.RLock() defer muExecutablePath.RUnlock() return executablePath != "" } func RunJoseCommand(ctx context.Context, t *testing.T, args []string, outw, errw io.Writer) error { var errout bytes.Buffer var capout bytes.Buffer cmd := exec.CommandContext(ctx, ExecutablePath(), args...) if outw == nil { cmd.Stdout = &capout } else { cmd.Stdout = io.MultiWriter(outw, &capout) } if errw == nil { cmd.Stderr = &errout } else { cmd.Stderr = io.MultiWriter(outw, &errout) } t.Logf("Executing `%s %s`\n", ExecutablePath(), strings.Join(args, " ")) if err := cmd.Run(); err != nil { t.Logf(`failed to execute command: %s`, err) if capout.Len() > 0 { t.Logf("captured output: %s", capout.String()) } if errout.Len() > 0 { t.Logf("captured error: %s", errout.String()) } return errors.Wrap(err, `failed to execute command`) } return nil } // GenerateJwk creates a new key using the jose tool, and returns its filename and // a cleanup function. // The caller is responsible for calling the cleanup // function and make sure all resources are released func GenerateJwk(ctx context.Context, t *testing.T, template string) (string, func(), error) { t.Helper() file, cleanup, err := jwxtest.CreateTempFile("jwx-jose-key-*.jwk") if err != nil { return "", nil, errors.Wrap(err, "failed to create temporary file") } if err := RunJoseCommand(ctx, t, []string{"jwk", "gen", "-i", template, "-o", file.Name()}, nil, nil); err != nil { return "", nil, errors.Wrap(err, `failed to generate key`) } return file.Name(), cleanup, nil } // EncryptJwe creates an encrypted JWE message and returns its filename and // a cleanup function. // The caller is responsible for calling the cleanup // function and make sure all resources are released func EncryptJwe(ctx context.Context, t *testing.T, payload []byte, alg string, keyfile string, enc string, compact bool) (string, func(), error) { t.Helper() var arg string if alg == "dir" { arg = fmt.Sprintf(`{"protected":{"alg":"dir","enc":"%s"}}`, enc) } else { arg = fmt.Sprintf(`{"protected":{"enc":"%s"}}`, enc) } cmdargs := []string{"jwe", "enc", "-k", keyfile, "-i", arg} if compact { cmdargs = append(cmdargs, "-c") } var pfile string if len(payload) > 0 { fn, pcleanup, perr := jwxtest.WriteFile("jwx-jose-payload-*", bytes.NewReader(payload)) if perr != nil { return "", nil, errors.Wrap(perr, `failed to write payload to file`) } cmdargs = append(cmdargs, "-I", fn) pfile = fn defer pcleanup() } ofile, ocleanup, oerr := jwxtest.CreateTempFile(`jwx-jose-key-*.jwe`) if oerr != nil { return "", nil, errors.Wrap(oerr, "failed to create temporary file") } cmdargs = append(cmdargs, "-o", ofile.Name()) if err := RunJoseCommand(ctx, t, cmdargs, nil, nil); err != nil { defer ocleanup() if pfile != "" { jwxtest.DumpFile(t, pfile) } jwxtest.DumpFile(t, keyfile) return "", nil, errors.Wrap(err, `failed to encrypt message`) } return ofile.Name(), ocleanup, nil } func DecryptJwe(ctx context.Context, t *testing.T, cfile, kfile string) ([]byte, error) { t.Helper() cmdargs := []string{"jwe", "dec", "-i", cfile, "-k", kfile} var output bytes.Buffer if err := RunJoseCommand(ctx, t, cmdargs, &output, nil); err != nil { jwxtest.DumpFile(t, cfile) jwxtest.DumpFile(t, kfile) return nil, errors.Wrap(err, `failed to decrypt message`) } return output.Bytes(), nil } func FmtJwe(ctx context.Context, t *testing.T, data []byte) ([]byte, error) { t.Helper() fn, pcleanup, perr := jwxtest.WriteFile("jwx-jose-fmt-data-*", bytes.NewReader(data)) if perr != nil { return nil, errors.Wrap(perr, `failed to write data to file`) } defer pcleanup() cmdargs := []string{"jwe", "fmt", "-i", fn} var output bytes.Buffer if err := RunJoseCommand(ctx, t, cmdargs, &output, nil); err != nil { jwxtest.DumpFile(t, fn) return nil, errors.Wrap(err, `failed to format JWE message`) } return output.Bytes(), nil } // SignJws signs a message and returns its filename and // a cleanup function. // The caller is responsible for calling the cleanup // function and make sure all resources are released func SignJws(ctx context.Context, t *testing.T, payload []byte, keyfile string, compact bool) (string, func(), error) { t.Helper() cmdargs := []string{"jws", "sig", "-k", keyfile} if compact { cmdargs = append(cmdargs, "-c") } var pfile string if len(payload) > 0 { fn, pcleanup, perr := jwxtest.WriteFile("jwx-jose-payload-*", bytes.NewReader(payload)) if perr != nil { return "", nil, errors.Wrap(perr, `failed to write payload to file`) } cmdargs = append(cmdargs, "-I", fn) pfile = fn defer pcleanup() } ofile, ocleanup, oerr := jwxtest.CreateTempFile(`jwx-jose-sig-*.jws`) if oerr != nil { return "", nil, errors.Wrap(oerr, "failed to create temporary file") } cmdargs = append(cmdargs, "-o", ofile.Name()) if err := RunJoseCommand(ctx, t, cmdargs, nil, nil); err != nil { defer ocleanup() if pfile != "" { jwxtest.DumpFile(t, pfile) } jwxtest.DumpFile(t, keyfile) return "", nil, errors.Wrap(err, `failed to sign message`) } return ofile.Name(), ocleanup, nil } func VerifyJws(ctx context.Context, t *testing.T, cfile, kfile string) ([]byte, error) { t.Helper() cmdargs := []string{"jws", "ver", "-i", cfile, "-k", kfile, "-O-"} var output bytes.Buffer if err := RunJoseCommand(ctx, t, cmdargs, &output, nil); err != nil { jwxtest.DumpFile(t, cfile) jwxtest.DumpFile(t, kfile) return nil, errors.Wrap(err, `failed to decrypt message`) } return output.Bytes(), nil }