enclaves/nitro/index.js

/**
 * @file AWS Nitro Enclaves
 * @copyright Multifactor 2021 All Rights Reserved
 *
 * @description
 * Cryptographic operations for AWS Nitro Enclaves
 *
 * @author Vivek Nair (https://nair.me) <[email protected]>
 */
const cbor = require('cbor')
const cose = require('cose-js')
const caroot = require('./caroot')
const x509 = require('@peculiar/x509')
const { Crypto } = require('@peculiar/webcrypto')
const crypto = (typeof window === 'undefined') ? new Crypto() : window.crypto
x509.cryptoProvider.set(crypto)

/**
 * The result of calling a verifyAttestation function.
 * @typedef {Object} AttestationResult
 * @property {boolean} valid - Indicates whether the attestation document is valid.
 * @property {string} [reason] - If the attestation document is not valid, describes why validation failed.
 * @property {string} [error] - If the attestation document is not valid and debug is true, includes error causing validation to fail.
 * @property {Object} [attr] - If the attestation document is valid, contains the attestation document attributes.
 */
const verify = (childCert, parentCert) => childCert.verify({ signatureOnly: true, publicKey: parentCert })

/**
 * Verify a AWS Nitro attestation document. Validates the attestation document signature and certificate.
 * NOTE: You still need to verify the PCRs yourself to check if the request is generated by the desired enclave image.
 *
 * @example
 * const trust = require('trust-center');
 * await trust.enclaves.nitro.verify(validDocument); // -> {valid: true, attr: {...}}
 * await trust.enclaves.nitro.verify(invalidDocument); // -> {valid: false, reason: '...'}
 *
 * @param {Buffer} document - AWS Nitro attestation document as a Buffer
 * @param {boolean} [debug=false] - Include advanced error details in response
 * @returns {AttestationResult} The validation result and attestation document attributes or rejection reason
 * @author Vivek Nair (https://nair.me) <[email protected]>
 * @since 0.1.7
 * @async
 * @memberOf nitro
 */
async function verifyAttestation (document, debug = false) {
  let AttestationDocument

  // Parse attestation document
  try {
    const COSESign1 = cbor.decodeAllSync(document)[0]
    AttestationDocument = cbor.decodeAllSync(COSESign1[2])[0]
    AttestationDocument.signature = COSESign1[3]
    AttestationDocument.certificate = new x509.X509Certificate(AttestationDocument.certificate)
  } catch (error) {
    if (debug) return { valid: false, reason: 'Failed to verify attestation document signature', error: error }
    return { valid: false, reason: 'Failed to validate attestation document' }
  }

  // Validate attestation document signature
  try {
    const publicKey = AttestationDocument.certificate.publicKey
    const cryptoKey = await crypto.subtle.importKey('spki', publicKey.rawData, { hash: 'SHA-256', ...publicKey.algorithm }, true, ['verify'])
    const key = await crypto.subtle.exportKey('jwk', cryptoKey)
    const verifier = {
      key: {
        x: Buffer.from(key.x, 'base64'),
        y: Buffer.from(key.y, 'base64')
      }
    }
    await cose.sign.verify(document, verifier, { defaultType: cose.sign.Sign1Tag })
  } catch (error) {
    if (debug) return { valid: false, reason: 'Failed to verify attestation document signature', error: error }
    return { valid: false, reason: 'Failed to verify attestation document signature' }
  }

  // Validate signing certificate
  AttestationDocument.cabundle = AttestationDocument.cabundle.map(certificate => new x509.X509Certificate(certificate))
  AttestationDocument.caroot = new x509.X509Certificate(caroot)
  if (!(await verify(AttestationDocument.certificate, AttestationDocument.cabundle[AttestationDocument.cabundle.length - 1]))) return { valid: false, reason: 'Failed to verify attestation document signing certificate' }
  for (let i = AttestationDocument.cabundle.length - 1; i > 0; i--) {
    if (!(await verify(AttestationDocument.cabundle[i], AttestationDocument.cabundle[i - 1]))) return { valid: false, reason: 'Failed to verify attestation document certificate chain' }
  }
  if (!(await verify(AttestationDocument.cabundle[0], AttestationDocument.caroot))) return { valid: false, reason: 'Failed to verify attestation document certificate chain' }

  // Return Results
  AttestationDocument.pcrs.forEach((value, key) => {
    AttestationDocument['pcr' + key] = value.toString('hex')
  })
  delete AttestationDocument.pcrs
  return { valid: true, attr: AttestationDocument }
}
module.exports.verifyAttestation = verifyAttestation