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 }