-
-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for X.509 and a website (#2)
- Loading branch information
1 parent
405fc35
commit 30b57c3
Showing
66 changed files
with
6,171 additions
and
789 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
config.*json | ||
node_modules |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,162 +1,97 @@ | ||
[![Go Report Card](https://goreportcard.com/badge/github.com/MicahParks/jwkset)](https://goreportcard.com/report/github.com/MicahParks/jwkset) [![Go Reference](https://pkg.go.dev/badge/github.com/MicahParks/jwkset.svg)](https://pkg.go.dev/github.com/MicahParks/jwkset) | ||
# JWK Set | ||
This is a JWK Set (JWKS or jwks) implementation. For a JWK Set client, | ||
see [`github.com/MicahParks/keyfunc`](https://github.com/MicahParks/keyfunc). Cryptographic keys can be added, deleted, | ||
and read from the JWK Set. A JSON representation of the JWK Set can be created for hosting via HTTPS. This project | ||
includes an in-memory storage implementation, but an interface is provided for more advanced use cases. For this | ||
implementation, a key ID (`kid`) is required. | ||
|
||
This project only depends on packages from the standard Go library. It has no external dependencies. | ||
|
||
The following key types have a JSON representation: | ||
|
||
| Key type | Go private key type | Go public key type | External link | | ||
|----------|----------------------------------------------------------------------|--------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------| | ||
| `EC` | [`*ecdsa.PrivateKey`](https://pkg.go.dev/crypto/ecdsa#PrivateKey) | [`*ecdsa.PublicKey`](https://pkg.go.dev/crypto/ecdsa#PublicKey) | [Elliptic Curve Digital Signature Algorithm (ECDSA)](https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm) | | ||
| `OKP` | [`ed25519.PrivateKey`](https://pkg.go.dev/crypto/ed25519#PrivateKey) | [`ed25519.PublicKey`](https://pkg.go.dev/crypto/ed25519#PublicKey) | [Edwards-curve Digital Signature Algorithm (EdDSA)](https://en.wikipedia.org/wiki/EdDSA) | | ||
| `RSA` | [`*rsa.PrivateKey`](https://pkg.go.dev/crypto/rsa#PrivateKey) | [`*rsa.PublicKey`](https://pkg.go.dev/crypto/rsa#PublicKey) | [Rivest–Shamir–Adleman (RSA)](https://en.wikipedia.org/wiki/RSA_(cryptosystem)) | | ||
| `oct` | `[]byte` | none | | | ||
|
||
Only the Go types listed in this table have a JSON representation. If you would like support for another key type, | ||
please open an issue on GitHub. | ||
|
||
# Example HTTP server | ||
```go | ||
package main | ||
|
||
import ( | ||
"context" | ||
"crypto/rand" | ||
"crypto/rsa" | ||
"log" | ||
"net/http" | ||
"os" | ||
|
||
"github.com/MicahParks/jwkset" | ||
) | ||
|
||
const ( | ||
logFmt = "%s\nError: %s" | ||
) | ||
|
||
func main() { | ||
ctx := context.Background() | ||
logger := log.New(os.Stdout, "", 0) | ||
|
||
jwkSet := jwkset.NewMemory[any]() | ||
|
||
key, err := rsa.GenerateKey(rand.Reader, 4096) | ||
if err != nil { | ||
logger.Fatalf(logFmt, "Failed to generate RSA key.", err) | ||
} | ||
|
||
err = jwkSet.Store.WriteKey(ctx, jwkset.NewKey[any](key, "my-key-id")) | ||
if err != nil { | ||
logger.Fatalf(logFmt, "Failed to store RSA key.", err) | ||
} | ||
|
||
http.HandleFunc("/jwks.json", func(writer http.ResponseWriter, request *http.Request) { | ||
// TODO Cache the JWK Set so storage isn't called for every request. | ||
response, err := jwkSet.JSONPublic(request.Context()) | ||
if err != nil { | ||
logger.Printf(logFmt, "Failed to get JWK Set JSON.", err) | ||
writer.WriteHeader(http.StatusInternalServerError) | ||
return | ||
} | ||
|
||
writer.Header().Set("Content-Type", "application/json") | ||
_, _ = writer.Write(response) | ||
}) | ||
|
||
logger.Print("Visit: http://localhost:8080/jwks.json") | ||
logger.Fatalf("Failed to listen and serve: %s", http.ListenAndServe(":8080", nil)) | ||
} | ||
[![Go Reference](https://pkg.go.dev/badge/github.com/MicahParks/jwkset.svg)](https://pkg.go.dev/github.com/MicahParks/jwkset) | ||
|
||
# JWK Set (JSON Web Key Set) | ||
|
||
This is a JWK Set (JSON Web Key Set) implementation written in Golang. | ||
|
||
If you would like to generate or validate a JWK without writing any Golang code, please visit | ||
the [Generate a JWK Set](#generate-a-jwk-set) section. | ||
|
||
If you would like to have a JWK Set client to help verify JWTs without writing any Golang code, you can use the | ||
[JWK Set Client Proxy (JCP) project](https://github.com/MicahParks/jcp) perform JWK Set client operations in the | ||
language of your choice using an OpenAPI interface. | ||
|
||
# Generate a JWK Set | ||
|
||
If you would like to generate a JWK Set without writing Golang code, this project publishes utilities to generate a JWK | ||
Set from: | ||
|
||
* PEM encoded X.509 Certificates | ||
* PEM encoded public keys | ||
* PEM encoded private keys | ||
|
||
The PEM block type is used to infer which key type to decode. Reference the [Supported keys](#supported-keys) section | ||
for a list of supported cryptographic key types. | ||
|
||
## Website | ||
|
||
Please visit [https://jwkset.com](https://jwkset.com) to use the web interface for this project. You can self-host this | ||
website by following the instructions in the `README.md` in the [website](https://github.com/MicahParks/jwkset/website) | ||
directory. | ||
|
||
## Command line | ||
|
||
Gather your PEM encoded keys or certificates and use the `cmd/jwksetinfer` command line tool to generate a JWK Set. | ||
|
||
**Install** | ||
|
||
``` | ||
go install github.com/MicahParks/jwkset/cmd/jwksetinfer@latest | ||
``` | ||
|
||
**Usage** | ||
|
||
# Example for marshalling a single key to a JSON Web Key | ||
```go | ||
package main | ||
|
||
import ( | ||
"crypto/ed25519" | ||
"crypto/rand" | ||
"encoding/json" | ||
"log" | ||
"os" | ||
|
||
"github.com/MicahParks/jwkset" | ||
) | ||
|
||
const logFmt = "%s\nError: %s" | ||
|
||
func main() { | ||
logger := log.New(os.Stdout, "", 0) | ||
|
||
// Create an EdDSA key. | ||
_, private, err := ed25519.GenerateKey(rand.Reader) | ||
if err != nil { | ||
logger.Fatalf(logFmt, "Failed to generate EdDSA key.", err) | ||
} | ||
|
||
// Wrap the key in the appropriate Go type. | ||
meta := jwkset.NewKey(private, "my-key-id") | ||
|
||
// Create the approrpiate options to include the private key material in the JSON representation. | ||
options := jwkset.KeyMarshalOptions{ | ||
AsymmetricPrivate: true, | ||
} | ||
|
||
// Marshal the key to a different Go type that can be serialized to JSON. | ||
marshal, err := jwkset.KeyMarshal(meta, options) | ||
if err != nil { | ||
logger.Fatalf(logFmt, "Failed to marshal key.", err) | ||
} | ||
|
||
// Marshal the new type to JSON. | ||
j, err := json.MarshalIndent(marshal, "", " ") | ||
if err != nil { | ||
logger.Fatalf(logFmt, "Failed to marshal JSON.", err) | ||
} | ||
println(string(j)) | ||
|
||
// Unmarshal the raw JSON into a Go type that can be deserialized into a key. | ||
err = json.Unmarshal(j, &marshal) | ||
if err != nil { | ||
logger.Fatalf(logFmt, "Failed to unmarshal JSON.", err) | ||
} | ||
|
||
// Create the appropriate options to include the private key material in the deserialization. | ||
// | ||
// If this option is not provided, the resulting key will be of the type ed25519.PublicKey. | ||
unmarshalOptions := jwkset.KeyUnmarshalOptions{ | ||
AsymmetricPrivate: true, | ||
} | ||
|
||
// Convert the Go type back into a key with metadata. | ||
meta, err = jwkset.KeyUnmarshal(marshal, unmarshalOptions) | ||
if err != nil { | ||
logger.Fatalf(logFmt, "Failed to unmarshal key.", err) | ||
} | ||
|
||
// Print the key ID. | ||
println(meta.KeyID) | ||
} | ||
``` | ||
jwksetinfer mykey.pem mycert.crt | ||
``` | ||
|
||
# Supported keys | ||
|
||
This project supports the following key types: | ||
|
||
* [Edwards-curve Digital Signature Algorithm (EdDSA)](https://en.wikipedia.org/wiki/EdDSA) (Ed25519 only) | ||
* Go Types: `ed25519.PrivateKey` and `ed25519.PublicKey` | ||
* [Elliptic-curve Diffie–Hellman (ECDH)](https://en.wikipedia.org/wiki/Elliptic-curve_Diffie%E2%80%93Hellman) (X25519 | ||
only) | ||
* Go Types: `*ecdh.PrivateKey` and `*ecdh.PublicKey` | ||
* [Elliptic Curve Digital Signature Algorithm (ECDSA)](https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm) | ||
* Go Types: `*ecdsa.PrivateKey` and `*ecdsa.PublicKey` | ||
* [Rivest–Shamir–Adleman (RSA)](https://en.wikipedia.org/wiki/RSA_(cryptosystem)) | ||
* Go Types: `*rsa.PrivateKey` and `*rsa.PublicKey` | ||
* [HMAC](https://en.wikipedia.org/wiki/HMAC), [AES Key Wrap](https://en.wikipedia.org/wiki/Key_Wrap), and other | ||
symmetric keys | ||
* Go Type: `[]byte` | ||
|
||
Cryptographic keys can be added, deleted, and read from the JWK Set. A JSON representation of the JWK Set can be created | ||
for hosting via HTTPS. This project includes an in-memory storage implementation, but an interface is provided for more | ||
advanced use cases. | ||
|
||
# Notes | ||
|
||
This project aims to implement the relevant RFCs to the fullest extent possible using the Go standard library, but does | ||
not implement any cryptographic algorithms itself. | ||
|
||
* RFC 8037 adds support for `Ed448`, `X448`, and `secp256k1`, but there is no Golang standard library support for these | ||
key types. | ||
* RFC 7518 specifies that `Base64urlUInt` must use the "minimum number of octets" to represent the number. This can lead | ||
to a problem with parsing JWK made by other projects that may contain leading zeros in the | ||
non-compliant `Base64urlUInt` encoding. This error happens during JWK validation and will look | ||
like: `failed to validate JWK: marshaled JWK does not match original JWK`. To work around this, please modify the | ||
JWK's JSON to remove the leading zeros for a proper `Base64urlUInt` encoding. If you need help doing this, please open | ||
a GitHub issue. | ||
* This project does not currently support JWK Set encryption using JWE. This would involve implementing the relevant JWE | ||
specifications. It may be implemented in the future if there is interest. Open a GitHub issue to express interest. | ||
|
||
# Test coverage | ||
Test coverage is currently `99%`. | ||
|
||
``` | ||
$ go test -cover -race | ||
$ go test -cover | ||
PASS | ||
coverage: 98.5% of statements | ||
ok github.com/MicahParks/jwkset 0.031s | ||
coverage: 85.5% of statements | ||
ok github.com/MicahParks/jwkset 0.013s | ||
``` | ||
|
||
# References | ||
This project was built and tested using various RFCs and services. The services are listed below: | ||
* [mkjwk.org](https://github.com/mitreid-connect/mkjwk.org) | ||
# See also | ||
|
||
See also: | ||
* [`github.com/MicahParks/keyfunc`](https://github.com/MicahParks/keyfunc) | ||
* [`github.com/golang-jwt/jwt/v4`](https://github.com/golang-jwt/jwt) | ||
* [`github.com/MicahParks/jcp`](https://github.com/MicahParks/jcp) | ||
* [`github.com/MicahParks/keyfunc`](https://github.com/MicahParks/keyfunc) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
package main | ||
|
||
import ( | ||
"crypto/ecdsa" | ||
"crypto/elliptic" | ||
"crypto/rand" | ||
"crypto/x509" | ||
"encoding/pem" | ||
"log" | ||
"os" | ||
) | ||
|
||
const ( | ||
logFmt = "%s\nError: %s" | ||
privFile = "ec256SEC1Priv.pem" | ||
) | ||
|
||
func main() { | ||
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) | ||
if err != nil { | ||
log.Fatalf(logFmt, "Failed to generate EC key.", err) | ||
} | ||
|
||
pemBytes, err := x509.MarshalECPrivateKey(priv) | ||
if err != nil { | ||
log.Fatalf(logFmt, "Failed to marshal EC private key.", err) | ||
} | ||
block := &pem.Block{ | ||
Type: "EC PRIVATE KEY", | ||
Bytes: pemBytes, | ||
} | ||
out := pem.EncodeToMemory(block) | ||
|
||
err = os.WriteFile(privFile, out, 0644) | ||
if err != nil { | ||
log.Fatalf(logFmt, "Failed to write EC private key.", err) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
package main | ||
|
||
import ( | ||
"crypto/rand" | ||
"crypto/rsa" | ||
"crypto/x509" | ||
"encoding/pem" | ||
"log" | ||
"os" | ||
) | ||
|
||
const ( | ||
logFmt = "%s\nError: %s" | ||
privFile = "rsa2048PKCS1Priv.pem" | ||
pubFile = "rsa2048PKCS1Pub.pem" | ||
) | ||
|
||
func main() { | ||
priv, err := rsa.GenerateKey(rand.Reader, 2048) | ||
if err != nil { | ||
log.Fatalf(logFmt, "Failed to generate RSA key.", err) | ||
} | ||
|
||
block := &pem.Block{ | ||
Type: "RSA PRIVATE KEY", | ||
Bytes: x509.MarshalPKCS1PrivateKey(priv), | ||
} | ||
out := pem.EncodeToMemory(block) | ||
|
||
err = os.WriteFile(privFile, out, 0644) | ||
if err != nil { | ||
log.Fatalf(logFmt, "Failed to write RSA private key.", err) | ||
} | ||
|
||
pub := &priv.PublicKey | ||
block = &pem.Block{ | ||
Type: "RSA PUBLIC KEY", | ||
Bytes: x509.MarshalPKCS1PublicKey(pub), | ||
} | ||
out = pem.EncodeToMemory(block) | ||
|
||
err = os.WriteFile(pubFile, out, 0644) | ||
if err != nil { | ||
log.Fatalf(logFmt, "Failed to write RSA public key.", err) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
module github.com/MicahParks/jwkset/cmd/jwksetinfer | ||
|
||
go 1.21.4 | ||
|
||
require github.com/MicahParks/jwkset v0.3.1 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
github.com/MicahParks/jwkset v0.3.1 h1:DIVazR/elD8CLWPblrVo610TzovIDYMcvlM4X0UT0vQ= | ||
github.com/MicahParks/jwkset v0.3.1/go.mod h1:Ob0sxSgMmQZFg4GO59PVBnfm+jtdQ1MJbfZDU90tEwM= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
go 1.21.4 | ||
|
||
use ( | ||
../.. | ||
. | ||
) |
Oops, something went wrong.