types/rsa-sha256.js

'use strict'

/**
 * @module types
 */

const crypto = require('crypto')
const constants = require('constants')
const Pss = require('../crypto/pss')
const pem = require('../util/pem')
const BaseSha256 = require('./base-sha256')
const Predictor = require('../lib/predictor')
const MissingDataError = require('../errors/missing-data-error')
const ValidationError = require('../errors/validation-error')

/**
 * RSA-SHA-256: RSA signature condition using SHA-256.
 *
 * This RSA condition uses RSA-PSS padding with SHA-256. The salt length is set
 * equal the digest length of 32 bytes.
 *
 * The public exponent is fixed at 65537 and the public modulus must be between
 * 128 (1017 bits) and 512 bytes (4096 bits) long.
 *
 * RSA-SHA-256 is assigned the type ID 3. It relies on the SHA-256 and RSA-PSS
 * feature suites which corresponds to a feature bitmask of 0x11.
 */
class RsaSha256 extends BaseSha256 {
  constructor () {
    super()
    this.modulus = null
    this.signature = null
  }

  /**
   * Write static header fields.
   *
   * Some fields are common between the hash and the fulfillment payload. This
   * method writes those field to anything implementing the Writer interface.
   * It is used internally when generating the hash of the condition, when
   * generating the fulfillment payload and when calculating the maximum
   * fulfillment size.
   *
   * @param {Writer|Hasher|Predictor} Target for outputting the header.
   *
   * @private
   */
  writeCommonHeader (writer) {
    if (!this.modulus) {
      throw new MissingDataError('Requires a public modulus')
    }

    writer.writeVarOctetString(this.modulus)
  }

  /**
   * Set the public modulus.
   *
   * This is the modulus of the RSA public key. It has to be provided as a raw
   * buffer with no leading zeros.
   *
   * @param {Buffer} modulus Public RSA modulus
   */
  setPublicModulus (modulus) {
    if (!Buffer.isBuffer(modulus)) {
      throw new TypeError('Modulus must be a buffer, was: ' + modulus)
    }

    if (modulus[0] === 0) {
      throw new Error('Modulus may not contain leading zeros')
    }

    if (modulus.length > 512 || modulus.length < 128) {
      throw new Error('Modulus must be between 128 bytes (1017 bits) and ' +
        '512 bytes (4096 bits), was: ' + modulus.length + ' bytes')
    }

    this.modulus = modulus
  }

  /**
   * Set the signature manually.
   *
   * The signature must be a valid RSA-PSS siganture.
   *
   * @param {Buffer} signature RSA signature.
   */
  setSignature (signature) {
    if (!Buffer.isBuffer(signature)) {
      throw new TypeError('Signature must be a buffer, was: ' + signature)
    }

    this.signature = signature
  }

  /**
   * Sign the message.
   *
   * This method will take the provided message and create a signature using the
   * provided RSA private key. The resulting signature is stored in the
   * fulfillment.
   *
   * The key should be provided as a PEM encoded private key string.
   *
   * The message is padded using RSA-PSS with SHA256.
   *
   * @param {Buffer} message Message to sign.
   * @param {String} privateKey RSA private key
   */
  sign (message, privateKey) {
    if (!this.modulus) {
      this.setPublicModulus(pem.modulusFromPrivateKey(privateKey))
    }
    const pss = new Pss()
    const modulusHighByteBitLength = this.modulus[0].toString(2).length
    const modulusBitLength = (this.modulus.length - 1) * 8 + modulusHighByteBitLength
    const paddedMessage = pss.encode(message, modulusBitLength - 1)
    this.signature = crypto.privateEncrypt({
      key: privateKey,
      padding: constants.RSA_NO_PADDING
    }, paddedMessage)
  }

  /**
   * Generate the contents of the condition hash.
   *
   * Writes the contents of the condition hash to a Hasher. Used internally by
   * `getCondition`.
   *
   * @param {Hasher} hasher Destination where the hash payload will be written.
   *
   * @private
   */
  writeHashPayload (hasher) {
    this.writeCommonHeader(hasher)
  }

  /**
   * Parse the payload of an RSA fulfillment.
   *
   * Read a fulfillment payload from a Reader and populate this object with that
   * fulfillment.
   *
   * @param {Reader} reader Source to read the fulfillment payload from.
   *
   * @private
   */
  parsePayload (reader) {
    this.setPublicModulus(reader.readVarOctetString())
    this.setSignature(reader.readVarOctetString())
  }

  /**
   * Generate the fulfillment payload.
   *
   * This writes the fulfillment payload to a Writer.
   *
   * @param {Writer} writer Subject for writing the fulfillment payload.
   *
   * @private
   */
  writePayload (writer) {
    if (!this.signature) {
      throw new MissingDataError('Requires a signature')
    }

    this.writeCommonHeader(writer)
    writer.writeVarOctetString(this.signature)
  }

  /**
   * Calculates the longest possible fulfillment length.
   *
   * The longest fulfillment for an RSA condition is the length of a fulfillment
   * where the dynamic message length equals its maximum length.
   *
   * @return {Number} Maximum length of the fulfillment payload
   *
   * @private
   */
  calculateMaxFulfillmentLength () {
    const predictor = new Predictor()

    if (!this.modulus) {
      throw new MissingDataError('Requires a public modulus')
    }

    // Calculate the length that the common header would have
    this.writeCommonHeader(predictor)

    // Signature
    predictor.writeVarOctetString(this.modulus)

    return predictor.getSize()
  }

  /**
   * Verify the signature of this RSA fulfillment.
   *
   * The signature of this RSA fulfillment is verified against the provided
   * message and the condition's public modulus.
   *
   * @param {Buffer} message Message to verify.
   * @return {Boolean} Whether this fulfillment is valid.
   */
  validate (message) {
    if (!Buffer.isBuffer(message)) {
      throw new Error('Message must be provided as a Buffer, was: ' + message)
    }

    // Verify signature
    const publicKey = pem.modulusToPem(this.modulus)
    const encodedMessage = crypto.publicDecrypt({
      key: publicKey,
      padding: constants.RSA_NO_PADDING
    }, this.signature)
    const pss = new Pss()
    const modulusHighByteBitLength = this.modulus[0].toString(2).length
    const modulusBitLength = (this.modulus.length - 1) * 8 + modulusHighByteBitLength
    const pssResult = pss.verify(message, encodedMessage, modulusBitLength - 1)

    if (!pssResult) {
      throw new ValidationError('Invalid RSA signature')
    }

    return true
  }
}

RsaSha256.TYPE_ID = 3
RsaSha256.FEATURE_BITMASK = 0x11

module.exports = RsaSha256