Add IdentityProof class

Helper class for ESR ID (v3) proof serialization and validation
This commit is contained in:
Johan Nordberg
2021-01-29 16:54:33 +09:00
parent cfe0d16c15
commit aefa15ed35
5 changed files with 169 additions and 5 deletions
+8 -5
View File
@@ -3,17 +3,20 @@
* Based on https://gist.github.com/jonleighton/958841
*/
const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'
const baseCharset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
const lookup = new Uint8Array(256)
for (let i = 0; i < 64; i++) {
lookup[charset.charCodeAt(i)] = i
for (let i = 0; i < 62; i++) {
lookup[baseCharset.charCodeAt(i)] = i
}
// support both urlsafe and standard base64
lookup[43] = lookup[45] = 62
lookup[47] = lookup[95] = 63
export function encode(data: Uint8Array): string {
export function encode(data: Uint8Array, urlSafe = true): string {
const byteLength = data.byteLength
const byteRemainder = byteLength % 3
const mainLength = byteLength - byteRemainder
const charset = baseCharset + (urlSafe ? '-_' : '+/')
const parts: string[] = []
let a: number
+123
View File
@@ -0,0 +1,123 @@
import {
Action,
Authority,
AuthorityType,
isInstanceOf,
Name,
NameType,
PermissionLevel,
PermissionLevelType,
PublicKey,
Serializer,
Signature,
SignatureType,
Struct,
TimePointSec,
TimePointType,
Transaction,
} from '@greymass/eosio'
import {IdentityV3} from './abi'
import {ChainId, ChainIdType} from './chain-id'
import {CallbackPayload, SigningRequest, SigningRequestEncodingOptions} from './signing-request'
import * as Base64u from './base64u'
export type IdentityProofType =
| IdentityProof
| string
| {
chainId: ChainIdType
scope: NameType
expiration: TimePointType
signer: PermissionLevelType
signature: SignatureType
}
@Struct.type('identity_proof')
export class IdentityProof extends Struct {
@Struct.field(ChainId) chainId!: ChainId
@Struct.field(Name) scope!: Name
@Struct.field(TimePointSec) expiration!: TimePointSec
@Struct.field(PermissionLevel) signer!: PermissionLevel
@Struct.field(Signature) signature!: Signature
static from(value: IdentityProofType): IdentityProof {
if (isInstanceOf(value, IdentityProof)) {
return value
} else if (typeof value === 'string') {
return IdentityProof.fromString(value)
} else {
return super.from(value) as IdentityProof
}
}
/**
* Create a new instance from an EOSIO authorization header string.
* "EOSIO <base64payload>"
*/
static fromString(string: string) {
const parts = string.split(' ')
if (parts.length !== 2 || parts[0] !== 'EOSIO') {
throw new Error('Invalid IdentityProof string')
}
const data = Base64u.decode(parts[1])
return Serializer.decode({data, type: IdentityProof})
}
/** Create a new instance from a callback payload. */
static fromPayload(payload: CallbackPayload, options: SigningRequestEncodingOptions = {}) {
const request = SigningRequest.from(payload.req, options)
if (!(request.version >= 3 && request.isIdentity())) {
throw new Error('Not an identity request')
}
return this.from({
chainId: payload.cid || request.getChainId(),
scope: request.getIdentityScope()!,
expiration: payload.ex,
signer: {actor: payload.sa, permission: payload.sp},
signature: payload.sig,
})
}
/**
* Recover the public key that signed this proof.
*/
recover(): PublicKey {
const action = Action.from({
account: '',
name: 'identity',
authorization: [this.signer],
data: IdentityV3.from({scope: this.scope, permission: this.signer}),
})
const transaction = Transaction.from({
ref_block_num: 0,
ref_block_prefix: 0,
expiration: this.expiration,
actions: [action],
})
return this.signature.recoverDigest(transaction.signingDigest(this.chainId))
}
/**
* Verify that given authority signed this proof.
* @param auth The accounts signing authority.
* @param currentTime Time to verify expiry against, if unset will use system time.
*/
verify(auth: AuthorityType, currentTime?: TimePointType) {
const now = TimePointSec.from(currentTime || new Date()).toMilliseconds()
return (
now < this.expiration.toMilliseconds() &&
Authority.from(auth).hasPermission(this.recover())
)
}
/**
* Encode the proof to an `EOSIO` auth header string.
*/
toString() {
const data = Serializer.encode({object: this})
return `EOSIO ${Base64u.encode(data.array, false)}`
}
}
+1
View File
@@ -1,3 +1,4 @@
export * from './signing-request'
export * from './abi'
export * from './chain-id'
export * from './identity-proof'
+16
View File
@@ -46,6 +46,7 @@ import {
RequestFlags,
RequestSignature,
} from './abi'
import {IdentityProof} from './identity-proof'
/** Current supported protocol version, backwards compatible with version 2. */
export const ProtocolVersion = 3
@@ -95,6 +96,8 @@ export interface CallbackPayload {
req: string
/** Expiration time used when resolving request. */
ex: string
/** The resolved chain id. */
cid?: string
/** All signatures 0-indexed as `sig0`, `sig1`, etc. */
[sig0: string]: string | undefined
}
@@ -1219,6 +1222,19 @@ export class ResolvedSigningRequest {
url,
}
}
public getIdentityProof(signature: SignatureType) {
if (!this.request.isIdentity()) {
throw new Error('Not a identity request')
}
return IdentityProof.from({
chainId: this.chainId,
scope: this.request.getIdentityScope()!,
expiration: this.transaction.expiration,
signer: this.signer,
signature,
})
}
}
function encodeAction(action: AnyAction, abis: Record<string, ABIDef>): Action {
+21
View File
@@ -13,6 +13,7 @@ import {
} from '../src'
import * as TSModule from '../src'
import {Name, PrivateKey, Serializer, Signature, UInt64} from '@greymass/eosio'
import {IdentityProof} from '../src/identity-proof'
let {SigningRequest, PlaceholderAuth, PlaceholderName} = TSModule
if (process.env['TEST_UMD']) {
@@ -638,6 +639,26 @@ describe('signing request', function () {
recreated.chainId.hexString,
'1064487b3cd1a897ce03ae5b6a865651747e2e152090f99c1d19d44e01aea5a4'
)
const proof = recreated.getIdentityProof(sig)
assert.equal(
String(proof),
'EOSIO EGRIezzRqJfOA65baoZWUXR+LhUgkPmcHRnUTgGupaQAAAAAAAAoXXQpCF8AAAAAAAAoXQAAAACo7TIyAB9I36p6NdMCKzksNwp4nFbiEhq8/sVAeji/4JMzk/CHAwc5ipaF8G/SNuXkJ9XWaDSu98DWzbXuvaVcimXUvGDQ'
)
const recreatedProof = IdentityProof.from(String(proof))
assert.ok(
recreatedProof.verify(
{threshold: 4, keys: [{weight: 4, key: key.toPublic()}]},
'2020-07-10T08:00:00'
),
'verifies valid proof'
)
assert.ok(
!recreatedProof.verify(
{threshold: 4, keys: [{weight: 4, key: key.toPublic()}]},
'2020-07-10T09:00:00'
),
'does not verify expired proof'
)
})
})