mirror of
https://github.com/wharfkit/signing-request.git
synced 2026-07-21 16:03:29 +00:00
Add IdentityProof class
Helper class for ESR ID (v3) proof serialization and validation
This commit is contained in:
+8
-5
@@ -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
|
||||
|
||||
@@ -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,3 +1,4 @@
|
||||
export * from './signing-request'
|
||||
export * from './abi'
|
||||
export * from './chain-id'
|
||||
export * from './identity-proof'
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user