将AWS KMS ECDSA_SHA_256签名从DER编码的ANS.1格式转换为JWT base64 url编码的R||NodeJS/Javascript中的S格式

hvvq6cgz  于 2023-04-04  发布在  Java
关注(0)|答案(2)|浏览(127)

我正在尝试使用AWS KMS客户管理密钥在NodeJS中使用ES 256算法创建JWT签名。
使用AWS KMS和加密签名算法ECDSA_SHA_256创建的签名不是JWT接受的R||S格式。根据AWS文档,签名将采用DER编码的ANS X9.62-2005格式(https://docs.aws.amazon.com/kms/latest/APIReference/API_Sign.html#API_Sign_ResponseSyntax)。
我尝试将AWS KMS Sign转换为JWT R||S格式使用下面的代码在NodeJS与ans 1 js(https://www.npmjs.com/package/asn1js),但R和S的长度是不一致的32 + 32,而它的变化33的大部分时间。

function toArrayBuffer(buffer) {
    const ab = new ArrayBuffer(buffer.length);
    const view = new Uint8Array(ab);
    for (let i = 0; i < buffer.length; ++i) {
        view[i] = buffer[i];
    }
    return ab;
}

//call this with your signature buffer
function parseBERSignature(sig) {
    const { result } = asn1js.fromBER(toArrayBuffer(sig));

    const part1 = result.valueBlock.value[0];
    const part2 = result.valueBlock.value[1];

    let r = Buffer.from(part1.valueBlock.valueHex);
    let s = Buffer.from(part2.valueBlock.valueHex);

    console.log("R value", r);
    console.log("S value", s);

    console.log("R value", r.toString('base64'));
    console.log("S value", s.toString('base64'));

    console.log("R length", r.length);
    console.log("S length", s.length);

    return base64url.fromBase64(Buffer.concat([r, s]).toString('base64'));

}

签名创建的完整代码:

const base64url = require('base64url')
const AWS = require('aws-sdk');
const kms = new AWS.KMS();
const asn1js = require('asn1js')

const keyid = "9001e08c-b7bc-4f53-9eca-ec034904cdd5";

const header = {
    "typ": "JWT",
    "alg": "ES256",
    "kid": keyid
}

const payload = {
    "sub": "name",
    "status": "valid",
    "aud": "name"
}

exports.handler = async function (event, context, callback) {
    console.log("Hello, new World");

    payload.iat = Math.floor(Date.now() / 1000);

    console.log("header", header);
    console.log("payload", payload);

    const jwtHeader = base64url(JSON.stringify(header));
    const jwtPayload = base64url(JSON.stringify(payload));

    console.log("jwtHeader", jwtHeader);
    console.log("jwtPayload", jwtPayload);

    const message = Buffer.from(jwtHeader + "." + jwtPayload);

    const messageDigest = createHash(message);

    let kmsResponse = await kms.sign({
        Message: message,
        KeyId: keyid,
        SigningAlgorithm: 'ECDSA_SHA_256',
        MessageType: 'RAW'
    }).promise();

    console.log("Signature RAW", kmsResponse.Signature);
    console.log("Signature String", kmsResponse.Signature.toString());
    console.log("Signature base64", kmsResponse.Signature.toString('base64'));

    let response = parseBERSignature(kmsResponse.Signature);
    console.log("response", response);
    return jwtHeader + "." + jwtPayload + "." + response;

}

将DER编码的ANS格式转换为R的任何NodeJs Javascript实现||S格式base64 url编码?

nqwrtyyt

nqwrtyyt1#

将DER编码的ANS格式转换为R的任何NodeJs Javascript实现||S格式base64url编码?
是的,ecdsa-sig-formatter将帮助您在格式之间来回转换签名。

ql3eal8s

ql3eal8s2#

🥳最后,使用ES256的AWS KMS工作解决方案
在解决这个问题后,我找到了一个适用于NodeJs的好解决方案。
前面提到的包' ecdsa-sig-formatter '无法用于EllipticCurve算法签名格式设置。
我将代码从Typescript转换为一个可工作的Javascript文件。以下代码改编自node-ecdsa-sig-formatter,并由stacks-network/jsontokens-js修复

import { fromByteArray, toByteArray } from "base64-js";

//#region " Base64Url"
function pad(base64) {
  return `${base64}${"=".repeat(4 - (base64.length % 4 || 4))}`;
}
function escape(base64) {
  return base64.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
}
function unescape(base64Url) {
  return pad(base64Url).replace(/-/g, "+").replace(/_/g, "/");
}
function encode(base64) {
  return escape(fromByteArray(new TextEncoder().encode(base64)));
}
function decode(base64Url) {
  return new TextDecoder().decode(toByteArray(pad(unescape(base64Url))));
}
//#endregion

function getParamSize(keySize) {
  return ((keySize / 8) | 0) + (keySize % 8 === 0 ? 0 : 1);
}
const paramBytesForAlg = {
  ES256: getParamSize(256),
  ES384: getParamSize(384),
  ES512: getParamSize(521),
};
function getParamBytesForAlg(alg) {
  const paramBytes = paramBytesForAlg[alg];
  if (paramBytes) {
    return paramBytes;
  }
  throw new Error(`Unknown algorithm "${alg}"`);
}
const MAX_OCTET = 0x80;
const CLASS_UNIVERSAL = 0;
const PRIMITIVE_BIT = 0x20;
const TAG_SEQ = 0x10;
const TAG_INT = 0x02;
const ENCODED_TAG_SEQ = TAG_SEQ | PRIMITIVE_BIT | (CLASS_UNIVERSAL << 6);
const ENCODED_TAG_INT = TAG_INT | (CLASS_UNIVERSAL << 6);
function signatureAsBytes(signature) {
  if (signature instanceof Uint8Array) {
    return signature;
  } else if ("string" === typeof signature) {
    return toByteArray(pad(signature));
  }
  throw new TypeError("ECDSA signature must be a Base64 string or a Uint8Array");
}
export function derToJose(signature, alg) {
  const signatureBytes = signatureAsBytes(signature);
  const paramBytes = getParamBytesForAlg(alg);
  // the DER encoded param should at most be the param size, plus a padding
  // zero, since due to being a signed integer
  const maxEncodedParamLength = paramBytes + 1;
  const inputLength = signatureBytes.length;
  let offset = 0;
  if (signatureBytes[offset++] !== ENCODED_TAG_SEQ) {
    throw new Error('Could not find expected "seq"');
  }
  let seqLength = signatureBytes[offset++];
  if (seqLength === (MAX_OCTET | 1)) {
    seqLength = signatureBytes[offset++];
  }
  if (inputLength - offset < seqLength) {
    throw new Error(`"seq" specified length of "${seqLength}", only "${inputLength - offset}" remaining`);
  }
  if (signatureBytes[offset++] !== ENCODED_TAG_INT) {
    throw new Error('Could not find expected "int" for "r"');
  }
  const rLength = signatureBytes[offset++];
  if (inputLength - offset - 2 < rLength) {
    throw new Error(`"r" specified length of "${rLength}", only "${inputLength - offset - 2}" available`);
  }
  if (maxEncodedParamLength < rLength) {
    throw new Error(`"r" specified length of "${rLength}", max of "${maxEncodedParamLength}" is acceptable`);
  }
  const rOffset = offset;
  offset += rLength;
  if (signatureBytes[offset++] !== ENCODED_TAG_INT) {
    throw new Error('Could not find expected "int" for "s"');
  }
  const sLength = signatureBytes[offset++];
  if (inputLength - offset !== sLength) {
    throw new Error(`"s" specified length of "${sLength}", expected "${inputLength - offset}"`);
  }
  if (maxEncodedParamLength < sLength) {
    throw new Error(`"s" specified length of "${sLength}", max of "${maxEncodedParamLength}" is acceptable`);
  }
  const sOffset = offset;
  offset += sLength;
  if (offset !== inputLength) {
    throw new Error(`Expected to consume entire array, but "${inputLength - offset}" bytes remain`);
  }
  const rPadding = paramBytes - rLength;
  const sPadding = paramBytes - sLength;
  const dst = new Uint8Array(rPadding + rLength + sPadding + sLength);
  for (offset = 0; offset < rPadding; ++offset) {
    dst[offset] = 0;
  }
  dst.set(signatureBytes.subarray(rOffset + Math.max(-rPadding, 0), rOffset + rLength), offset);
  offset = paramBytes;
  for (const o = offset; offset < o + sPadding; ++offset) {
    dst[offset] = 0;
  }
  dst.set(signatureBytes.subarray(sOffset + Math.max(-sPadding, 0), sOffset + sLength), offset);
  return escape(fromByteArray(dst));
}
function countPadding(buf, start, stop) {
  let padding = 0;
  while (start + padding < stop && buf[start + padding] === 0) {
    ++padding;
  }
  const needsSign = buf[start + padding] >= MAX_OCTET;
  if (needsSign) {
    --padding;
  }
  return padding;
}
export function joseToDer(signature, alg) {
  signature = signatureAsBytes(signature);
  const paramBytes = getParamBytesForAlg(alg);
  const signatureBytes = signature.length;
  if (signatureBytes !== paramBytes * 2) {
    throw new TypeError(`"${alg}" signatures must be "${paramBytes * 2}" bytes, saw "${signatureBytes}"`);
  }
  const rPadding = countPadding(signature, 0, paramBytes);
  const sPadding = countPadding(signature, paramBytes, signature.length);
  const rLength = paramBytes - rPadding;
  const sLength = paramBytes - sPadding;
  const rsBytes = 1 + 1 + rLength + 1 + 1 + sLength;
  const shortLength = rsBytes < MAX_OCTET;
  const dst = new Uint8Array((shortLength ? 2 : 3) + rsBytes);
  let offset = 0;
  dst[offset++] = ENCODED_TAG_SEQ;
  if (shortLength) {
    // Bit 8 has value "0"
    // bits 7-1 give the length.
    dst[offset++] = rsBytes;
  } else {
    // Bit 8 of first octet has value "1"
    // bits 7-1 give the number of additional length octets.
    dst[offset++] = MAX_OCTET | 1;
    // length, base 256
    dst[offset++] = rsBytes & 0xff;
  }
  dst[offset++] = ENCODED_TAG_INT;
  dst[offset++] = rLength;
  if (rPadding < 0) {
    dst[offset++] = 0;
    dst.set(signature.subarray(0, paramBytes), offset);
    offset += paramBytes;
  } else {
    dst.set(signature.subarray(rPadding, paramBytes), offset);
    offset += paramBytes - rPadding;
  }
  dst[offset++] = ENCODED_TAG_INT;
  dst[offset++] = sLength;
  if (sPadding < 0) {
    dst[offset++] = 0;
    dst.set(signature.subarray(paramBytes), offset);
  } else {
    dst.set(signature.subarray(paramBytes + sPadding), offset);
  }
  return dst;
}

在使用AWS KMS(使用EC256)签名后,您需要做的是将签名从DERtoJOSE转换为:

const joseSignature = derToJose(awsSignature, 'ES256');

相关问题