Skip to content
待添加

鸿蒙双向认证

开发环境 基于API12

切换到鸿蒙也要用上双向认证。使用的其中的 rcp 功能,详细文档https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/remote-communication-rcp-V5

双向认证包含两个方向,分为客户端验证服务端的证书和服务端验证客户端的证书。

客户端验证服务端的证书

这个就需要把服务端的证书内置在客户端里边,并且在请求的时候获取到服务端的证书,然后自己进行对比证书是否同内置的一样。

服务器证书的校验等级有以下几种:

  1. DefaultTrustEvaluator,使用默认的验证方式,验证证书的有效性,证书信任链那套
  2. RevocationTrustEvaluator,验证证书是否被吊销
  3. PinnedCertificatesTrustEvaluator,验证证书是否同本地的一致,可以是自签证书
  4. PublicKeysTrustEvaluator,验证证书的公钥,可以是自签证书,不过这个有个好处就是不用关心证书的有效期了
  5. CompositeTrustEvaluator,混合模式
  6. 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()
    })
  })
}

如此这般就能实现双向认证。感觉安全级别又上了一个等级。