196 lines
4.8 KiB
Go
196 lines
4.8 KiB
Go
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
|
|
}
|