Skip to content

Commit

Permalink
restructured jwt internals and added custom error types
Browse files Browse the repository at this point in the history
  • Loading branch information
MichaelFraser99 committed Sep 10, 2023
1 parent ea1ed18 commit 0bb4d7f
Show file tree
Hide file tree
Showing 17 changed files with 394 additions and 470 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,11 @@ Signature returns the signature of the provided token used to verify it
func (s *SdJwt) Token() string
```
Token returns the JWT token as it was received

### Errors
This package defines the following errors:
- InvalidToken - The provided token is malformed in some way
- InvalidSignature - The signature of the token is invalid
- UnsupportedAlgorithm - The algorithm used to sign the token is not supported
- InvalidPublicKey - The provided public key is invalid
- SigningError - An error occurred while signing the token
9 changes: 9 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#TODO
- SD-Jwt creation
- KB-Jwt creation
- Option to provide public key when key-binding is used without cnf claim
- Validation 'profiles' i.e. validate as holder or verifier
- Full kb-jwt validation - exp, iat
- Option to provide "additional validation" for sd-jwt validation
- Option to provide "additional validation" for kb-jwt validation
- Function to retrieve kb-jwt contents as map
9 changes: 9 additions & 0 deletions internal/error/error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package error

type InvalidToken struct {
Message string
}

func (e *InvalidToken) Error() string {
return e.Message
}
125 changes: 125 additions & 0 deletions internal/jose/algorithms/common/elliptic-curve.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package common

import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
e "github.com/MichaelFraser99/go-sd-jwt/internal/jose/error"
"math/big"
)

type PublicKey struct {
Kty string `json:"kty"`
Crv string `json:"crv"`
X string `json:"x"`
Y string `json:"y"`
}

func (pubKey *PublicKey) Equal(x PublicKey) bool {
if pubKey.X == x.X && pubKey.Y == x.Y && pubKey.Kty == x.Kty && pubKey.Crv == x.Crv {
return true
}
return false
}

func NewPublicKeyFromJson(publicKeyJson string, curve elliptic.Curve) (*ecdsa.PublicKey, error) {
var publicKey PublicKey
err := json.Unmarshal([]byte(publicKeyJson), &publicKey)
if err != nil {
return nil, &e.InvalidPublicKey{Message: fmt.Sprintf("provided public key json isn't valid es256 public key: %s", err.Error())}
}

xBytes, err := base64.RawURLEncoding.DecodeString(publicKey.X)
if err != nil {
return nil, &e.InvalidPublicKey{Message: fmt.Sprintf("error decoding provided public key: %s", err.Error())}
}

yBytes, err := base64.RawURLEncoding.DecodeString(publicKey.Y)
if err != nil {
return nil, &e.InvalidPublicKey{Message: fmt.Sprintf("error decoding provided public key: %s", err.Error())}
}

pk := &ecdsa.PublicKey{
Curve: curve,
X: big.NewInt(0).SetBytes(xBytes),
Y: big.NewInt(0).SetBytes(yBytes),
}
return pk, nil
}

func ExtractRSFromSignature(signature string, keySize int) (*big.Int, *big.Int, error) {
decodedSignature, err := base64.RawURLEncoding.DecodeString(signature)
if err != nil {
return nil, nil, &e.InvalidSignature{Message: fmt.Sprintf("error decoding signature: %s", err.Error())}
}

if len(decodedSignature) != keySize {
return nil, nil, &e.InvalidSignature{Message: fmt.Sprintf("signature should be %d bytes for given algorithm", keySize)}
}
rb := decodedSignature[:keySize/2]
sb := decodedSignature[keySize/2:]

r := big.NewInt(0).SetBytes(rb)
s := big.NewInt(0).SetBytes(sb)

return r, s, nil
}

func GenerateToken(alg string, header map[string]string, body map[string]any) (*string, error) {
h := header
h["alg"] = alg

headerBytes, err := json.Marshal(h)
if err != nil {
return nil, &e.SigningError{Message: fmt.Sprintf("failed to marshal header to bytes: %s", err.Error())}
}
base64Header := base64.RawURLEncoding.EncodeToString(headerBytes)

bodyBytes, err := json.Marshal(body)
if err != nil {
return nil, &e.SigningError{Message: fmt.Sprintf("failed to marshal body to bytes: %s", err.Error())}
}
base64Body := base64.RawURLEncoding.EncodeToString(bodyBytes)

token := fmt.Sprintf("%s.%s", base64Header, base64Body)
return &token, nil
}

func SignToken(token string, pk ecdsa.PrivateKey, digest []byte, keySize int) (*string, error) {
r, s, err := ecdsa.Sign(rand.Reader, &pk, digest)
if err != nil {
return nil, &e.SigningError{Message: fmt.Sprintf("failed to sign token: %s", err.Error())}
}

sigBytes := make([]byte, keySize)

r.FillBytes(sigBytes[0 : keySize/2])
s.FillBytes(sigBytes[keySize/2:])

base64Sig := base64.RawURLEncoding.EncodeToString(sigBytes)
signedToken := fmt.Sprintf("%s.%s", token, base64Sig)
return &signedToken, nil
}

func GeneratePublicKey(pk ecdsa.PrivateKey, curveName string, keySize int) PublicKey {
cryptoPubKey := pk.PublicKey

xb := make([]byte, keySize/2)
yb := make([]byte, keySize/2)

cryptoPubKey.X.FillBytes(xb)
cryptoPubKey.Y.FillBytes(yb)

x := base64.RawURLEncoding.EncodeToString(xb)
y := base64.RawURLEncoding.EncodeToString(yb)

return PublicKey{
Kty: "EC",
Crv: curveName,
X: x,
Y: y,
}
}
57 changes: 57 additions & 0 deletions internal/jose/algorithms/es256/ES256.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package es256

import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"fmt"
"github.com/MichaelFraser99/go-sd-jwt/internal/jose/algorithms/common"
e "github.com/MichaelFraser99/go-sd-jwt/internal/jose/error"
)

type ES256 struct{}

func (signer *ES256) ValidateSignature(token, signature string, publicKeyJson string) (bool, error) {
pk, err := common.NewPublicKeyFromJson(publicKeyJson, elliptic.P256())
if err != nil {
return false, err
}

bodyHash := sha256.Sum256([]byte(token))

r, s, err := common.ExtractRSFromSignature(signature, 64)
if err != nil {
return false, err
}

return ecdsa.Verify(pk, bodyHash[:], r, s), nil
}

func (signer *ES256) Sign(body map[string]interface{}, headerKeys map[string]string) (*string, crypto.PrivateKey, crypto.PublicKey, error) {
curve := elliptic.P256()
pk, err := ecdsa.GenerateKey(curve, rand.Reader)
if err != nil {
return nil, nil, nil, &e.SigningError{Message: fmt.Sprintf("failed to generate key: %s", err.Error())}
}

token, err := common.GenerateToken("ES256", headerKeys, body)
if err != nil {
return nil, nil, nil, err
}

digest := sha256.Sum256([]byte(*token))

signedToken, err := common.SignToken(*token, *pk, digest[:], 64)
if err != nil {
return nil, nil, nil, err
}

pubKey := common.GeneratePublicKey(*pk, "P-256", 64)

return signedToken, pk, pubKey, nil
}
func (signer *ES256) SignWithKey(body map[string]interface{}, headerKeys map[string]string, privateKey string) (*string, error) {
return nil, nil //todo
}
File renamed without changes.
57 changes: 57 additions & 0 deletions internal/jose/algorithms/es384/ES384.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package es384

import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha512"
"fmt"
"github.com/MichaelFraser99/go-sd-jwt/internal/jose/algorithms/common"
e "github.com/MichaelFraser99/go-sd-jwt/internal/jose/error"
)

type ES384 struct{}

func (signer *ES384) ValidateSignature(token, signature string, publicKeyJson string) (bool, error) {
pk, err := common.NewPublicKeyFromJson(publicKeyJson, elliptic.P384())
if err != nil {
return false, err
}

bodyHash := sha512.Sum384([]byte(token))

r, s, err := common.ExtractRSFromSignature(signature, 96)
if err != nil {
return false, err
}

return ecdsa.Verify(pk, bodyHash[:], r, s), nil
}

func (signer *ES384) Sign(body map[string]interface{}, headerKeys map[string]string) (*string, crypto.PrivateKey, crypto.PublicKey, error) {
curve := elliptic.P384()
pk, err := ecdsa.GenerateKey(curve, rand.Reader)
if err != nil {
return nil, nil, nil, &e.SigningError{Message: fmt.Sprintf("failed to generate key: %s", err.Error())}
}

token, err := common.GenerateToken("ES384", headerKeys, body)
if err != nil {
return nil, nil, nil, err
}

digest := sha512.Sum384([]byte(*token))

signedToken, err := common.SignToken(*token, *pk, digest[:], 96)
if err != nil {
return nil, nil, nil, err
}

pubKey := common.GeneratePublicKey(*pk, "P-384", 96)

return signedToken, pk, pubKey, nil
}
func (signer *ES384) SignWithKey(body map[string]interface{}, headerKeys map[string]string, privateKey string) (*string, error) {
return nil, nil //todo
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func TestES384_Sign(t *testing.T) {
}
jsonPk, err := json.Marshal(publicKey)
if err != nil {
t.Error("no error should be thrown", err)
t.Error("no error should be thrown:", err)
t.FailNow()
}
t.Log(*token)
Expand All @@ -69,7 +69,7 @@ func TestES384_Sign(t *testing.T) {
components := strings.Split(*token, ".")
valid, err := es384.ValidateSignature(strings.Join(components[0:2], "."), components[2], string(jsonPk))
if err != nil {
t.Error("no error should be thrown", err)
t.Error("no error should be thrown:", err)
}
if !valid {
t.Error("signature is not valid")
Expand Down
57 changes: 57 additions & 0 deletions internal/jose/algorithms/es512/ES512.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package es512

import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha512"
"fmt"
"github.com/MichaelFraser99/go-sd-jwt/internal/jose/algorithms/common"
e "github.com/MichaelFraser99/go-sd-jwt/internal/jose/error"
)

type ES512 struct{}

func (signer *ES512) ValidateSignature(token, signature string, publicKeyJson string) (bool, error) {
pk, err := common.NewPublicKeyFromJson(publicKeyJson, elliptic.P521())
if err != nil {
return false, err
}

bodyHash := sha512.Sum512([]byte(token))

r, s, err := common.ExtractRSFromSignature(signature, 132)
if err != nil {
return false, err
}

return ecdsa.Verify(pk, bodyHash[:], r, s), nil
}

func (signer *ES512) Sign(body map[string]interface{}, headerKeys map[string]string) (*string, crypto.PrivateKey, crypto.PublicKey, error) {
curve := elliptic.P521()
pk, err := ecdsa.GenerateKey(curve, rand.Reader)
if err != nil {
return nil, nil, nil, &e.SigningError{Message: fmt.Sprintf("failed to generate key: %s", err.Error())}
}

token, err := common.GenerateToken("ES512", headerKeys, body)
if err != nil {
return nil, nil, nil, err
}

digest := sha512.Sum512([]byte(*token))

signedToken, err := common.SignToken(*token, *pk, digest[:], 132)
if err != nil {
return nil, nil, nil, err
}

pubKey := common.GeneratePublicKey(*pk, "P-521", 132)

return signedToken, pk, pubKey, nil
}
func (signer *ES512) SignWithKey(body map[string]interface{}, headerKeys map[string]string, privateKey string) (*string, error) {
return nil, nil //todo
}
File renamed without changes.
33 changes: 33 additions & 0 deletions internal/jose/error/error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package error

type InvalidSignature struct {
Message string
}

func (e *InvalidSignature) Error() string {
return e.Message
}

type UnsupportedAlgorithm struct {
Message string
}

func (e *UnsupportedAlgorithm) Error() string {
return e.Message
}

type InvalidPublicKey struct {
Message string
}

func (e *InvalidPublicKey) Error() string {
return e.Message
}

type SigningError struct {
Message string
}

func (e *SigningError) Error() string {
return e.Message
}
Loading

0 comments on commit 0bb4d7f

Please sign in to comment.