-
-
Save timmc/d2814d7da19521dda1883dd3cc046217 to your computer and use it in GitHub Desktop.
#!/bin/bash | |
echo "DO NOT USE -- incorrect signature format, see comments on gist." | |
exit 1 | |
# Create and sign a JWT token with ES256 given the path to an ECDSA | |
# private key and a JSON payload. | |
# $0 path/to/keypair.der '{"JSON": "payload"}' | |
# Example keypair creation: | |
# openssl ecparam -name prime256v1 -genkey -noout -outform DER > example-keypair.der | |
# A few tips for generating the payload: | |
# - Pipe raw strings through `jq --raw-input .` to encode them as | |
# JSON strings. https://stedolan.github.io/jq/ | |
# - GNU date is great for generating the iat, nbf, and exp time | |
# fields: `date --date="15 minutes" +"%s"` | |
set -eu -o pipefail | |
keypair_path="$1" | |
payload="$2" | |
function base64_urlsafe { | |
# Implement own URL-safe Base64 based on standard version. Delete | |
# padding, undo wrapping, and swap out chars 62 and 63. Not all | |
# versions of `base64` have the `--wrap=0` that GNU coreutils has. | |
base64 | tr -d '\r\n=' | tr '+/' '-_' | |
} | |
header_enc="$(echo -n '{"typ":"JWT","alg":"ES256"}' | base64_urlsafe)" | |
payload_enc="$(echo -n "$payload" | base64_urlsafe)" | |
message="$header_enc.$payload_enc" | |
# If you're on a Mac, you might have a really old version of openssl | |
# that doesn't support ECDSA signing this way. | |
sig="$(echo -n "$message" | openssl dgst -sha256 -sign "$keypair_path" -keyform DER | base64_urlsafe)" | |
echo -n "$message.$sig" |
No idea, sorry. I don't have access to a Mac.
@timmc thx for replying that fast, I also tried on Fedora with OpenSSL 1.1.1d FIPS 10 Sep 2019
, and the jwt signature is also invalid according to jwt.io I'm nost sure what's wrong as openssl does not report any error.
For reference I was able to do what I wanted using python
#!/usr/bin/env python3
# Run with
# python3 siwa_client_secret.py a.code
#
# requires to install
# pip3 install cryptography
# pip3 install pyjwt
import sys
import jwt
import time
import urllib
from urllib.parse import urlencode
from urllib.request import Request, urlopen
authorization_code = sys.argv[1]
bundle_id = "..."
key_id = "..."
team_id = "..."
key = """
-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----
"""
issued_at = int(round(time.time()))
expiration = issued_at + 3600
claims = {
'aud': 'https://appleid.apple.com',
'sub': bundle_id,
'iss': team_id,
'exp': expiration,
'iat': issued_at,
}
client_secret = jwt.encode(claims, key, algorithm='ES256', headers={'kid': key_id}).decode("utf-8")
print("using secret: %s\n" % client_secret)
url = 'https://appleid.apple.com/auth/token'
payload = {
"grant_type": "authorization_code",
"client_secret": client_secret,
"code": authorization_code,
"client_id": bundle_id
}
headers = {
"Content-type": "application/x-www-form-urlencoded"
}
print("calling apple...")
try:
response = urlopen(Request(url, urlencode(payload).encode()))
print("\033[1;32;48mcode: %d => %s \033[1;37;0m" % (response.code, response.read.decode("utf8", "ignore")))
except urllib.error.HTTPError as e:
print("\033[1;31;48mcode: %d => %s \033[1;37;0m" % (e.code, e.read().decode("utf8", 'ignore')))
@bric3, @timmc, I think this is wrong. See this Stack Overflow for details, but effectively openssl is outputting a specific format of signature that isn't actually a valid JWS signature (not even a base64url decoded one). It's wrapping the two integers (R and S) in a DER format that can be read by the ASN1 module within openssl. This SO article goes into detail about that encoding/wrapper.
Hmm, thanks! That's unfortunate.
Luckily, I only ever used this script to generate values for a test suite, or something similar. We did have trouble with it not working on everyone's computer, so I wonder if there was a difference in output for certain openssl versions.
I'll put a giant disclaimer at the top. :-)
I posted my take on the procedure here.
I think you can fix this by making the following switch:
# Current signature construction
sig="$(echo -n "$message" | openssl dgst -sha256 -sign "$keypair_path" -keyform DER | base64_urlsafe)"
# Replacement
sig ="$(echo -n "$message" | openssl dgst -sha256 -sign "$keypair_path" -keyform DER | openssl asn1parse -inform DER | perl -n -e'/INTEGER :([0-9A-Z]*)$/ && print $1' | xxd -p -r | base64_urlsafe)"
I've just added a couple extra processing steps:
- To let openssl parse the signature
- A regex to extract the R and S integers from that output
- xxd to parse the strings as hex (that's how asn1parse outputs them) and converts them to binary before passing to your base64_urlsafe alg.
Hi could you be more specific about the issue about signing ?
My macOs has version LibreSSL 2.8.3, and installing LibreSSL 3.0.2, did not yield a signature that appear valid on jwt.io. By the way my input is a p8 file, which is encoded as a PEM.