Skip to content
This repository was archived by the owner on Mar 8, 2024. It is now read-only.

Commit 441a744

Browse files
authored
Server keys (#19)
* functions and types for signing and validating a VerifiablePayID * get pem and x5c working simplify verify call to use Embedded option * lint fixes and refactored SigningParams types from interface to classes * exports * packages.json cleanup * WIP: get PKI with certificate chain validation workiing * lint fixes use generated Root CA, Intermediate CA, and server cert in tests * add 'name' to crit section * splitting web-pki into multiple branches * add TODO comment on types
1 parent fa4bb26 commit 441a744

20 files changed

+549
-128
lines changed

package-lock.json

Lines changed: 20 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,16 @@
2424
"test": "nyc mocha 'test/**/*.test.ts'"
2525
},
2626
"dependencies": {
27-
"jose": "^1.27.2"
27+
"jose": "^1.27.3",
28+
"node-forge": "^0.9.1"
2829
},
2930
"devDependencies": {
3031
"@arkweid/lefthook": "^0.7.2",
3132
"@fintechstudios/eslint-plugin-chai-as-promised": "^3.0.2",
3233
"@types/chai": "^4.2.11",
3334
"@types/mocha": "^7.0.2",
3435
"@types/node": "^14.0.14",
36+
"@types/node-forge": "^0.9.4",
3537
"@types/pem-jwk": "^1.5.0",
3638
"@typescript-eslint/eslint-plugin": "^3.7.1",
3739
"@typescript-eslint/parser": "^3.7.1",
@@ -52,7 +54,7 @@
5254
"prettier": "^2.0.5",
5355
"ts-node": "^8.10.2",
5456
"tsc-watch": "^4.2.9",
55-
"typescript": "^3.9.5"
57+
"typescript": "^3.9.7"
5658
},
5759
"engines": {
5860
"node": ">=12.0.0",

src/verifiable/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
1+
export * from './keys'
2+
export { default as IdentityKeySigningParams } from './identity-key-signing-params'
3+
export { default as ServerKeySigningParams } from './server-key-signing-params'
14
export * from './signatures'
25
export * from './verifiable-payid'

src/verifiable/keys.ts

Lines changed: 109 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,128 @@
11
import { promises } from 'fs'
22

3-
import { JWK } from 'jose'
3+
import {
4+
BasicParameters,
5+
JWK,
6+
JWKECKey,
7+
JWKOctKey,
8+
JWKOKPKey,
9+
JWKRSAKey,
10+
JWS,
11+
KeyParameters,
12+
} from 'jose'
413

514
import RSAKey = JWK.RSAKey
615
import ECKey = JWK.ECKey
716
import OKPKey = JWK.OKPKey
817
import OctKey = JWK.OctKey
18+
import JWSRecipient = JWS.JWSRecipient
19+
20+
const CERT_HEADER = '-----BEGIN CERTIFICATE-----'
21+
const CERT_TRAILER = '-----END CERTIFICATE-----'
922

1023
/**
1124
* Reads JWK key from a file.
1225
*
1326
* @param path - The full file path of the key file.
1427
* @returns A JWK key.
1528
*/
16-
export default async function getKeyFromFile(
29+
export async function getSigningKeyFromFile(
1730
path: string,
1831
): Promise<RSAKey | ECKey | OKPKey | OctKey> {
1932
const pem = await promises.readFile(path, 'ascii')
2033
return JWK.asKey(pem)
2134
}
35+
36+
/**
37+
* Reads JWK from a file. If file contains a chain of certificates, JWK is generated from the first
38+
* certificate and the rest of the certificates in file are added to the x5c section of the JWK.
39+
*
40+
* @param path - The full file path of the key file.
41+
* @returns A JWK key.
42+
*/
43+
export async function getJwkFromFile(
44+
path: string,
45+
): Promise<JWKRSAKey | JWKECKey | JWKOKPKey | JWKOctKey> {
46+
const content = await promises.readFile(path, 'ascii')
47+
// in the case of a fullchain cert being used, the content will contain the end certificate followed by 1
48+
// or more intermediate CA certs. The end certificate (index 0) is the one to be manifested as the public key
49+
// in the JWS headers. The full chain of certificates will be included in the x5c section.
50+
const certs = splitCerts(content)
51+
const primary = JWK.asKey(certs[0]).toJWK(false)
52+
const x5c = getX5cChain(certs.map((part) => JWK.asKey(part).toJWK(false)))
53+
if (isX5C(primary)) {
54+
return { ...primary, x5c }
55+
}
56+
return primary
57+
}
58+
59+
/**
60+
* Extracts the JWK property from the base64 json in the protected section of a JWK recipient.
61+
*
62+
* @param recipient - The recipient to process.
63+
* @returns The JWK if found, otherwise undefined.
64+
*/
65+
export function getJwkFromRecipient(
66+
recipient: JWSRecipient,
67+
): JWKRSAKey | JWKECKey | JWKOKPKey | JWKOctKey | undefined {
68+
if (recipient.protected) {
69+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- because JSON
70+
const headers = JSON.parse(
71+
Buffer.from(recipient.protected, 'base64').toString('utf-8'),
72+
)
73+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- because JSON
74+
if (headers.jwk) {
75+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- because JSON
76+
return JWK.asKey(headers.jwk).toJWK(false)
77+
}
78+
}
79+
return undefined
80+
}
81+
82+
/**
83+
* Split of full chain certificate (which contains multiple certificate blocks) into an array
84+
* of strings.
85+
*
86+
* @param text - Text content of the certificate chain.
87+
* @returns The list of certificates.
88+
*/
89+
export function splitCerts(text: string): string[] {
90+
let parts: string[] = []
91+
let startIndex = text.indexOf(CERT_HEADER)
92+
let endIndex = text.indexOf(CERT_TRAILER, startIndex)
93+
while (startIndex >= 0) {
94+
parts = parts.concat(
95+
text.substring(startIndex, endIndex + CERT_TRAILER.length),
96+
)
97+
startIndex = text.indexOf(CERT_HEADER, endIndex + 1)
98+
endIndex = text.indexOf(CERT_TRAILER, startIndex + 1)
99+
}
100+
return parts
101+
}
102+
103+
/**
104+
* Extracts the x5c values from a JWK.
105+
*
106+
* @param keys - The JWK to process.
107+
* @returns Array of values from the x5c fields. Empty if it doesn't exist or is empty.
108+
*/
109+
function getX5cChain(
110+
keys: Array<JWKRSAKey | JWKECKey | JWKOKPKey | JWKOctKey>,
111+
): string[] {
112+
return keys.flatMap((jwk) => {
113+
if (isX5C(jwk) && jwk.x5c) {
114+
return jwk.x5c
115+
}
116+
return []
117+
})
118+
}
119+
120+
/**
121+
* Checks if the params contains is a KeyParameters with an x5c property.
122+
*
123+
* @param params - The value to be checked.
124+
* @returns True if x5c found.
125+
*/
126+
export function isX5C(params: BasicParameters): params is KeyParameters {
127+
return 'x5c' in params
128+
}

src/verifiable/server-key-signing-params.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { JWK } from 'jose'
1+
import { JWK, JWKECKey, JWKOctKey, JWKOKPKey, JWKRSAKey } from 'jose'
22

33
import { SigningParams } from './verifiable-payid'
44

@@ -14,22 +14,22 @@ export default class ServerKeySigningParams implements SigningParams {
1414
public readonly keyType = 'serverKey'
1515
public readonly key: ECKey | RSAKey | OctKey | OKPKey
1616
public readonly alg: string
17-
public readonly x5c: ECKey | RSAKey | OctKey | OKPKey
17+
public readonly jwk: JWKRSAKey | JWKECKey | JWKOKPKey | JWKOctKey
1818

1919
/**
2020
* Default constructor.
2121
*
2222
* @param key - The private key to sign with.
2323
* @param alg - The signing algorithm.
24-
* @param x5c - The public x509 certificate.
24+
* @param jwk - The public jwk to include in the jws.
2525
*/
2626
public constructor(
2727
key: ECKey | RSAKey | OctKey | OKPKey,
2828
alg: string,
29-
x5c: ECKey | RSAKey | OctKey | OKPKey,
29+
jwk: JWKRSAKey | JWKECKey | JWKOKPKey | JWKOctKey,
3030
) {
3131
this.key = key
3232
this.alg = alg
33-
this.x5c = x5c
33+
this.jwk = jwk
3434
}
3535
}

src/verifiable/signatures.ts

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { JWS, JWK } from 'jose'
22

33
import IdentityKeySigningParams from './identity-key-signing-params'
44
import ServerKeySigningParams from './server-key-signing-params'
5-
import { Address } from './verifiable-payid'
5+
import { Address, PaymentInformation } from './verifiable-payid'
66

77
import GeneralJWS = JWS.GeneralJWS
88

@@ -44,15 +44,15 @@ function signWithIdentityKey(
4444
}
4545

4646
const signer = new JWS.Sign(unsigned)
47-
const publicKey = signingParams.key.toJWK(false)
47+
const jwk = signingParams.key.toJWK(false)
4848

4949
const protectedHeaders = {
5050
name: 'identityKey',
5151
alg: signingParams.alg,
5252
typ: 'JOSE+JSON',
5353
b64: false,
54-
crit: ['b64'],
55-
jwk: publicKey,
54+
crit: ['b64', 'name'],
55+
jwk,
5656
}
5757

5858
signer.recipient(signingParams.key, protectedHeaders)
@@ -84,8 +84,8 @@ export function signWithServerKey(
8484
alg: signingParams.alg,
8585
typ: 'JOSE+JSON',
8686
b64: false,
87-
crit: ['b64'],
88-
jwk: signingParams.x5c,
87+
crit: ['b64', 'name'],
88+
jwk: signingParams.jwk,
8989
}
9090

9191
signer.recipient(signingParams.key, protectedHeaders)
@@ -118,6 +118,32 @@ export function signWithKeys(
118118
})
119119
}
120120

121+
/**
122+
* Verifies a PayID using the verified addresses within the PayID.
123+
*
124+
* @param toVerify - The PayID (as a json or as a parsed PaymentInformation).
125+
*
126+
* @returns True if verified.
127+
*/
128+
export function verifyPayId(toVerify: string | PaymentInformation): boolean {
129+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- because JSON
130+
const paymentInformation: PaymentInformation =
131+
typeof toVerify === 'string' ? JSON.parse(toVerify) : toVerify
132+
133+
const payId = paymentInformation.payId
134+
if (payId) {
135+
return paymentInformation.verifiedAddresses
136+
.map((address) =>
137+
verifySignedAddress(payId, {
138+
payload: address.payload,
139+
signatures: address.signatures.slice(),
140+
}),
141+
)
142+
.every((value) => value)
143+
}
144+
return false
145+
}
146+
121147
/**
122148
* Verify an address is properly signed.
123149
*
@@ -127,19 +153,25 @@ export function signWithKeys(
127153
*/
128154
export function verifySignedAddress(
129155
expectedPayId: string,
130-
verifiedAddress: GeneralJWS,
156+
verifiedAddress: GeneralJWS | string,
131157
): boolean {
132158
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- because JSON
133-
const address: UnsignedVerifiedAddress = JSON.parse(verifiedAddress.payload)
159+
const jws: GeneralJWS =
160+
typeof verifiedAddress === 'string'
161+
? JSON.parse(verifiedAddress)
162+
: verifiedAddress
163+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- because JSON
164+
const address: UnsignedVerifiedAddress = JSON.parse(jws.payload)
134165

135166
if (expectedPayId !== address.payId) {
136167
// payId does not match what was inside the signed payload
137168
return false
138169
}
139170

140171
try {
141-
JWS.verify(verifiedAddress, JWK.EmbeddedJWK, {
142-
crit: ['b64'],
172+
// verifies signatures
173+
JWS.verify(jws, JWK.EmbeddedJWK, {
174+
crit: ['b64', 'name'],
143175
complete: true,
144176
})
145177
return true

0 commit comments

Comments
 (0)