Initial
This commit is contained in:
195
internal/jws/token.go
Normal file
195
internal/jws/token.go
Normal file
@@ -0,0 +1,195 @@
|
||||
package jws
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
appcrypto "netisjwt/internal/crypto"
|
||||
"netisjwt/internal/statuslist"
|
||||
)
|
||||
|
||||
type StatusClaim struct {
|
||||
EncodedList string `json:"encodedList"`
|
||||
Index int `json:"index"`
|
||||
}
|
||||
|
||||
type Claims struct {
|
||||
Issuer string `json:"iss"`
|
||||
IssuedAt int64 `json:"iat"`
|
||||
ExpiresAt int64 `json:"exp"`
|
||||
Status StatusClaim `json:"status"`
|
||||
}
|
||||
|
||||
type TokenHeader struct {
|
||||
Algorithm string `json:"alg"`
|
||||
Type string `json:"typ"`
|
||||
JWK JWKValue `json:"jwk"`
|
||||
}
|
||||
|
||||
type JWKValue struct {
|
||||
Kty string `json:"kty"`
|
||||
Crv string `json:"crv"`
|
||||
X string `json:"x"`
|
||||
Y string `json:"y"`
|
||||
}
|
||||
|
||||
// SestaviTrditve pripravi JWT payload za status endpoint.
|
||||
func SestaviTrditve(issuer string, list *statuslist.StatusList, index int, now time.Time) (Claims, error) {
|
||||
encoded, err := list.KodiranSeznam()
|
||||
if err != nil {
|
||||
return Claims{}, err
|
||||
}
|
||||
return Claims{
|
||||
Issuer: issuer,
|
||||
IssuedAt: now.Unix(),
|
||||
ExpiresAt: now.Add(24 * time.Hour).Unix(),
|
||||
Status: StatusClaim{
|
||||
EncodedList: encoded,
|
||||
Index: index,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UstvariZetonIzPEM ustvari JWS compact token iz PEM kljuca.
|
||||
func UstvariZetonIzPEM(privatePEM []byte, claims Claims) (string, error) {
|
||||
privateKey, err := appcrypto.PreberiZasebniKljucPEM(privatePEM)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return UstvariZeton(privateKey, claims)
|
||||
}
|
||||
|
||||
// UstvariZeton ustvari JWS compact token iz ECDSA kljuca.
|
||||
func UstvariZeton(privateKey *ecdsa.PrivateKey, claims Claims) (string, error) {
|
||||
header := TokenHeader{
|
||||
Algorithm: "ES256",
|
||||
Type: "JWT",
|
||||
JWK: javniKljucVJWK(&privateKey.PublicKey),
|
||||
}
|
||||
|
||||
headerPart, err := kodirajDel(header)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
claimsPart, err := kodirajDel(claims)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
signingInput := headerPart + "." + claimsPart
|
||||
hash := sha256.Sum256([]byte(signingInput))
|
||||
signatureRaw, err := ecdsa.SignASN1(rand.Reader, privateKey, hash[:])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
signature := base64.RawURLEncoding.EncodeToString(signatureRaw)
|
||||
return signingInput + "." + signature, nil
|
||||
}
|
||||
|
||||
// PreveriZeton preveri podpis in vrne dekodirane trditve.
|
||||
func PreveriZeton(token string) (Claims, error) {
|
||||
parts := strings.Split(token, ".")
|
||||
if len(parts) != 3 {
|
||||
return Claims{}, errors.New("token must have 3 parts")
|
||||
}
|
||||
|
||||
headerBytes, err := base64.RawURLEncoding.DecodeString(parts[0])
|
||||
if err != nil {
|
||||
return Claims{}, err
|
||||
}
|
||||
claimsBytes, err := base64.RawURLEncoding.DecodeString(parts[1])
|
||||
if err != nil {
|
||||
return Claims{}, err
|
||||
}
|
||||
|
||||
var header TokenHeader
|
||||
if err := json.Unmarshal(headerBytes, &header); err != nil {
|
||||
return Claims{}, err
|
||||
}
|
||||
var claims Claims
|
||||
if err := json.Unmarshal(claimsBytes, &claims); err != nil {
|
||||
return Claims{}, err
|
||||
}
|
||||
|
||||
pub, err := jwkVJavniKljuc(header.JWK)
|
||||
if err != nil {
|
||||
return Claims{}, err
|
||||
}
|
||||
signature, err := base64.RawURLEncoding.DecodeString(parts[2])
|
||||
if err != nil {
|
||||
return Claims{}, err
|
||||
}
|
||||
|
||||
hash := sha256.Sum256([]byte(parts[0] + "." + parts[1]))
|
||||
if !ecdsa.VerifyASN1(pub, hash[:], signature) {
|
||||
return Claims{}, errors.New("invalid token signature")
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
// PreveriTrditve preveri veljavnost iat, exp in iss vrednosti.
|
||||
func PreveriTrditve(claims Claims, expectedIssuer string, now time.Time) error {
|
||||
if expectedIssuer != "" && claims.Issuer != expectedIssuer {
|
||||
return fmt.Errorf("invalid issuer")
|
||||
}
|
||||
nowUnix := now.Unix()
|
||||
if claims.IssuedAt > nowUnix {
|
||||
return errors.New("iat is in future")
|
||||
}
|
||||
if claims.ExpiresAt < nowUnix {
|
||||
return errors.New("token expired")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// kodirajDel serializira del tokena v Base64URL JSON.
|
||||
func kodirajDel(v any) (string, error) {
|
||||
raw, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString(raw), nil
|
||||
}
|
||||
|
||||
// javniKljucVJWK pretvori ECDSA javni kljuc v JWK zapis.
|
||||
func javniKljucVJWK(pub *ecdsa.PublicKey) JWKValue {
|
||||
return JWKValue{
|
||||
Kty: "EC",
|
||||
Crv: "P-256",
|
||||
X: base64.RawURLEncoding.EncodeToString(pub.X.FillBytes(make([]byte, 32))),
|
||||
Y: base64.RawURLEncoding.EncodeToString(pub.Y.FillBytes(make([]byte, 32))),
|
||||
}
|
||||
}
|
||||
|
||||
// jwkVJavniKljuc pretvori JWK zapis v ECDSA javni kljuc.
|
||||
func jwkVJavniKljuc(jwk JWKValue) (*ecdsa.PublicKey, error) {
|
||||
if jwk.Kty != "EC" || jwk.Crv != "P-256" {
|
||||
return nil, errors.New("unsupported key type")
|
||||
}
|
||||
xBytes, err := base64.RawURLEncoding.DecodeString(jwk.X)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
yBytes, err := base64.RawURLEncoding.DecodeString(jwk.Y)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pub := &ecdsa.PublicKey{
|
||||
Curve: elliptic.P256(),
|
||||
X: new(big.Int).SetBytes(xBytes),
|
||||
Y: new(big.Int).SetBytes(yBytes),
|
||||
}
|
||||
if !pub.Curve.IsOnCurve(pub.X, pub.Y) {
|
||||
return nil, errors.New("jwk is not on P-256 curve")
|
||||
}
|
||||
return pub, nil
|
||||
}
|
||||
48
internal/jws/token_test.go
Normal file
48
internal/jws/token_test.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package jws
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
appcrypto "netisjwt/internal/crypto"
|
||||
"netisjwt/internal/statuslist"
|
||||
)
|
||||
|
||||
func TestCreateAndVerifyToken(t *testing.T) {
|
||||
privatePEM, err := appcrypto.UstvariZasebniKljucECDSAP256PEM()
|
||||
if err != nil {
|
||||
t.Fatalf("key gen: %v", err)
|
||||
}
|
||||
|
||||
list := statuslist.New()
|
||||
list.Dodaj(true)
|
||||
claims, err := SestaviTrditve("http://localhost/api/status/abc", list, 0, time.Now())
|
||||
if err != nil {
|
||||
t.Fatalf("build claims: %v", err)
|
||||
}
|
||||
|
||||
token, err := UstvariZetonIzPEM(privatePEM, claims)
|
||||
if err != nil {
|
||||
t.Fatalf("create token: %v", err)
|
||||
}
|
||||
|
||||
got, err := PreveriZeton(token)
|
||||
if err != nil {
|
||||
t.Fatalf("verify token: %v", err)
|
||||
}
|
||||
if got.Status.Index != 0 {
|
||||
t.Fatalf("expected index 0, got %d", got.Status.Index)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateClaimsExpired(t *testing.T) {
|
||||
claims := Claims{
|
||||
Issuer: "http://localhost",
|
||||
IssuedAt: time.Now().Add(-2 * time.Hour).Unix(),
|
||||
ExpiresAt: time.Now().Add(-1 * time.Hour).Unix(),
|
||||
}
|
||||
err := PreveriTrditve(claims, "http://localhost", time.Now())
|
||||
if err == nil {
|
||||
t.Fatalf("expected expiration error")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user