Skip to content

Commit 1b69c68

Browse files
authored
Merge pull request #893 from nextcloud/artonge/fix/user_certificate_validation_sha-1
fix: Support sha-1 algo for user certificate signature
2 parents 78b58b8 + 245eb26 commit 1b69c68

11 files changed

+49
-26
lines changed

js/end_to_end_encryption-files.mjs

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

js/end_to_end_encryption-files.mjs.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/services/api.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,11 @@ export async function getPrivateKey(): Promise<PrivateKeyInfo> {
3535
}
3636
}
3737

38-
export async function getServerPublicKey(): Promise<CryptoKey> {
38+
export async function getServerPublicKey(): Promise<string> {
3939
const response = await axios.get<OCSResponse<{'public-key': string}>>(
4040
generateOcsUrl(Url.ServerKey),
4141
{ headers: { 'X-E2EE-SUPPORTED': 'true' } },
4242
)
4343

44-
return await loadServerPublicKey(pemToBuffer(response.data.ocs.data['public-key']))
44+
return await response.data.ocs.data['public-key']
4545
}

src/services/crypto.spec.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,29 @@
66
import { expect, test } from 'vitest'
77

88
import { base64ToBuffer } from './bufferUtils.ts'
9-
import { sha256Hash } from './crypto.ts'
9+
import { sha256Hash, validateCertificateSignature } from './crypto.ts'
1010

1111
test('sha256Hash correctly returns a hex string', async () => {
1212
const buffer = 'KPJswKr0owRxrcj4/3SRIw=='
1313
const hash = '9a60be9846978884033fcdfb978fbdd428221b20583bca6bfcb425f1b540152a'
1414
const computedHash = await sha256Hash(base64ToBuffer(buffer))
1515
expect(computedHash).toEqual(hash)
1616
})
17+
18+
test('Validate user certificate signed with SHA-1', async () => {
19+
const certificate = '-----BEGIN CERTIFICATE-----\nMIIDlDCCAnygAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJERTEb\nMBkGA1UECAwSQmFkZW4tV3VlcnR0ZW1iZXJnMRIwEAYDVQQHDAlTdHV0dGdhcnQx\nEjAQBgNVBAoMCU5leHRjbG91ZDEPMA0GA1UEAwwGbG91aXNjMB4XDTI1MDIwNTEw\nNDYxN1oXDTQ1MDEzMTEwNDYxN1owYzELMAkGA1UEBhMCREUxGzAZBgNVBAgMEkJh\nZGVuLVd1ZXJ0dGVtYmVyZzESMBAGA1UEBwwJU3R1dHRnYXJ0MRIwEAYDVQQKDAlO\nZXh0Y2xvdWQxDzANBgNVBAMMBmxvdWlzYzCCASIwDQYJKoZIhvcNAQEBBQADggEP\nADCCAQoCggEBAMppd4np6dzg/QGbC8nuKM/gvU4eEL/AqnuygaKaEsGtvgcdeDKe\nO2gtok4zPoH6soCi3AznEkcvnzg3p7UrSxA9a59DSnY9b9468tY+2LFqwjQIDNef\ngcdyBxLMOA1rOJ5FV18+XHq5D8Hl0172frcyXlSYfxvg7fxE91v1IJecF0jeca9v\nCW3zTu5VA70waZrQPWUJg3QodfYX6bKV968+S66BI0hr/KGu3cgR4ASE9Fos+RnW\nrxkstlblp81RBMjP+wcHlRuGWXeqcxVWV6Ky74G5WbY1eUpUmzxCrIa5lY1P6kpm\n9pWuFz1Tj28fES7He3TtSFpx+HGWeBRKglUCAwEAAaNTMFEwHQYDVR0OBBYEFAYJ\nS6Fhq18DHpOAI5iA3Z9LNxn+MB8GA1UdIwQYMBaAFAYJS6Fhq18DHpOAI5iA3Z9L\nNxn+MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAKPMbkLSQDnG\nz3iDvPxkxJ0vDurSVVpyajbyj6L2/Nw5qMfRIiI64XD7iVDzRoAJrH62hpn1L0a4\nCSNF7PDabtuDEkcVDWx1Dhk+IsF2rXlWGYva8SnFkDRS2RUenzYCzdlB0YsLY1/g\nRWHUmm0d7jn7ai/h1T89txXCO0NGkl/D/H8G5pwu55DZ6RhCz7RvBLm4H0qFun1m\nr/ZJtNtRi4nu40utPZIDjAXFCZjbu+IjCMSBQ5R3nIpd3Qpjqf+LZcqUzr5aeANy\nq2mmbO+i+RjD8pNICh/px3j97XcgZF8klU8fO1TvOrPSUZ6aa0pk/ngR7uqdM/ng\nptl32lAcZwQ=\n-----END CERTIFICATE-----'
20+
const rawPublicKey = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5e394Y8Cp2lI8DKE+jE1\nRD14o933p9mXfSKRdnLOgDnGEZfdL4OeALAS7PArysL5GMNG+UdaTxYLcoYe4osM\nUpp9WjJYpfGutZh3CSDZXLYlwSIzRwznAaymsK0yN+ujT+sQIQrbfGQ2BMNyxJy+\naRC/W3JnKVTCtrIM6E7QpZ2lwo0DJ/a1Ahyaphm1+A7OX1Wwgh69gzpCjG1KRti9\nvbPdKVSYQpMZ4he+tzDUXZK6GLHKX69ZEBNc6ZPKSDW67r232NduoHxf2lS/723p\ndkBpHiZGTTGVFGfTQBUW215+UhCm5vHX1mNHw2gN2IPi0o+mfvVKPbjuI+dQtcYg\n5wIDAQAB\n-----END PUBLIC KEY-----'
21+
22+
const result = await validateCertificateSignature(certificate, rawPublicKey)
23+
24+
expect(result).toBeTruthy()
25+
})
26+
27+
test('Validate user certificate signed with SHA-256', async () => {
28+
const certificate = '-----BEGIN CERTIFICATE-----\nMIIDkjCCAnqgAwIBAgIBADANBgkqhkiG9w0BAQsFADBiMQswCQYDVQQGEwJERTEb\nMBkGA1UECAwSQmFkZW4tV3VlcnR0ZW1iZXJnMRIwEAYDVQQHDAlTdHV0dGdhcnQx\nEjAQBgNVBAoMCU5leHRjbG91ZDEOMAwGA1UEAwwFYWRtaW4wHhcNMjUwMjA1MTYx\nMjE3WhcNNDUwMTMxMTYxMjE3WjBiMQswCQYDVQQGEwJERTEbMBkGA1UECAwSQmFk\nZW4tV3VlcnR0ZW1iZXJnMRIwEAYDVQQHDAlTdHV0dGdhcnQxEjAQBgNVBAoMCU5l\neHRjbG91ZDEOMAwGA1UEAwwFYWRtaW4wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw\nggEKAoIBAQCJjQj0CBVpalWd4FBj3BVUlhZf3GQITP4jzIjtUsOf7CF+wF3zSTt2\nxTQsIiu3MvsaR1BE+8AdTuttq5PUFp/DgsHHI/s3jNX01oMQxZfUoR9g0HgV1EeD\n/KXqi+iIxWx5Lyqg8YQClCeq3OjXhGAnSHqgeV+nCCYOaOHsLgHGkPrgsGbDXjKB\nbn8iT6UrKuz1LBVFX48lYFXPft1515fNLF534mj8bbc9FSopIgWF/8Bky4NqYALc\n3tKl5MP8OyWCDfMHgXOUXKHij2rv8UONMJOG6LuxS7agSRlb/cF6eEwcvNhvQE8j\nFynWxotZgq+wdQXzxkJ70escOWp9vFCxAgMBAAGjUzBRMB0GA1UdDgQWBBRcWI5h\naCLM4pjAD5/Ve9zHjokIXDAfBgNVHSMEGDAWgBRcWI5haCLM4pjAD5/Ve9zHjokI\nXDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQC1fCDirq3dJ/hi\nmHQakF/Jl2PrVdMkaDFjparrJgoBkQUPo4DymxAIQ2s5k0xMn9hSBgb+G5USSfQo\nZxKIAaKLXPlwSqL9pntHBRyj/dy0TtTeCR2J0/hEC699Ud50tV0NjyTA4qd/Dzeg\nVmn2lQ/ixzLCarWwFLAx1/UoD/AuWTku3apjK/FW9N81zlSHENelriBjQrv9CpJA\n4I6qFAKvviDBw7Y/+VbOKjAlm8eftu5BYJMIAMH8e84PbD1YW9dJ/bnWLCWIYs1Q\npFuoSm19qirT01ZvuDSt3j4zqPiYwBtsJog5YtJd+ZsRJJrpZ94a5gTVHzfPUzOC\nIw9iHRL1\n-----END CERTIFICATE-----'
29+
const rawPublicKey = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxXvMFtdhOhwcug/cv+lE\njy2GeHiWXM3490EvJFrp/udRsPVXl5aNABKi4/KOfev025f+j9t9tvHi1NfNymK2\nJnqUK+0c5mk3iiQa5hAXmj/Yc1BMPNCzWlKF++QQ6W2l9VCYyHzTG3dhRCG0xWjw\nz8NU+5DZmV80HdocR9UU0O5ZcWYYWN0+7QZ8pC7IU7XtQLazb5bhVxpqDQBNEdUC\n4lNrZFjpl3e7wJ/MY5f6otMBlPh6xVmnwYD8BVrNtZ9n5kKSGfJq5VbQd8dhYfHI\nc2bLJprs+dvk/1+j3PQ3OSOcxNmfyXYuqCl1D6kTesq3gTvnXXZeDYBOjdPy+HOR\ngQIDAQAB\n-----END PUBLIC KEY-----'
30+
31+
const result = await validateCertificateSignature(certificate, rawPublicKey)
32+
33+
expect(result).toBeTruthy()
34+
})

src/services/crypto.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,13 @@ export async function loadAESPrivateKey(key: Uint8Array): Promise<CryptoKey> {
5555
)
5656
}
5757

58-
export async function loadServerPublicKey(key: Uint8Array): Promise<CryptoKey> {
58+
export async function loadServerPublicKey(key: Uint8Array, hash: 'SHA-1'|'SHA-256'): Promise<CryptoKey> {
5959
return await self.crypto.subtle.importKey(
6060
'spki',
6161
key,
6262
{
6363
name: 'RSASSA-PKCS1-v1_5',
64-
hash: 'SHA-256', // TODO: get from server?
64+
hash,
6565
},
6666
true,
6767
['verify'],
@@ -98,8 +98,12 @@ export async function sha256Hash(buffer: Uint8Array): Promise<string> {
9898
return bufferToHex(new Uint8Array(hashBuffer))
9999
}
100100

101-
export async function validateCertificateSignature(certificate: string, publicKey: CryptoKey): Promise<boolean> {
101+
export async function validateCertificateSignature(certificate: string, publicKeyPEM: string): Promise<boolean> {
102102
const cert = new X509Certificate(certificate)
103+
const publicKey = await loadServerPublicKey(
104+
pemToBuffer(publicKeyPEM),
105+
cert.signatureAlgorithm.hash.name as 'SHA-1'|'SHA-256',
106+
)
103107

104108
return cert.verify({ publicKey }, getPatchedCrypto())
105109
}

src/services/security.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ test('Users certificates validation against server public key works', async () =
3232
await expect(validateUserCertificates(rootFolderMetadata, await getServerPublicKey())).resolves.toEqual([true, true])
3333
})
3434

35-
test('Users certificates validation against server public key works', async () => {
35+
test('Altered users certificates validation against server public key does not works', async () => {
3636
rootFolderMetadata.users[0].certificate = rootFolderMetadata.users[0].certificate.replace('a', 'b')
3737
await expect(validateUserCertificates(rootFolderMetadata, await getServerPublicKey())).rejects.toThrow()
3838
})

src/services/security.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ export async function validateMetadataSignature(metadata: Metadata, signature: s
2929
return verificationResult
3030
}
3131

32-
export async function validateUserCertificates(metadata: RootMetadata, serverPublicKey: CryptoKey): Promise<true[]> {
32+
export async function validateUserCertificates(metadata: RootMetadata, serverPublicKeyPEM: string): Promise<true[]> {
3333
const verifications = metadata.users.map(async ({ userId, certificate }) => {
34-
const result = await validateCertificateSignature(certificate, serverPublicKey)
34+
const result = await validateCertificateSignature(certificate, serverPublicKeyPEM)
3535

3636
if (!result) {
3737
throw new Error(`Certificate verification failed for user ${userId}`)

src/services/state.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ test("Correctly fetch user's private key", async () => {
2929
})
3030

3131
test("Correctly fetch server's public key", async () => {
32-
const userPrivateKey = await state.getServerPublicKey()
33-
expect(userPrivateKey instanceof CryptoKey).toBeTruthy()
32+
const serverPublicKeyPEM = await state.getServerPublicKeyPEM()
33+
expect(serverPublicKeyPEM).toBeTypeOf('string')
3434
})
3535

3636
test('Correctly fetch root folder metadata', async () => {

src/services/state.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,15 @@ const davClient = getClient() as WebDAVClient
2222

2323
export const state = {
2424
_userPrivateKey: undefined as CryptoKey | undefined,
25-
_serverPublicKey: undefined as CryptoKey | undefined,
25+
_serverPublicKey: undefined as string | undefined,
2626
_metadataCache: {} as Record<string, Metadata>,
2727

2828
async getUserPrivateKey(): Promise<CryptoKey> {
2929
this._userPrivateKey ??= await decryptPrivateKey(await getPrivateKey(), await promptUserForMnemonic())
3030
return this._userPrivateKey
3131
},
3232

33-
async getServerPublicKey(): Promise<CryptoKey> {
33+
async getServerPublicKeyPEM(): Promise<string> {
3434
this._serverPublicKey ??= await getServerPublicKey()
3535
return this._serverPublicKey
3636
},
@@ -59,7 +59,7 @@ export const state = {
5959

6060
if (isRootMetadata(metadata)) {
6161
await validateMetadataSignature(metadata, metadataSignature, metadata)
62-
await validateUserCertificates(metadata, await this.getServerPublicKey())
62+
await validateUserCertificates(metadata, await this.getServerPublicKeyPEM())
6363
} else {
6464
await validateMetadataSignature(metadata, metadataSignature, await this.getRootMetadata(dirname(path)))
6565
}

src/services/webDavProxy.spec.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,13 @@ import { encryptedFileContent, rootFolderMetadataInfo, rootFolderPropfindRespons
1313
test('Correctly replace file info in PROPFIND', async () => {
1414
const xml = await parseXML(rootFolderPropfindResponse)
1515
replacePlaceholdersInPropfind(xml, '/remote.php/dav/files/admin/New%20folder/', rootFolderMetadataInfo)
16+
expect(xml.multistatus.response[0].propstat?.prop.permissions).toBe('G')
1617
expect(xml.multistatus.response[1].propstat?.prop.displayname).toBe('test.txt')
1718
expect(xml.multistatus.response[1].propstat?.prop.getcontenttype).toBe('text/plain')
19+
expect(xml.multistatus.response[1].propstat?.prop.permissions).toBe('G')
1820
expect(xml.multistatus.response[2].propstat?.prop.displayname).toBe('Test')
1921
expect(xml.multistatus.response[2].propstat?.prop.getcontenttype).toBe('httpd/unix-directory')
22+
expect(xml.multistatus.response[2].propstat?.prop.permissions).toBe('G')
2023
})
2124

2225
test('Correctly decrypt file on GET', async () => {

0 commit comments

Comments
 (0)