Skip to content

salrashid123/golang-jwt-pqc

Repository files navigation

golang-jwt for post quantum cryptography

Another extension for go-jwt that allows creating and verifying JWT tokens where the signature schemes uses a set of post quantum cryptography signature algorithms.

Specifically, this implement ML-DSA family and a TODO would be SLH-DSA when thats available.

A sample JWT generated is in the form:

{
  "alg": "ML-DSA-44",
  "kid": "EMHG0l4cWeRqdIdxtHAYbzoxjLZsyaweF9NMIIDI6hU=",
  "typ": "JWT"
}
{
  "iss": "test",
  "exp": 1739907597
}

Note, this library uses cloudflare's implementation. A TODO is to use upstream go after issues/64537 implements ML-DSA and other algorithms (eg SLH-DSA)

critically, the standards aren't complete yet so this is just a toy and will possibly change. See draft Internet X.509 Public Key Infrastructure: Algorithm Identifiers for ML-DSA

This code is NOT supported by google and is just experimental

For other references, see:

Supported Algorithms

  • ML-DSA-44
  • ML-DSA-65

TODO: SLH-DSA

Also, the alg field is simply one derived from the draft: ML-DSA for JOSE and COSE and may change later (since its still draft)

Usage

Using this is really easy...you just need something that surfaces that interface.

I've written some simple ones here...the examples/ folder

package main

import (
	jwt "github.com/golang-jwt/jwt/v5"
	jwtsigner "github.com/salrashid123/golang-jwt-pqc"
	"github.com/cloudflare/circl/pki"
)

var ()

func main() {

	ctx := context.Background()

	// load and initialize the public and private keys
	privKeyPEMBytes, err := os.ReadFile("certs/ml-dsa-44-private.pem")
	pr, err := pki.UnmarshalPEMPrivateKey(privKeyPEMBytes)

	pubKeyPEMBytes, err := os.ReadFile("certs/ml-dsa-44-public.pem")
	pu, err := pki.UnmarshalPEMPublicKey(pubKeyPEMBytes)

	claims := &jwt.RegisteredClaims{
		ExpiresAt: &jwt.NumericDate{time.Now().Add(time.Minute * 1)},
		Issuer:    "test",
	}

	token := jwt.NewWithClaims(jwtsigner.SigningMethodMLDSA44, claims)

	keyctx, err := jwtsigner.NewSignerContext(ctx, &jwtsigner.SignerConfig{
		PrivateKey: pr,
		// PublicKey:  pu,
	})

	tokenString, err := token.SignedString(keyctx)

	fmt.Printf("TOKEN: %s\n", tokenString)

	// // verify with embedded publickey
	keyFunc, err := jwtsigner.SignerVerfiyKeyfunc(ctx, &jwtsigner.SignerConfig{
		PublicKey: pu,
	})

	vtoken, err := jwt.Parse(tokenString, keyFunc)

	if vtoken.Valid {
		fmt.Println("verified")
	}
}

The output is a signed JWT

$ cd examples/

$ go run ml-dsa-44/main.go 
2025/02/18 14:38:57 TOKEN: eyJhbGciOiJNTC1EU0EtNDQiLCJraWQiOiJFTUhHMGw0Y1dlUnFkSWR4dEhBWWJ6b3hqTFpzeWF3ZUY5Tk1JSURJNmhVPSIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ0ZXN0IiwiZXhwIjoxNzM5OTA3NTk3fQ.flKEYHjzRNDQjQZEuA4eeW_jAM2atqrXcCcxh-cKk-UBdIDA4sppgIvaJWVl46JLT-2rkrthCQ4uzVrp3hp9iuV93l9q8fhjwFTRfNUNDsm4H3Qi6jNd-y2vvbXnpRH5sQA0g9Q09d58-o7eZFTe5vGzPfnaLnnm3-gcIsqe8gxTMhu0qJAh7OVCIxpOF6Qtn7iBHJh1X1jUQ9kDtjWYxS2QvvxZ0deo_o33nfyi5YMd6GI4xwylpvusmBbEqbt8qIyE8bx3T8NR1zrrrWeir-8C_gvS4nLacPU8ihkgMM8rRlhkfXiknf8pMtj-NIFPTEmHjDkCGukHEQQ0vg6kks3qt254V2IJgFobceUgi4AaKijEV_v-heSg56ZyLidCVDwP1M-3vUM2X3HhlOT05bDHx1Grvx4ghchLNnnRrJtvN7ESS6DvnKCG-pO7UKTB_fiaRFGTZ5zTT7LhXwZcCLzIm1U_5Od7OFLSowSM1mR7vdY7Ft4V9_OIItO3Ka1LK7V_8n2EFAFu__vDc5Y5PSjA1xY2SudLVLRYRWefa8fYSxYqqLzG0--lDD0WjFfdk3PXh5D_whbPPXSUDBuZOf6OFI6_w1HwnumTXKEEv-tXF5RaxDMhRHmmmqUO3O7ulHkEmBWNDW4a6k4CpxGoTrlO-XubRbghQ6hG3E_PA8cuUUuLXZ6TaO63TB4V1lTuQKOHZH4YWx7wsVwlnVOM69oeFrB1OaVvHwwAU5zHUXsviUZk7yTnD9GKktDMfpSNQ_TTEpuJ-cGfrccCajCw42tzRrPAMVOpRYImzWQ1G3ztrTJjqDGSETENxEwtCfdfZ_cA20N00I5b3Ylz03tkaPOXhYds-JQQFoIxcOo_0mkCLvIb4tmlaU1RCQYFLT2ZWhmMMQdOXul5W3tsskAaEN9Y3TZccsknLGvB1DpxYu-b2kyptYvxYu0wKLGBZ4TDntuEHUc9n2N9LG5syE9sTQ5j_B-4Clu1EQBMr2cS0i0xHW5_qUoC-n8XZgcE-i7XHNh4XX-D0W8q5aXGyQQY_oBbd4u4QD0YbcAXN033AYfdHCE_vusMMk-gamLi7wC9s3cu2YA8VCZBrH_YrCTVo8GATxQq7aJVHLlBoL4FuT24BeUQFg5oGK7dxUcu9_1bebBydeaclgs6v048uQkkur2FKQLCX6f76S92KcPesXXOFPYYpvOR7WONZcJr2tJXMH5TH70DYRNhJp6XCXSc76vZMeaS4kQR-Bh6Vha_OZgP1iqGCE41zwNSVtTaE8H8ai52uizBaL6vQqxtPrh2lRtM7swUnFC6FbTDLmp_JxRLe_fkV7P-3SVDdF2nK_mduN9f9HkpjTvbdMGs8wM5SLf2kN7x4eQmtS5gEozjRawPDoCPtoe02wsDnf2RkFMS1ai0JInKD4Q8SBjQmeACAJGBUrWE3B1VciQ-9kWtQB6_MWC0Wk_0lEXSycW9o9zFtzdZ3EGDk66hdXakm0MC_KE-u5UOI6KYGt2-2y5jdZlylnn4amVQc3uzKdblOcm8wqJUcRHPY1ahTrMbAOwLLm1b97SDBvuYy05wmUGoyTEKEkteUCbyc3b_9ZW5772Q13PiwU1_IScS9Vb3klHEtSd-4alKShocSLm-A7Kmk8kYeaFZX-pZZGcxmAtMfhE68ppND_0LrlgIYCc0oCsRZavOiltWXpP4JLlkIZLbx0PYXk0Oa4Q7DhrWjDrKkxUREUpsqkpGTD1Tdh_lcaGoAsg3h6-RFMFqpbFGW69zBs9T9s--aUQN8koMKvz5Fm_Pp-mPJn8EU2yqFlhZGkzqV363YVSX3Cnl1DFaQqxAVgRRg6xLzbRvRXUCxVMcUrCS0EDMeCmlQ3eKQFmUEMYh3mMR6brGZH0nlfNHLJ2t-LP7PfQzDBH7Z5bXU5FczhSx3zJSEdj5kQGbzZJeSDA5mS5RaXJFEm_WEIVS0I_-vr2kJVXi1yfxJ1kRzLMCoPqUc7W3thT3zGsfbV7GYGTTpIRxNsPYTKCnRhHD-sPNZjUjfMkUbDmyApSH86d1Y3RMaqmtHTLfqE-NKmJklTJ40boYzoTvQogJA31EfJSweMwcjJHaqGJ8jaou6jMadTCJBNj663hkSST0B7_skl0uexlKcDymwy2599xtfPkKiKDeMkCyAitD2Ru0daDdXbn39Ecso4eQJnEt4stStO-z2IVBTqMNDnGMPkVtb4y216XKUIbIsH6eDNeWONCMxcd977Yc9Dtx9IOlNZkG66YSNaXFUfsSAT_jpbLBplg5dtKUoUF2Xke89pIoZO_42La5nKhR2HVfVPySu4lciX7u6Wc4_AtQJjM0nCKjAVtypioVLn6gktNfHSqh6WEg3n45ukjBl9NFwPXDfv2JdVoVCViiC-yd5KDGR4VeeMPcQIgsvBW00SUDNNS-qx0cp3KymA5_MYF-obGnWxkRmJ2Exs6jG8SBPhLIl6Ln66SzBHTLASSLPJFXwiVaQyinAmdkQMc0RnU2Fg5g0y2aMlwgE_46087k3OWeX8g5OnIyNOBp0LuiKJlKDVVEbsKcuVzvstY8cwxo8b_0DUvlJpMevEZJlpBNse1yeIC39HDpOKXDnwB5G2sOUs6bnGUB-IzKcE9WZHBb7g2QDdZ7XzhhalGt2OU7wwgngj-ul0qeHfZVmtDzdB_xboDerS7WTjPsTRAZvQpMhhQzIx5JKN47pN9lodlkDr8YIX6QIZY-IRgnft_bn6nUJTljOeXeItYkzH_AeT2Uu1dajKPSd28cfxicVrNLWcHwJKz_eHk0mEEjExuHzTVj4TwPqzeWpz3Art59P2L9lmFxkoOsORvLeKZQ0Iba2MMP1w0C_LITHUkYb2QXsPv0yNGAgZ4LBxQ-f4ezd1jAZvYmFOK7NhqUCHwbm-VEJ0ANJjUGp4vVHarWGK8npFczpD1-Lk57Ia5-tTotDEqsWPb190YP9OZSMhW1mg82ZS3Zo7ANvAO3QUMy0GMOTPP1qYU2e1i2iMUnW87gL5_U1G6hBX37w2Smeve9P9cRPPd14YkLpd9Rk0O65v82FQwxQ9bjPiZwe5y-0AwM1rnLYXaPL1EXKwN5193xR8QI_gqaQdCXH0YZBAMMJi9FV2l1h4uWuLm60uswMj5NVoWQm6u4vcrb3uTm8QgLEy00R1FdcoaMoavxARATVGyWl5mbpuLv9_kAAAAAAAAAAAAAAAAAAAAAAAAAAA8gLjw
2025/02/18 14:38:57 verified with Signer PublicKey
2025/02/18 14:38:57 verified with PubicKey
2025/02/18 14:38:57 verified with JWK KeyFunc URL

JWK Parsing

Also, note that GetThumbPrintFromContext() function generates a keyID consistent with JSON Web Key (JWK) Thumbprint. The pub field is base64urlencoded:

{
  "keys": [
    {
      "kty": "ML-DSA",
      "kid": "EMHG0l4cWeRqdIdxtHAYbzoxjLZsyaweF9NMIIDI6hU=",
      "alg": "ML-DSA-44",
      "pub": "wNA5o465+iisEdUKdpIsuGwW+ojAAOSw6FMNiuxU9nPb3fd1Lg/3awgi+5akk+lypJ6ixNmO4rRIA9KXb7iP+hcefFzJwG9bOxiFg5JB31hgzq7MFO18o3RM9+9FLONwes2bj0iwUwG2au6QoXpzXbwUqjqUxGIUoTP+wKpoxF/f1jEasG18yeaq+ikaSMxiPLmdTcluR8h99CpHY+K02I9c9Um7IAGb35r6/v4ChCbDDXXGUY1q33frq6zS4+pyhCESp0rnVdimbthRvn0DOh7KFrL30DKxz8j1FSHJndqwC/t22TmPNcsarayBNL3PLXv72i+4pRwIZuzBDykTo2OGECVsvFCvuEfHfltyPYWYFCTtdKGqWHHFRY/hsxo3ia/kkQvYdaxZ+k6uUrH5mZASSUfOeys6s3rSOY+/pqkLFYQOjxXM1vT83kKx53D0i4RCbolzCgPvW0dw94aOJyj5L6qsSmV3G32ZEqMXDip0yOJI128YgaJzLSbniNkgHVOPbn018lQxw+41kJkkLnDOCKOkzAsSXirvK2QmkA9qgVpIYVSO/NBAY9uUtZgdoJBjc634r0k59idyTimVQK/JrYBU6d22xi854taD4j+bMbXxsPttzVARGFGrbsjQNQ2ITHNivWQdeUB2evGoxv6ARAaDvjSz9M9oVGW/OZOnx9g/yd2Aq2UO13nlHHrW+hunqunhDnnL84cL3rPjBIctRBpcmc1WX2DVS4kI1MLfUzwphfgMOwUNa8DNwNEWEofL2HQbahcJkLViDtzGvhzqTtUk9dV6Da1XCtocxwWweXz/yQxc1wqfjRlwvaxzjsG9GnuWxq9YBANAFjacN1DpxXk0g4Bf3B19RkHyOJxbdbch+OdVqndtkrkoUcGbAsFC+aOGokingAlrSpbSEUblZf5rmIUu533+tA6yK/gQEkP+NGCCDWTmvSy68TLp5CH3bRaPBfZiolyrmhxEwqQRWMW5gLq7ahft6O8SFjhH0kh8+YkGXEDd7u3DrHdJNRfvePcfsjaN4/15We7qTDbDCJwAjqDlqPz7tpRWgmwfQWlSYTfsbZ+mEcqC7JaEZlfkWdisJqAJpwNaMiiB8V5NT1UCFHPuF950DuM5SZYD4mtgMUMXuNlo+0felges4eF/byKD9/5EHu2vIlmdkrVGTsK2bOd1ZU19xCw4roIRregBpKxDnMBtswAHJAxr7OECrCT2vO+fJ1wGQh20QffLtMDUQX9XZ9HcJy6CliqQyXAUNi2K4m+FL6oHSMiKxV4YOzYO/A5okozO+N4YYnc64ymdlQwHSwRGdjerLf1oSlwZaXzGI2Hq5RZnkMHVDaBDStEnFTb7h31qXFVVrMZs6X+Wsr6JwDGcfvLRougcXGhoY0oq7F3CjwPEZjehCMP4qNErXGVCXmEyNdM4zNHMR+D21cHzz6efSZuvMOI61ZbDKAt3gZqUWESUPDrs5fC3WIGYtJGcyFnzErLOCNTBNo63YbNwfRbAKVl0KGqB8P+9IL2aFpS2UcJ7pIroXajth+nlJUVNkxLh4TFxJG4k/mOj7OlbZDRHgWCUEmQ/fm405JgnjTEDrrDWx28EUnCk7eiwBirlfKNkVsBS7tru/ed2llRzFs6k0CqhmK48H/L6+7Pcnx01op5f0k6FAWOK2z4MYeCMs1iy1/QufdF/bx8c+tyo2STGMJaPBXi7ZFu37l7gR8m/JRFQ5xq0FDhcma9HjvTNisQvJT7x7A=="
    },
    {
      "kty": "ML-DSA",
      "kid": "JSs1QLnP64IvTd00Tsy4FSJq4loJ5dDHJWeIcA5nky0=",
      "alg": "ML-DSA-65",
      "pub": "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"
    }
  ]
}

If you want to read the JWK from a url or file directly, you can recall it using a custom keyFunc

	vr, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
		kidInter, ok := token.Header["kid"]

		kid, ok := kidInter.(string)

		jwkBytes, err := os.ReadFile("certs/jwk.json")

		var keyset jwtsigner.JSONWebKeySet

		for _, k := range keyset.Keys {
			if k.Kid == kid {
				switch k.Alg {
				case "ML-DSA-44":
					pu, err := mldsa44.Scheme().UnmarshalBinaryPublicKey(k.Pub)
					return pu, nil
				case "ML-DSA-65":
					pu, err := mldsa65.Scheme().UnmarshalBinaryPublicKey(k.Pub)
					return pu, nil
				default:
					return nil, fmt.Errorf("error unsupported key alg: %s", k.Alg)
				}
			}
		}
		return nil, fmt.Errorf("keyset not found for key %s", kid)
	})

Openssl Compatibility

Note, as of writing, the ML-DSA keys generated by openssl is inconsistent with this library

About

golang-jwt for post quantum cryptography

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages