Quick start

What You'll Build

This guide will show you how to start with a new Narval Managed Cloud account and progress to a fully operational Wallet-as-a-Service environment for your organization.

How the Armory Stack works

The Armory Stack plays the core role of an Auth Service: it grant access to protected resources by enforcing the organization's access policies. If the user is authorized, the server grants a client-bound access token. The user then uses this token to access secure resources, such as a crypto wallet keys managed by the Vault.

The Armory Vault is an optional companion service used either for wallet key generation & signing, or as an integration proxy to downstream resources not natively integrated.

In production, you may choose to use another key manager or self-host the Vault

Before Getting Started

Ensure you have the following prerequisites to follow this guide effectively:

  • An Armory Cloud account, including a client ID and secret.

  • A key pair that we'll use as credential for a new user.

  • A local Node.js application that uses TypeScript.

Setting Secrets

To interact with the Armory Stack, you need to provide your client ID and secret. Secure these credentials as managed secrets by storing them as environment variables or directly within your application's configuration. Use the following names for your secrets:

  • ARMORY_CLIENT_ID

  • ARMORY_CLIENT_SECRET

During your Armory Cloud account setup, you were asked to generate a key pair and submit the public key for your policy and entity store. Securely store the corresponding private keys:

  • ARMORY_POLICY_STORE_PRIVATE_KEY

  • ARMORY_ENTITY_STORE_PRIVATE_KEY

You also need a key pair for the user making authorized requests. Store this user’s private key as:

  • ARMORY_USER_PRIVATE_KEY

Your secrets configuration file should look like this:

ARMORY_CLIENT_ID="..."
ARMORY_CLIENT_SECRET="..."
ARMORY_USER_PRIVATE_KEY="..."
ARMORY_ENTITY_STORE_PRIVATE_KEY="..."
ARMORY_POLICY_STORE_PRIVATE_KEY="..."

1. Install the Armory SDK

Begin by installing the Armory SDK into your local Node.js project using the following command:

npm install @narval-xyz/armory-sdk --save

2. Create a new account in the Vault

First, we'll generate a new wallet in the Vault to derive accounts from.

import {
  Action,
  AuthClient,
  Permission,
  VaultClient,
  buildSignerForAlg,
  privateKeyToJwk,
  toHex
} from '@narval/armory-sdk'
import assert from 'assert'
import { v4 as uuid } from 'uuid'

const getEnv = (key: string): string => {
  const value = process.env[key]

  assert(value !== undefined, `Missing env variable ${key}`)

  return value
}

const getPrivateKey = (key: string) => privateKeyToJwk(toHex(getEnv(key)))

const clientId = getEnv('ARMORY_CLIENT_ID')
const clientSecret = getEnv('ARMORY_CLIENT_SECRET')
const userPrivateKey = getPrivateKey('ARMORY_USER_PRIVATE_KEY')
const entityStorePrivateKey = getPrivateKey('ARMORY_ENTITY_STORE_PRIVATE_KEY')
const policyStorePrivateKey = getPrivateKey('ARMORY_POLICY_STORE_PRIVATE_KEY')

const run = async () => {
  const authClient = new AuthClient({
    host: 'https://auth.armory.narval.xyz/',
    clientId,
    signer: {
      jwk: userPrivateKey,
      alg: userPrivateKey.alg,
      sign: await buildSignerForAlg(userPrivateKey)
    }
  })

  const vaultClient = new VaultClient({
    host: 'https://vault.armory.narval.xyz/',
    clientId,
    signer: {
      jwk: userPrivateKey,
      alg: userPrivateKey.alg,
      sign: await buildSignerForAlg(userPrivateKey)
    }
  })

  // Request access to create wallet and account.
  const walletCreateAccessToken = await authClient.requestAccessToken({
    action: Action.GRANT_PERMISSION,
    resourceId: 'vault',
    nonce: uuid(),
    permissions: [Permission.WALLET_CREATE]
  })

  const wallet = await vaultClient.generateWallet({ accessToken: walletCreateAccessToken })

  const { accounts } = await vaultClient.deriveAccounts({
    data: { keyId: wallet.keyId }, // Wallet to derive the account from
    accessToken: walletCreateAccessToken // Token authorizing the request
  })
}

run()

3. Set up the Entity Store

Initialize a new entity store with a user, associated credentials, and the created account. Entities, such as individuals, accounts, credentials, and groups, form the foundation of your organization. In the subsequent section, we will explore how these entities are regulated by policies.

The code snippet below demonstrates how to create a new account and user entity with a linked credential to it. This setup allows your SDK to make authenticated requests, confirming the identity of your application to the Auth Server through digital signatures.

import {
  AccountType,
  Action,
  AuthClient,
  Entities,
  EntityStoreClient,
  Permission,
  UserEntity,
  UserRole,
  VaultClient,
  buildSignerForAlg,
  credential,
  getAddress,
  getPublicKey,
  privateKeyToJwk,
  toHex
} from '@narval/armory-sdk'
import assert from 'assert'
import { v4 as uuid } from 'uuid'

// ...

const run = async () => {
  // ...

  const entityStoreClient = new EntityStoreClient({
    host: 'https://auth.armory.narval.xyz/',
    clientId,
    clientSecret,
    signer: {
      jwk: entityStorePrivateKey,
      alg: entityStorePrivateKey.alg,
      sign: await buildSignerForAlg(entityStorePrivateKey)
    }
  })

  const user: UserEntity = {
    id: uuid(),
    role: UserRole.ADMIN
  }

  const entities: Partial<Entities> = {
    users: [user],
    credentials: [credential(user, getPublicKey(userPrivateKey))],
    accounts: [
      {
        id: uuid(),
        address: getAddress(accounts[0].address),
        accountType: AccountType.EOA
      }
    ]
  }

  await entityStoreClient.signAndPush(entities)
}

run()

4. Set up the Policy Store

Next, configure the policy store with a rule that allows admin users to perform any action. Policies consist of criteria and actions to control the operations of entities within your organization.

The key used to sign the Policy store can be the same key used to sign the Entity store.

import { Criterion, Policy, PolicyStoreClient, Then, UserRole, buildSignerForAlg } from '@narval/armory-sdk'
import { v4 as uuid } from 'uuid'

// ...

const run = async () => {
  // ...
  
  const policyStoreClient = new PolicyStoreClient({
    host: 'https://auth.armory.narval.xyz/',
    clientId,
    clientSecret,
    signer: {
      jwk: policyStorePrivateKey,
      alg: policyStorePrivateKey.alg,
      sign: await buildSignerForAlg(policyStorePrivateKey)
    }
  })

  const policies: Policy[] = [
    {
      id: uuid(),
      description: 'Grant admins full access',
      when: [
        {
          criterion: Criterion.CHECK_PRINCIPAL_ROLE,
          args: [UserRole.ADMIN]
        }
      ],
      then: Then.PERMIT
    }
  ]

  await policyStoreClient.signAndPush(policies)
}

run()

5. Sign a Message

The Armory SDK simplifies this process by handling request signatures and token generation required for accessing the Armory Vault and Auth services.

import { Action } from '@narval/armory-sdk'
import { v4 as uuid } from 'uuid'

// ...

const run = async () => {
  // ...

  const signMessage = {
    action: Action.SIGN_MESSAGE,
    nonce: uuid(),
    resourceId: accounts[0].id,
    message: 'Hello world'
  }

  const signMessageAccessToken = await authClient.requestAccessToken(signMessage)

  const signature = await vaultClient.sign({
    data: signMessage,
    accessToken: signMessageAccessToken
  })

  console.log('Signature:', signature)
}

run()

Last updated