Skip to content

Commit b0b0b13

Browse files
authored
Merge pull request #90 from PeculiarVentures/rsa-pss-x509-fix
Fix RSA-PSS no-params URI support and deterministic key extraction for X509 verification
2 parents 2414ff0 + 8c818a1 commit b0b0b13

7 files changed

Lines changed: 227 additions & 34 deletions

File tree

package-lock.json

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

packages/xmldsig/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
"homepage": "https://github.com/PeculiarVentures/xmldsigjs/blob/master/packages/xmldsig/README.md",
6262
"dependencies": {
6363
"asn1js": "^3.0.6",
64-
"pkijs": "^3.3.2",
64+
"pkijs": "^3.4.0",
6565
"pvtsutils": "^1.3.6",
6666
"pvutils": "^1.1.3",
6767
"tslib": "^2.8.1",

packages/xmldsig/src/algorithm.registry.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,19 @@ import {
5252
SHA384_NAMESPACE,
5353
Sha512,
5454
SHA512_NAMESPACE,
55+
56+
// RSA PSS with params
5557
RSA_PSS_WITH_PARAMS_NAMESPACE,
5658
RsaPssWithParams,
59+
// RSA PSS without params
60+
RSA_PSS_SHA1_NAMESPACE,
61+
RSA_PSS_SHA256_NAMESPACE,
62+
RSA_PSS_SHA384_NAMESPACE,
63+
RSA_PSS_SHA512_NAMESPACE,
64+
RsaPssWithoutParamsSha1,
65+
RsaPssWithoutParamsSha256,
66+
RsaPssWithoutParamsSha384,
67+
RsaPssWithoutParamsSha512,
5768
} from './algorithms/index.js';
5869

5970
// Register RSA PKCS1 algorithms
@@ -94,6 +105,23 @@ algorithmRegistry.set(RSA_PSS_WITH_PARAMS_NAMESPACE, {
94105
type: 'signature',
95106
algorithm: RsaPssWithParams,
96107
});
108+
// Register RSA PSS algorithms without params
109+
algorithmRegistry.set(RSA_PSS_SHA1_NAMESPACE, {
110+
type: 'signature',
111+
algorithm: RsaPssWithoutParamsSha1,
112+
});
113+
algorithmRegistry.set(RSA_PSS_SHA256_NAMESPACE, {
114+
type: 'signature',
115+
algorithm: RsaPssWithoutParamsSha256,
116+
});
117+
algorithmRegistry.set(RSA_PSS_SHA384_NAMESPACE, {
118+
type: 'signature',
119+
algorithm: RsaPssWithoutParamsSha384,
120+
});
121+
algorithmRegistry.set(RSA_PSS_SHA512_NAMESPACE, {
122+
type: 'signature',
123+
algorithm: RsaPssWithoutParamsSha512,
124+
});
97125

98126
// ECDSA algorithms
99127
algorithmRegistry.set(ECDSA_SHA1_NAMESPACE, {
Lines changed: 43 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,69 @@
1-
import { SignatureAlgorithm } from '../algorithm.js';
1+
import { ISignatureAlgorithm, SignatureAlgorithm } from '../algorithm.js';
22
import { SHA1, SHA256, SHA384, SHA512 } from './rsa_hash.js';
33
import { RSA_PSS } from './rsa_pss_sign.js';
44

5-
// https://tools.ietf.org/html/rfc6931#section-2.3.10
6-
75
export const RSA_PSS_SHA1_NAMESPACE = 'http://www.w3.org/2007/05/xmldsig-more#sha1-rsa-MGF1';
86
export const RSA_PSS_SHA256_NAMESPACE = 'http://www.w3.org/2007/05/xmldsig-more#sha256-rsa-MGF1';
97
export const RSA_PSS_SHA384_NAMESPACE = 'http://www.w3.org/2007/05/xmldsig-more#sha384-rsa-MGF1';
108
export const RSA_PSS_SHA512_NAMESPACE = 'http://www.w3.org/2007/05/xmldsig-more#sha512-rsa-MGF1';
119

10+
interface RsaPssWithoutParamsAlgorithm extends Algorithm {
11+
name: typeof RSA_PSS;
12+
hash: Algorithm;
13+
saltLength: number;
14+
}
15+
1216
export class RsaPssWithoutParamsBase extends SignatureAlgorithm {
13-
public algorithm: any = {
17+
// No-params URI variants are selected from SignatureMethod URI, not from Algorithm input.
18+
public static fromAlgorithm(_alg: Algorithm): ISignatureAlgorithm | null {
19+
return null;
20+
}
21+
}
22+
23+
export class RsaPssWithoutParamsSha1 extends RsaPssWithoutParamsBase {
24+
public algorithm: RsaPssWithoutParamsAlgorithm = {
1425
name: RSA_PSS,
1526
hash: {
1627
name: SHA1,
1728
},
29+
saltLength: 20,
1830
};
1931

2032
public namespaceURI = RSA_PSS_SHA1_NAMESPACE;
2133
}
2234

23-
export class RsaPssWithoutParamsSha1 extends RsaPssWithoutParamsBase {
24-
constructor() {
25-
super();
26-
this.algorithm.hash.name = SHA1;
27-
this.algorithm.saltLength = 20;
28-
}
29-
}
30-
3135
export class RsaPssWithoutParamsSha256 extends RsaPssWithoutParamsBase {
32-
constructor() {
33-
super();
34-
this.algorithm.hash.name = SHA256;
35-
this.algorithm.saltLength = 32;
36-
}
36+
public algorithm: RsaPssWithoutParamsAlgorithm = {
37+
name: RSA_PSS,
38+
hash: {
39+
name: SHA256,
40+
},
41+
saltLength: 32,
42+
};
43+
44+
public namespaceURI = RSA_PSS_SHA256_NAMESPACE;
3745
}
3846

3947
export class RsaPssWithoutParamsSha384 extends RsaPssWithoutParamsBase {
40-
constructor() {
41-
super();
42-
this.algorithm.hash.name = SHA384;
43-
this.algorithm.saltLength = 48;
44-
}
48+
public algorithm: RsaPssWithoutParamsAlgorithm = {
49+
name: RSA_PSS,
50+
hash: {
51+
name: SHA384,
52+
},
53+
saltLength: 48,
54+
};
55+
56+
public namespaceURI = RSA_PSS_SHA384_NAMESPACE;
4557
}
4658

4759
export class RsaPssWithoutParamsSha512 extends RsaPssWithoutParamsBase {
48-
constructor() {
49-
super();
50-
this.algorithm.hash.name = SHA512;
51-
this.algorithm.saltLength = 64;
52-
}
60+
public algorithm: RsaPssWithoutParamsAlgorithm = {
61+
name: RSA_PSS,
62+
hash: {
63+
name: SHA512,
64+
},
65+
saltLength: 64,
66+
};
67+
68+
public namespaceURI = RSA_PSS_SHA512_NAMESPACE;
5369
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { describe, it, assert } from 'vitest';
2+
import '../test/config.js';
3+
import * as xmldsig from './index.js';
4+
5+
class TestSignedXml extends xmldsig.SignedXml {
6+
public getPublicKeys(): Promise<CryptoKey[]> {
7+
return this.GetPublicKeys();
8+
}
9+
}
10+
11+
function createSignedXmlWithMethod(method: xmldsig.SignatureMethod): TestSignedXml {
12+
const doc = xmldsig.Parse('<root xmlns:ds="http://www.w3.org/2000/09/xmldsig#" />');
13+
const signedXml = new TestSignedXml(doc);
14+
signedXml.XmlSignature.SignedInfo.SignatureMethod = method;
15+
return signedXml;
16+
}
17+
18+
async function createRsaPssPublicKey(): Promise<CryptoKey> {
19+
const keyPair = (await xmldsig.Application.crypto.subtle.generateKey(
20+
{
21+
name: 'RSA-PSS',
22+
hash: 'SHA-256',
23+
modulusLength: 2048,
24+
publicExponent: new Uint8Array([1, 0, 1]),
25+
},
26+
true,
27+
['sign', 'verify'],
28+
)) as CryptoKeyPair;
29+
30+
return keyPair.publicKey;
31+
}
32+
33+
function addX509ExportStub(signedXml: xmldsig.SignedXml, publicKey: CryptoKey) {
34+
let seenAlgorithm: Algorithm | undefined;
35+
const cert = {
36+
exportKey: async (alg?: Algorithm) => {
37+
seenAlgorithm = alg;
38+
return publicKey;
39+
},
40+
} as xmldsig.X509Certificate;
41+
42+
const x509Data = new xmldsig.KeyInfoX509Data();
43+
x509Data.AddCertificate(cert);
44+
signedXml.XmlSignature.KeyInfo.Add(x509Data);
45+
46+
return (): Algorithm | undefined => seenAlgorithm;
47+
}
48+
49+
describe('SignedXml', () => {
50+
describe('GetPublicKeys', () => {
51+
it('passes the resolved signature algorithm to X509 certificate export', async () => {
52+
const algorithm = { name: 'RSA-PSS', hash: { name: 'SHA-256' }, saltLength: 32 } as Algorithm;
53+
const signatureAlgorithm = xmldsig.CryptoConfig.GetSignatureAlgorithm(algorithm);
54+
const method = xmldsig.CryptoConfig.CreateSignatureMethod(signatureAlgorithm);
55+
const signedXml = createSignedXmlWithMethod(method);
56+
const publicKey = await createRsaPssPublicKey();
57+
const getSeenAlgorithm = addX509ExportStub(signedXml, publicKey);
58+
59+
const keys = await signedXml.getPublicKeys();
60+
const seenAlgorithm = getSeenAlgorithm();
61+
62+
assert.equal(keys.length, 1);
63+
assert.equal(seenAlgorithm?.name, 'RSA-PSS');
64+
assert.equal((seenAlgorithm as RsaHashedImportParams).hash.name, 'SHA-256');
65+
assert.equal((seenAlgorithm as RsaPssParams).saltLength, 32);
66+
});
67+
68+
it('supports RSA-PSS no-params URI (sha256-rsa-MGF1) for X509 certificate export', async () => {
69+
const method = xmldsig.CryptoConfig.CreateSignatureMethod(
70+
new xmldsig.RsaPssWithoutParamsSha256(),
71+
);
72+
const signedXml = createSignedXmlWithMethod(method);
73+
const publicKey = await createRsaPssPublicKey();
74+
const getSeenAlgorithm = addX509ExportStub(signedXml, publicKey);
75+
76+
const keys = await signedXml.getPublicKeys();
77+
const seenAlgorithm = getSeenAlgorithm();
78+
79+
assert.equal(keys.length, 1);
80+
assert.equal(
81+
signedXml.XmlSignature.SignedInfo.SignatureMethod.Algorithm,
82+
xmldsig.RSA_PSS_SHA256_NAMESPACE,
83+
);
84+
assert.equal(signedXml.XmlSignature.SignedInfo.SignatureMethod.Any.Count, 0);
85+
assert.equal(seenAlgorithm?.name, 'RSA-PSS');
86+
assert.equal((seenAlgorithm as RsaHashedImportParams).hash.name, 'SHA-256');
87+
assert.equal((seenAlgorithm as RsaPssParams).saltLength, 32);
88+
});
89+
});
90+
});

packages/xmldsig/src/signed_xml.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -286,11 +286,11 @@ export class SignedXml implements IXmlSerializable {
286286
for (const kic of this.XmlSignature.KeyInfo.GetIterator()) {
287287
if (kic instanceof KeyInfos.KeyInfoX509Data) {
288288
for (const cert of kic.Certificates) {
289-
const key = await cert.exportKey();
289+
const key = await cert.exportKey(alg.algorithm);
290290
keys.push(key);
291291
}
292292
} else {
293-
const key = await kic.exportKey();
293+
const key = await kic.exportKey(alg.algorithm);
294294
keys.push(key);
295295
}
296296
}

packages/xmldsig/test/rsa_pss.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,63 @@ describe('RSA-PSS', () => {
111111
});
112112
});
113113
});
114+
115+
describe('No params URI', () => {
116+
const vectors = [
117+
{
118+
hash: 'SHA-1',
119+
saltLength: 20,
120+
namespace: xmldsig.RSA_PSS_SHA1_NAMESPACE,
121+
algorithm: xmldsig.RsaPssWithoutParamsSha1,
122+
},
123+
{
124+
hash: 'SHA-256',
125+
saltLength: 32,
126+
namespace: xmldsig.RSA_PSS_SHA256_NAMESPACE,
127+
algorithm: xmldsig.RsaPssWithoutParamsSha256,
128+
},
129+
{
130+
hash: 'SHA-384',
131+
saltLength: 48,
132+
namespace: xmldsig.RSA_PSS_SHA384_NAMESPACE,
133+
algorithm: xmldsig.RsaPssWithoutParamsSha384,
134+
},
135+
{
136+
hash: 'SHA-512',
137+
saltLength: 64,
138+
namespace: xmldsig.RSA_PSS_SHA512_NAMESPACE,
139+
algorithm: xmldsig.RsaPssWithoutParamsSha512,
140+
},
141+
] as const;
142+
143+
vectors.forEach((vector) => {
144+
it(`resolves ${vector.hash} namespace and signs/verifies`, async () => {
145+
const method = xmldsig.CryptoConfig.CreateSignatureMethod(new vector.algorithm());
146+
assert.equal(method.Algorithm, vector.namespace);
147+
assert.equal(method.Any.Count, 0);
148+
149+
const resolved = xmldsig.CryptoConfig.CreateSignatureAlgorithm(method);
150+
assert.equal(resolved.algorithm.name, 'RSA-PSS');
151+
assert.equal((resolved.algorithm as RsaHashedImportParams).hash.name, vector.hash);
152+
assert.equal((resolved.algorithm as RsaPssParams).saltLength, vector.saltLength);
153+
154+
const algorithm = new vector.algorithm();
155+
const data = '<SignedInfo />';
156+
const keys = (await xmldsig.Application.crypto.subtle.generateKey(
157+
{
158+
name: 'RSA-PSS',
159+
hash: vector.hash,
160+
modulusLength: 2048,
161+
publicExponent: new Uint8Array([1, 0, 1]),
162+
},
163+
true,
164+
['sign', 'verify'],
165+
)) as Required<CryptoKeyPair>;
166+
167+
const signature = await algorithm.Sign(data, keys.privateKey, algorithm.algorithm);
168+
const ok = await algorithm.Verify(data, keys.publicKey, new Uint8Array(signature));
169+
assert.equal(ok, true);
170+
});
171+
});
172+
});
114173
});

0 commit comments

Comments
 (0)