Skip to content

Instantly share code, notes, and snippets.

@kousu
Last active April 14, 2024 00:55
Show Gist options
  • Save kousu/f3174af57e1fc42a0a88586b5a5ffdc9 to your computer and use it in GitHub Desktop.
Save kousu/f3174af57e1fc42a0a88586b5a5ffdc9 to your computer and use it in GitHub Desktop.
Using jose-util with ed25519 (aka EdDSA), PEM-formatted, keys

Using jose-util with ed25519 (aka EdDSA), PEM-formatted, keys

Using go-jose takes a bit of a knack. And it turns out there's a snag if you want to use the latest and greatest crypto.

I installed jose-util from the latest git version:

$ pwd
/Users/kousu/src/go-jose
anaesthetic-mac:go-jose kousu$ git log HEAD~1..HEAD
commit 009d17693689078186824d7147ef3d3ce7948967 (HEAD -> master, origin/master, origin/HEAD)
Merge: 723929d 68b7811
Author: Mat Byczkowski <mbyczkowski@squareup.com>
Date:   Fri Aug 30 15:21:30 2019 -0700

    Merge pull request #255 from square/dependabot/go_modules/github.com/stretchr/testify-1.4.0
    
    Bump github.com/stretchr/testify from 1.3.0 to 1.4.0

commit 68b7811e41ba9702e9725f4ae23067e5a0c7a390
Author: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
Date:   Fri Aug 16 06:39:31 2019 +0000

    Bump github.com/stretchr/testify from 1.3.0 to 1.4.0
    
    Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.3.0 to 1.4.0.
    - [Release notes](https://github.com/stretchr/testify/releases)
    - [Commits](https://github.com/stretchr/testify/compare/v1.3.0...v1.4.0)
    
    Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
$ cd jose-util/
$ go install
$ which jose-util
/Users/kousu/go/bin/jose-util

I'll make a scratch workspace:

$ mkdir -p ~/src/experiment/eddsa
$ cd ~/src/experiment/eddsa/

Key Generation

$ jose-util generate-key --alg=EdDSA --use=sig
$ ls -l
total 16
-r--------  1 kousu  staff  200 14 sep 19:55 jwk-sig-a72a862d-585f-4ff0-8730-a26639ce3bbf-priv.json
-r--r--r--  1 kousu  staff  150 14 sep 19:55 jwk-sig-a72a862d-585f-4ff0-8730-a26639ce3bbf-pub.json

$ cat jwk-sig-a72a862d-585f-4ff0-8730-a26639ce3bbf-priv.json ; echo
{"use":"sig","kty":"OKP","kid":"a72a862d-585f-4ff0-8730-a26639ce3bbf","crv":"Ed25519","alg":"EdDSA","x":"TdGIibGMdQhIu7MGDXjYMTxrcPu8-SoE6D2sEiQI2os","d":"H-y7E7OsM_Ka44fvNdkdu2x-zDaZzhYG_y1KjA--59E"}

$ cat jwk-sig-a72a862d-585f-4ff0-8730-a26639ce3bbf-pub.json ; echo
{"use":"sig","kty":"OKP","kid":"a72a862d-585f-4ff0-8730-a26639ce3bbf","crv":"Ed25519","alg":"EdDSA","x":"TdGIibGMdQhIu7MGDXjYMTxrcPu8-SoE6D2sEiQI2os"}

These keys are in the JOSE JWK format.

Sample message

$ echo "Hello, World" > message.txt

Signing

$ jose-util --key=jwk-sig-a72a862d-585f-4ff0-8730-a26639ce3bbf-priv.json --in=message.txt sign --alg=EdDSA | tee message.jwt; echo
eyJhbGciOiJFZERTQSIsImtpZCI6ImE3MmE4NjJkLTU4NWYtNGZmMC04NzMwLWEyNjYzOWNlM2JiZiJ9.SGVsbG8sIFdvcmxkCg.sMxfupn8NsH8fhJktx6Fhknet2gthJp93VsJt9AkWmjabEC1oHj7ezBF4608v0Yw2k5EpJwjly3q1HbFRUlJAQ

Verifying + Unwrapping

With JWTs, the signature is always attached to the message, so verifying also unwraps and shows us the message (In contrast, with GPG you may have an attached signature OR a detached signature, and with OpenSSL signatures are always detached: https://stackoverflow.com/questions/10782826/digital-signature-for-a-file-using-openssl). I think the JWT people think this is a good idea because it means it's unlikely you'll use data in a token without verifying it first.

$ jose-util --key=jwk-sig-a72a862d-585f-4ff0-8730-a26639ce3bbf-pub.json --in=sig.jwt verify
Hello, World

But what if you want to use OpenSSL (aka x509) keys? I didn't realize go-jose supported these, but https://github.com/square/go-jose/blob/master/jose-util/utils.go shows that it does.

I am using

$ openssl version
OpenSSL 1.1.1d  10 Sep 2019

which I installed by brew install openssl@1.1 && echo 'export PATH="/usr/local/opt/openssl@1.1/bin:$PATH' >> ~/.bash_profile"; you need this version because EdDSA (aka ed25519) is still pretty new and not supported everywhere. It's not even an obligatory part of the JWT spec, it's only an optional addition.

Key Generation

You have to generate the keys in two steps with OpenSSL: first the private key, then extract the public part from it:

$ openssl genpkey -algorithm ED25519 | tee ed25519-key-pair.pem
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEICrDqHiXizmAHQYFnwX6dE/qP8KblxKXYEucmVZTbMwy
-----END PRIVATE KEY-----
$ openssl pkey -in ed25519-key-pair.pem -pubout | tee ed25519-key-pair.pub.pem
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEA/DIbglGpx4BhW+yFV3q8A7xgmrIS0QtPU1ABUMps+UM=
-----END PUBLIC KEY-----

Signing?

But if you try to use this key:

$ jose-util --key=ed25519-key-pair.pem --in=message.txt sign --alg=EdDSA | tee message.jwt; echo
jose-util: error: unable to make signer: square/go-jose: unsupported key type/format

Bummer.

To test my understanding of what was supposed to happen, I generated an ECDSA key:

$ openssl genpkey -algorithm ec -pkeyopt ec_paramgen_curve:prime256v1 | tee ec256-key-pair.pem 
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgBe33HNk4/MZyGbg7
vHH0dhIiPgOIyFG4gx/c6zc3cnyhRANCAAT9HTm8Xqi9WenQVTQgp9Jc0bjfs/gI
8Jh3fkvo/yCyFGGFHgl4FFNTJ+iyjhLW7TwCBL1dy0qK1R6kdX9Joc+m
-----END PRIVATE KEY-----
$ openssl pkey -in ec256-key-pair.pem -pubout | tee ec256-key-pair.pub.pem 
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/R05vF6ovVnp0FU0IKfSXNG437P4
CPCYd35L6P8gshRhhR4JeBRTUyfoso4S1u08AgS9XctKitUepHV/SaHPpg==
-----END PUBLIC KEY-----

((btw, for EC keys OpenSSL has a separate but equivalent(?) ecparam command that can generate keys:

$ openssl ecparam -genkey -name prime256v1 -noout | tee some-new-key.ec256.pem
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIP9cRh8oUv55AXCbuD+IGqsT1+qAKqUOQX6dEaz5y+rIoAoGCCqGSM49
AwEHoUQDQgAEnF8bxMBAqYFWbinB5h+hQlgNbjGLiGZ3wy+/HtmDzmpFvPPBNIbc
0lJCz5g1SM4aBwQsMfBfY3q6RA2E6fcExw==
-----END EC PRIVATE KEY-----

))

Then I tried to use this key with go-jose and it worked!

$ jose-util --key=ec256-key-pair.pem --in=message.txt sign --alg=ES256 | tee message.openssl-jwt; echo
eyJhbGciOiJFUzI1NiJ9.SGVsbG8sIFdvcmxkCg.2XvQu0-JGGUJMi9UEEeJw4krN2MNgcOBIy22krDAxi2K3vNRxfIoqSBPk_sKSAW7XvZ6ZntoK9UpSeh3smNdDw
$ jose-util --key=ec256-key-pair.pub.pem --in=message.openssl-jwt verify
Hello, World

That's Weird

https://golang.org/pkg/crypto/x509/ says

ParsePKCS8PrivateKey parses an unencrypted private key in PKCS#8, ASN.1 DER form. It returns a *rsa.PrivateKey, a *ecdsa.PrivateKey, or a ed25519.PrivateKey. More types might be supported in the future. This kind of key is commonly encoded in PEM blocks of type "PRIVATE KEY".

so it should be able to handle ed25519 keys. Turns out, it's a bug and here's a patch: square/go-jose#258

Bonus: Hacking around it with asn1parse

If we look again at the JWK EdDSA fornat

$ cat jwk-sig-a72a862d-585f-4ff0-8730-a26639ce3bbf-priv.json 
{"use":"sig","kty":"OKP","kid":"a72a862d-585f-4ff0-8730-a26639ce3bbf","crv":"Ed25519","alg":"EdDSA","x":"TdGIibGMdQhIu7MGDXjYMTxrcPu8-SoE6D2sEiQI2os","d":"H-y7E7OsM_Ka44fvNdkdu2x-zDaZzhYG_y1KjA--59E"}$ 
$ cat jwk-sig-a72a862d-585f-4ff0-8730-a26639ce3bbf-pub.json 
{"use":"sig","kty":"OKP","kid":"a72a862d-585f-4ff0-8730-a26639ce3bbf","crv":"Ed25519","alg":"EdDSA","x":"TdGIibGMdQhIu7MGDXjYMTxrcPu8-SoE6D2sEiQI2os"}$ 

it's just some headers, the word "EdDSA", and then two numbers, probably in base64 format, and the public key is simply the private key with one of those numbers censored.

we can inspect the content of the OpenSSL key like this

$ openssl pkey -in ed25519-key-pair.pem -text -noout
ED25519 Private-Key:
priv:
    2a:c3:a8:78:97:8b:39:80:1d:06:05:9f:05:fa:74:
    4f:ea:3f:c2:9b:97:12:97:60:4b:9c:99:56:53:6c:
    cc:32
pub:
    fc:32:1b:82:51:a9:c7:80:61:5b:ec:85:57:7a:bc:
    03:bc:60:9a:b2:12:d1:0b:4f:53:50:01:50:ca:6c:
    f9:43

(the -noout is one of OpenSSL's many sharp and confusing edges. Otherwise, the key in its internal format is replicated to stdout in addition to the unpacked version)

So if we convert this into base64 and paste it into the slots in the JWK, we should have a usable key:

$ python
Python 3.7.4 (default, Sep  7 2019, 18:27:02) 
[Clang 10.0.1 (clang-1001.0.46.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> priv = """
...     2a:c3:a8:78:97:8b:39:80:1d:06:05:9f:05:fa:74:
...     4f:ea:3f:c2:9b:97:12:97:60:4b:9c:99:56:53:6c:
...     cc:32
... """
>>> pub = """
...     fc:32:1b:82:51:a9:c7:80:61:5b:ec:85:57:7a:bc:
...     03:bc:60:9a:b2:12:d1:0b:4f:53:50:01:50:ca:6c:
...     f9:43
... """
>>> import base64
>>> priv = [int(b,16) for b in priv.strip().split(":")]
>>> priv = bytes(priv)
>>> priv = base64.b64encode(priv)
>>> # add the special b64 modifications from base64.b64encode
>>> priv = priv.replace(b"+",b"-").replace(b"/",b"_").rstrip(b"=")
>>> priv
b'KsOoeJeLOYAdBgWfBfp0T-o_wpuXEpdgS5yZVlNszDI'
>>>
>>> pub = [int(b,16) for b in pub.strip().split(":")]
>>> pub = bytes(pub)
>>> pub = base64.b64encode(pub)
>>> pub = pub.replace("+","-").replace("/","_").rstrip("=")
>>> pub = pub.replace(b"+",b"-").replace(b"/",b"_").rstrip(b"=")
>>> pub
b'_DIbglGpx4BhW-yFV3q8A7xgmrIS0QtPU1ABUMps-UM'

Reasoning that x must be the public key and d the private key, since it's the one that gets hidden, we should be able to take the sample JWK and construct:

$ cat jwk-hacked-test-1-priv.json
{"use":"sig","kty":"OKP","kid":"hacked-test-1","crv":"Ed25519","alg":"EdDSA","x":"_DIbglGpx4BhW-yFV3q8A7xgmrIS0QtPU1ABUMps-UM","d":"KsOoeJeLOYAdBgWfBfp0T-o_wpuXEpdgS5yZVlNszDI"}
$ cat jwk-hacked-test-1-pub.json 
{"use":"sig","kty":"OKP","kid":"hacked-test-1","crv":"Ed25519","alg":"EdDSA","x":"_DIbglGpx4BhW-yFV3q8A7xgmrIS0QtPU1ABUMps-UM"}

and it works!

$ jose-util --key=jwk-hacked-test-1-priv.json --in=message.txt sign --alg=EdDSA | tee message.jwt; echo
eyJhbGciOiJFZERTQSIsImtpZCI6ImhhY2tlZC10ZXN0LTEifQ.SGVsbG8sIFdvcmxkCg.HsuiM9WqvLnWsfrwgcha9cNewEmOr5PrxEPYvj7ryer8lhWw01ePhJw67z0WjOeNMLw2VeQ-tP0P7qQV3HxaBw
$ jose-util --key=jwk-hacked-test-1-pub.json  --in=message.jwt verify
Hello, World

In fact, when run against my patched jose-util that supports, you can use either format key to sign/verify each other's:

$ # signing with the OpenSSL format, verifying with both formats
$ jose-util --key=ed25519-key-pair.pem --in=message.txt sign --alg=EdDSA | tee message.jwt; echo
eyJhbGciOiJFZERTQSJ9.SGVsbG8sIFdvcmxkCg.1LnjnGjU-4QBKKJ3-t-mb-qAxC7J1P_HvhkWcuEGC75o3hRMEzQpHxMlRY7F5Gb37NPDl13X_qDstpuZPoFUDw
$ jose-util --key=ed25519-key-pair.pub.pem  --in=message.jwt verify
Hello, World
$ jose-util --key=jwk-hacked-test-1-pub.json  --in=message.jwt verify
Hello, World
$
$ # signing with the JWK format, verifying with both formats
$ jose-util --key=jwk-hacked-test-1-priv.json --in=message.txt sign --alg=EdDSA | tee message.jwt; echo
eyJhbGciOiJFZERTQSIsImtpZCI6ImhhY2tlZC10ZXN0LTEifQ.SGVsbG8sIFdvcmxkCg.HsuiM9WqvLnWsfrwgcha9cNewEmOr5PrxEPYvj7ryer8lhWw01ePhJw67z0WjOeNMLw2VeQ-tP0P7qQV3HxaBw
$ jose-util --key=jwk-hacked-test-1-pub.json  --in=message.jwt verify
Hello, World
$ jose-util --key=ed25519-key-pair.pub.pem  --in=message.jwt verify
Hello, World

If you wanted, you could probably script getting the public/private keys out using openssl asn1parse but I hate using that command with a firey passion, so I'm not going to try to figure it out again right now. You could also find an asn1/PEM/DER parser for your language of choice. Or maybe just script openssl pkey ... | awk ...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment