From c6a6a5f45b29cdfce777aa2478270a0b763f7058 Mon Sep 17 00:00:00 2001
From: Paul Greenberg <greenpau@outlook.com>
Date: Tue, 8 Feb 2022 14:17:53 -0500
Subject: [PATCH] cmd: add handling of HS keys

Signed-off-by: Paul Greenberg <greenpau@outlook.com>
---
 cmd/jwt/README.md | 88 ++++++++++++++++++++++++++++++++++++++++++++---
 cmd/jwt/main.go   | 36 +++++++++++++++++--
 2 files changed, 117 insertions(+), 7 deletions(-)

diff --git a/cmd/jwt/README.md b/cmd/jwt/README.md
index 4388e5f9..ed515e4d 100644
--- a/cmd/jwt/README.md
+++ b/cmd/jwt/README.md
@@ -1,19 +1,97 @@
-`jwt` command-line tool
-=======================
+# `jwt` command-line tool
 
 This is a simple tool to sign, verify and show JSON Web Tokens from
 the command line.
 
+## Getting Started
+
 The following will create and sign a token, then verify it and output the original claims:
 
-     echo {\"foo\":\"bar\"} | ./jwt -key ../../test/sample_key -alg RS256 -sign - | ./jwt -key ../../test/sample_key.pub -alg RS256 -verify -
+```bash
+echo {\"foo\":\"bar\"} | ./jwt -key ../../test/sample_key -alg RS256 -sign - | ./jwt -key ../../test/sample_key.pub -alg RS256 -verify -
+```
 
 Key files should be in PEM format. Other formats are not supported by this tool.
 
 To simply display a token, use:
 
-    echo $JWT | ./jwt -show -
+```bash
+echo $JWT | ./jwt -show -
+```
 
 You can install this tool with the following command:
 
-     go install github.com/golang-jwt/jwt/v4/cmd/jwt
\ No newline at end of file
+```bash
+go install github.com/golang-jwt/jwt/v4/cmd/jwt
+```
+
+## Sign/Verify with Shared Secret
+
+First, create a JSON document with token payload, e.g. `~/experimental/jwt/data`.
+
+```json
+{
+    "email": "jsmith@foo.bar",
+    "aud": "foo.bar",
+    "exp": 2559489932,
+    "iat": 1612805132,
+    "iss": "foo.bar",
+    "sub": "jsmith"
+}
+```
+
+Then, create a file with shared secret key, e.g. `~/experimental/jwt/token.key`.
+
+```
+foobarbaz
+```
+
+Next, sign the token:
+
+```bash
+./jwt -key ~/experimental/jwt/token.key -alg HS512 -sign ~/experimental/jwt/data > ~/experimental/jwt/token.jwt
+```
+
+After that, review the token:
+
+```bash
+./jwt -show ~/experimental/jwt/token.jwt
+```
+
+The expected output follows:
+
+```
+Header:
+{
+    "alg": "HS512",
+    "typ": "JWT"
+}
+Claims:
+{
+    "aud": "foo.bar",
+    "email": "jsmith@foo.bar",
+    "exp": 2559489932,
+    "iat": 1612805132,
+    "iss": "foo.bar",
+    "sub": "jsmith"
+}
+```
+
+Subsequently, validate the token:
+
+```bash
+./jwt -key ~/experimental/jwt/token.key -alg HS512 -verify ~/experimental/jwt/token.jwt
+```
+
+The expected output follows:
+
+```
+{
+    "aud": "foo.bar",
+    "email": "jsmith@foo.bar",
+    "exp": 2559489932,
+    "iat": 1612805132,
+    "iss": "foo.bar",
+    "sub": "jsmith"
+}
+```
diff --git a/cmd/jwt/main.go b/cmd/jwt/main.go
index 8706ab01..ce5e7b32 100644
--- a/cmd/jwt/main.go
+++ b/cmd/jwt/main.go
@@ -7,6 +7,7 @@
 package main
 
 import (
+	"bytes"
 	"encoding/json"
 	"flag"
 	"fmt"
@@ -16,6 +17,7 @@ import (
 	"regexp"
 	"sort"
 	"strings"
+	"unicode"
 
 	"github.com/golang-jwt/jwt/v4"
 )
@@ -142,6 +144,8 @@ func verifyToken() error {
 			return jwt.ParseRSAPublicKeyFromPEM(data)
 		} else if isEd() {
 			return jwt.ParseEdPublicKeyFromPEM(data)
+		} else if isHs() {
+			return parseHSKey(data)
 		}
 		return data, nil
 	})
@@ -196,9 +200,19 @@ func signToken() error {
 
 	// get the key
 	var key interface{}
-	if isNone() {
+	switch {
+	case isNone():
 		key = jwt.UnsafeAllowNoneSignatureType
-	} else {
+	case isHs():
+		kb, err := loadData(*flagKey)
+		if err != nil {
+			return fmt.Errorf("couldn't read key: %w", err)
+		}
+		key, err = parseHSKey(kb)
+		if err != nil {
+			return err
+		}
+	default:
 		key, err = loadData(*flagKey)
 		if err != nil {
 			return fmt.Errorf("couldn't read key: %w", err)
@@ -292,6 +306,10 @@ func showToken() error {
 	return nil
 }
 
+func isHs() bool {
+	return strings.HasPrefix(*flagAlg, "HS")
+}
+
 func isEs() bool {
 	return strings.HasPrefix(*flagAlg, "ES")
 }
@@ -342,3 +360,17 @@ func (l ArgList) Set(arg string) error {
 	l[parts[0]] = parts[1]
 	return nil
 }
+
+func parseHSKey(b []byte) ([]byte, error) {
+	if len(b) == 0 {
+		return nil, fmt.Errorf("shared key is empty")
+	}
+	f := func(c rune) bool {
+		return unicode.IsSpace(c)
+	}
+	i := bytes.IndexFunc(b, f)
+	if i < 0 {
+		return b, nil
+	}
+	return b[:i], nil
+}