Skip to content

Commit dfbfe08

Browse files
committed
Added support for ES256 (ECDSA with P-256 and SHA-256) JWT signing algorithm.
1 parent cd15dc5 commit dfbfe08

2 files changed

Lines changed: 218 additions & 0 deletions

File tree

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
Class {
2+
#name : 'JWAES256Test',
3+
#superclass : 'TestCase',
4+
#category : 'JSONWebToken-Core-Tests',
5+
#package : 'JSONWebToken-Core-Tests'
6+
}
7+
8+
{ #category : 'tests' }
9+
JWAES256Test >> testInvalidSignature [
10+
11+
| privPem pubPem message signature parts |
12+
privPem := '-----BEGIN EC PRIVATE KEY-----
13+
MHcCAQEEIJMAmRbBAEzALqgw+fnF1iPFRXfeQO/3kKzw0Fr0kiVGoAoGCCqGSM49
14+
AwEHoUQDQgAEAXizW3CKL9NeATTYgWuD7cE4s6F7Zsd10kEtEwclNb8dXqbx3x/O
15+
x7uN34ptxuwIV4y6QmstfKDbIp9qlgDSfQ==
16+
-----END EC PRIVATE KEY-----'.
17+
pubPem := '-----BEGIN PUBLIC KEY-----
18+
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAXizW3CKL9NeATTYgWuD7cE4s6F7
19+
Zsd10kEtEwclNb8dXqbx3x/Ox7uN34ptxuwIV4y6QmstfKDbIp9qlgDSfQ==
20+
-----END PUBLIC KEY-----'.
21+
22+
parts := {
23+
(Base64UrlEncoder new encode: 'header' asByteArray).
24+
(Base64UrlEncoder new encode: 'payload' asByteArray).
25+
'' }.
26+
message := parts first , '.' , parts second.
27+
signature := LargoJWAES256 signMessage: message withKey: privPem.
28+
29+
"Tamper with signature"
30+
signature at: 1 put: (signature at: 1) + 1 \\ 256.
31+
32+
parts at: 3 put: (Base64UrlEncoder new encode: signature).
33+
34+
self should: [ LargoJWAES256 checkSignatureOfParts: parts withKey: pubPem ] raise: Error
35+
]
36+
37+
{ #category : 'tests' }
38+
JWAES256Test >> testSignAndVerify [
39+
40+
| privPem pubPem message signature parts |
41+
privPem := '-----BEGIN EC PRIVATE KEY-----
42+
MHcCAQEEIJMAmRbBAEzALqgw+fnF1iPFRXfeQO/3kKzw0Fr0kiVGoAoGCCqGSM49
43+
AwEHoUQDQgAEAXizW3CKL9NeATTYgWuD7cE4s6F7Zsd10kEtEwclNb8dXqbx3x/O
44+
x7uN34ptxuwIV4y6QmstfKDbIp9qlgDSfQ==
45+
-----END EC PRIVATE KEY-----'.
46+
pubPem := '-----BEGIN PUBLIC KEY-----
47+
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAXizW3CKL9NeATTYgWuD7cE4s6F7
48+
Zsd10kEtEwclNb8dXqbx3x/Ox7uN34ptxuwIV4y6QmstfKDbIp9qlgDSfQ==
49+
-----END PUBLIC KEY-----'.
50+
51+
parts := {
52+
(Base64UrlEncoder new encode: 'header' asByteArray).
53+
(Base64UrlEncoder new encode: 'payload' asByteArray).
54+
'' }.
55+
message := parts first , '.' , parts second.
56+
signature := LargoJWAES256 signMessage: message withKey: privPem.
57+
self assert: signature size equals: 64.
58+
59+
parts at: 3 put: (Base64UrlEncoder new encode: signature).
60+
LargoJWAES256 checkSignatureOfParts: parts withKey: pubPem
61+
]
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
"
2+
I am an implementation of the ES256 (ECDSA with P-256 and SHA-256) JWT signing algorithm.
3+
4+
I use the OpenSSL EVP interface through LcEvpPublicKey for signing and verification.
5+
Since the ES256 standard requires the signature to be a 64-byte concatenation of R and S, and OpenSSL uses the DER format, I provide the necessary conversion logic.
6+
7+
Example usage:
8+
```pharo
9+
signature := JWAES256 signMessage: 'message' withKey: ecPrivatePemKey.
10+
JWAES256 checkSignatureOfParts: { header . payload . signature } withKey: ecPublicPemKey.
11+
```
12+
"
13+
Class {
14+
#name : 'JWAES256',
15+
#superclass : 'JsonWebAlgorithm',
16+
#category : 'JSONWebToken-Core-Algorithms',
17+
#package : 'JSONWebToken-Core',
18+
#tag : 'Algorithms'
19+
}
20+
21+
{ #category : 'sign' }
22+
JWAES256 class >> checkSignatureOfParts: parts withKey: key [
23+
24+
| jwtHeaderAndPayload signatureByteArray publicKey derSignature |
25+
jwtHeaderAndPayload := $. join: {
26+
parts first.
27+
parts second }.
28+
signatureByteArray := Base64UrlEncoder new decode: parts third base64Padded.
29+
30+
"ES256 signature is 64 bytes (R | S). OpenSSL needs it in DER format."
31+
derSignature := self rsToDer: signatureByteArray.
32+
33+
publicKey := LcEvpPublicKey fromPublicKeyPemString: key.
34+
35+
jwtHeaderAndPayload pinInMemory.
36+
derSignature pinInMemory.
37+
[
38+
(publicKey digestVerifyMessage: jwtHeaderAndPayload asByteArray with: derSignature)
39+
ifFalse: [ Error signal: 'signature does not match' ] ] ensure: [
40+
jwtHeaderAndPayload unpinInMemory.
41+
derSignature unpinInMemory ]
42+
]
43+
44+
{ #category : 'private' }
45+
JWAES256 class >> copyInteger: source into: target startingAt: targetOffset [
46+
47+
| srcOffset len |
48+
srcOffset := 1.
49+
len := source size.
50+
"Strip leading zeros if it makes it longer than 32"
51+
[ len > 32 and: [ (source at: srcOffset) = 0 ] ] whileTrue: [
52+
srcOffset := srcOffset + 1.
53+
len := len - 1 ].
54+
55+
"If still longer than 32, it's an error for ES256 (P-256)"
56+
len > 32 ifTrue: [ Error signal: 'Integer too large for ES256' ].
57+
58+
"Copy and pad with leading zeros if needed"
59+
target
60+
replaceFrom: targetOffset + (32 - len)
61+
to: targetOffset + 31
62+
with: source
63+
startingAt: srcOffset
64+
]
65+
66+
{ #category : 'private' }
67+
JWAES256 class >> derIntegerFor: aByteArray [
68+
69+
| firstByte srcOffset |
70+
srcOffset := 1.
71+
"Strip leading zeros"
72+
[ srcOffset < aByteArray size and: [ (aByteArray at: srcOffset) = 0 ] ] whileTrue: [
73+
srcOffset := srcOffset + 1 ].
74+
75+
firstByte := aByteArray at: srcOffset.
76+
firstByte > 127 ifTrue: [
77+
^ #[ 0 ] , (aByteArray copyFrom: srcOffset to: aByteArray size) ].
78+
^ aByteArray copyFrom: srcOffset to: aByteArray size
79+
]
80+
81+
{ #category : 'private' }
82+
JWAES256 class >> derToRS: derSignature [
83+
84+
| r s offset lenR lenS rs |
85+
"DER: 30 L 02 LR R 02 LS S"
86+
(derSignature at: 1) = 16r30 ifFalse: [ Error signal: 'Invalid DER signature' ].
87+
offset := 3.
88+
(derSignature at: offset) = 16r02 ifFalse: [ Error signal: 'Invalid DER signature (R)' ].
89+
lenR := derSignature at: offset + 1.
90+
r := derSignature copyFrom: offset + 2 to: offset + 1 + lenR.
91+
92+
offset := offset + 2 + lenR.
93+
(derSignature at: offset) = 16r02 ifFalse: [ Error signal: 'Invalid DER signature (S)' ].
94+
lenS := derSignature at: offset + 1.
95+
s := derSignature copyFrom: offset + 2 to: offset + 1 + lenS.
96+
97+
rs := ByteArray new: 64.
98+
"If R is > 32 bytes (leading zero), strip it. If < 32 bytes, pad it."
99+
self copyInteger: r into: rs startingAt: 1.
100+
self copyInteger: s into: rs startingAt: 33.
101+
102+
^ rs
103+
]
104+
105+
{ #category : 'accessing' }
106+
JWAES256 class >> parameterValue [
107+
108+
^ 'ES256'
109+
]
110+
111+
{ #category : 'private' }
112+
JWAES256 class >> rsToDer: aByteArray [
113+
114+
| r s derR derS result offset |
115+
r := aByteArray copyFrom: 1 to: 32.
116+
s := aByteArray copyFrom: 33 to: 64.
117+
118+
derR := self derIntegerFor: r.
119+
derS := self derIntegerFor: s.
120+
121+
result := ByteArray new: derR size + derS size + 6.
122+
result at: 1 put: 16r30. "Sequence"
123+
result at: 2 put: derR size + derS size + 4.
124+
125+
offset := 3.
126+
result at: offset put: 16r02. "Integer"
127+
result at: offset + 1 put: derR size.
128+
result
129+
replaceFrom: offset + 2
130+
to: offset + 1 + derR size
131+
with: derR
132+
startingAt: 1.
133+
134+
offset := offset + 2 + derR size.
135+
result at: offset put: 16r02. "Integer"
136+
result at: offset + 1 put: derS size.
137+
result
138+
replaceFrom: offset + 2
139+
to: offset + 1 + derS size
140+
with: derS
141+
startingAt: 1.
142+
143+
^ result
144+
]
145+
146+
{ #category : 'sign' }
147+
JWAES256 class >> signMessage: message withKey: anObject [
148+
149+
| pkey derSig |
150+
pkey := LcEvpPublicKey fromPrivateKeyPemString: anObject.
151+
message pinInMemory.
152+
derSig := [ pkey digestSignMessage: message asByteArray ] ensure: [
153+
message unpinInMemory ].
154+
155+
"OpenSSL returns DER format. ES256 requires 64 bytes (R | S)."
156+
^ self derToRS: derSig
157+
]

0 commit comments

Comments
 (0)