鸿蒙双向认证
开发环境 基于API12
切换到鸿蒙也要用上双向认证。使用的其中的 rcp 功能,详细文档https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/remote-communication-rcp-V5
双向认证包含两个方向,分为客户端验证服务端的证书和服务端验证客户端的证书。
客户端验证服务端的证书
这个就需要把服务端的证书内置在客户端里边,并且在请求的时候获取到服务端的证书,然后自己进行对比证书是否同内置的一样。
服务器证书的校验等级有以下几种:
- DefaultTrustEvaluator,使用默认的验证方式,验证证书的有效性,证书信任链那套
- RevocationTrustEvaluator,验证证书是否被吊销
- PinnedCertificatesTrustEvaluator,验证证书是否同本地的一致,可以是自签证书
- PublicKeysTrustEvaluator,验证证书的公钥,可以是自签证书,不过这个有个好处就是不用关心证书的有效期了
- CompositeTrustEvaluator,混合模式
- DisabledTrustEvaluator,不验证证书
可以按需自己处理。本文以对比公钥为例。
首先获取本地证书及证书的公钥。
javascript
let context = getContext(this)
const getRawFileContent = (ctx: Context, file: string) : string => {
let buffer = ctx.resourceManager.getRawFileContentSync(file).buffer
return String.fromCharCode(...new Uint8Array(buffer))
}
function stringToUint8Array(str: string): Uint8Array {
let arr: Array<number> = [];
for (let i = 0, j = str.length; i < j; i++) {
arr.push(str.charCodeAt(i));
}
return new Uint8Array(arr);
}
const uInt8ToString = (buffer: Uint8Array): string => {
return String.fromCharCode(...new Uint8Array(buffer))
}
let serverCert: cert.X509Cert | null = null
let serverCertStr: string | null = null
cert.createX509Cert({
data: stringToUint8Array(getRawFileContent(context, 'server.cer')),
encodingFormat: cert.EncodingFormat.FORMAT_DER
}).then(x509Cert => {
serverCert = x509Cert
serverCertStr = uInt8ToString(serverCert?.getPublicKey().getEncoded().data)
}).catch((error: BusinessError) => {
})
增加自定义验证证书函数。对比公钥是否一致。
javascript
{
security: {
remoteValidation: (context: rcp.ValidationContext) => {
let tar = uInt8ToString(context.x509Certs[0].getPublicKey().getEncoded().data)
if (serverCertStr === tar) {
return true
}
return false
},
},
}
这样客户端就验证了服务端证书是否符合要求。
服务端验证客户端的证书
由于接口请求的问题,需要把客户端证书写入到沙盒里边,然后把沙盒地址传进去,就有点麻烦。
首先写入客户端证书到沙盒。
本文需要使用到crt及key两个文件。接口crt文件需要文本形式,但是key又需要沙盒地址。
javascript
const getRawFileContent = (ctx: Context, file: string) : string => {
let buffer = ctx.resourceManager.getRawFileContentSync(file).buffer
return String.fromCharCode(...new Uint8Array(buffer))
}
let context = getContext(this)
let filesDir = context.filesDir
function saveFile(fn: string) {
let file = fs.openSync(filesDir + '/' + fn, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE)
let clientContent = context.resourceManager.getRawFileContentSync(fn)
fs.writeSync(file.fd, clientContent.buffer)
fs.fsyncSync(file.fd)
fs.closeSync(file)
}
saveFile('client.key')
然后提供客户端证书给服务端进行校验。
javascript
{
security: {
certificate: {
content: getRawFileContent(context, 'client.crt'),
key: filesDir + '/client.key',
type: 'PEM',
keyPassword: 'xxx'
},
},
}
这样就能把证书提供给服务端进行校验了。
完整实例
javascript
import { BusinessError } from '@ohos.base';
import { rcp } from "@kit.RemoteCommunicationKit";
import fs from '@ohos.file.fs';
import { cert } from '@kit.DeviceCertificateKit';
import { cryptoFramework } from '@kit.CryptoArchitectureKit';
const getRawFileContent = (ctx: Context, file: string) : string => {
let buffer = ctx.resourceManager.getRawFileContentSync(file).buffer
return String.fromCharCode(...new Uint8Array(buffer))
}
let context = getContext(this)
let filesDir = context.filesDir
function saveFile(fn: string) {
let file = fs.openSync(filesDir + '/' + fn, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE)
let clientContent = context.resourceManager.getRawFileContentSync(fn)
fs.writeSync(file.fd, clientContent.buffer)
fs.fsyncSync(file.fd)
fs.closeSync(file)
}
saveFile('client.key')
function stringToUint8Array(str: string): Uint8Array {
let arr: Array<number> = [];
for (let i = 0, j = str.length; i < j; i++) {
arr.push(str.charCodeAt(i));
}
return new Uint8Array(arr);
}
const uInt8ToString = (buffer: Uint8Array): string => {
return String.fromCharCode(...new Uint8Array(buffer))
}
let serverCert: cert.X509Cert | null = null
let serverCertStr: string | null = null
cert.createX509Cert({
data: stringToUint8Array(getRawFileContent(context, 'server.cer')),
encodingFormat: cert.EncodingFormat.FORMAT_DER
}).then(x509Cert => {
serverCert = x509Cert
serverCertStr = uInt8ToString(serverCert?.getPublicKey().getEncoded().data)
}).catch((error: BusinessError) => {
console.error('createX509Cert failed, errCode: ' + error.code + ', errMsg: ' + error.message);
})
const defaultTimeout: number = 60*1000
const createRequestConfiguration = (timeout: number): rcp.Configuration => {
return {
security: {
remoteValidation: (context: rcp.ValidationContext) => {
let tar = uInt8ToString(context.x509Certs[0].getPublicKey().getEncoded().data)
if (serverCertStr === tar) {
return true
}
return false
},
certificate: {
content: getRawFileContent(context, 'client.crt'),
key: filesDir + '/client.key',
type: 'PEM',
keyPassword: 'xxx'
},
},
transfer: {
autoRedirect: true,
timeout: {
connectMs: 5000,
transferMs: timeout,
}
}
}
}
const sessionConfig: rcp.SessionConfiguration = {
requestConfiguration: createRequestConfiguration(defaultTimeout)
}
const session: rcp.Session = rcp.createSession(sessionConfig)
export const rcpget = (url:string, timeout: number = defaultTimeout):Promise<object> => {
return new Promise((resolve, reject) => {
let request = new rcp.Request(url, 'GET', getHeaders())
if (timeout != defaultTimeout) {
request.configuration = createRequestConfiguration(timeout)
}
session.fetch(request).then((response:rcp.Response) => {
if (response.statusCode == 200) {
resolve(data)
} else {
reject()
}
}).catch((err: BusinessError) => {
reject()
})
})
}
export const rcppost = (url: string, params: Record<string, string | number>, timeout: number = defaultTimeout):Promise<object> => {
let p: string[] = []
for(let kv of Object.entries(params)) {
p.push(`${kv[0]}=${kv[1]}`)
}
let pstr: string = p.join('&')
return new Promise((resolve, reject) => {
let request: rcp.Request
if (pstr == '') {
request = new rcp.Request(url, 'POST', getHeaders())
} else {
request = new rcp.Request(url, 'POST', getHeaders(), pstr)
}
if (timeout != defaultTimeout) {
request.configuration = createRequestConfiguration(timeout)
}
session.fetch(request).then((response:rcp.Response) => {
if (response.statusCode == 200) {
resolve(data)
} else {
reject()
}
}).catch((err: BusinessError) => {
reject()
})
})
}
如此这般就能实现双向认证。感觉安全级别又上了一个等级。