ox
Version:
Ethereum Standard Library
171 lines • 5.57 kB
JavaScript
import * as Bytes from './Bytes.js';
import * as Cbor from './Cbor.js';
import * as Errors from './Errors.js';
import * as Cursor from './internal/cursor.js';
import * as PublicKey from './PublicKey.js';
/**
* Converts a P256 {@link ox#PublicKey.PublicKey} to a CBOR-encoded COSE_Key.
*
* The COSE_Key uses integer map keys per [RFC 9053](https://datatracker.ietf.org/doc/html/rfc9053):
* - `1` (kty): `2` (EC2)
* - `3` (alg): `-7` (ES256)
* - `-1` (crv): `1` (P-256)
* - `-2` (x): x coordinate bytes
* - `-3` (y): y coordinate bytes
*
* @example
* ```ts twoslash
* import { CoseKey, P256 } from 'ox'
*
* const { publicKey } = P256.createKeyPair()
*
* const coseKey = CoseKey.fromPublicKey(publicKey)
* ```
*
* @param publicKey - The P256 public key to convert.
* @returns The CBOR-encoded COSE_Key as a Hex string.
*/
export function fromPublicKey(publicKey) {
const pkBytes = PublicKey.toBytes(publicKey);
const x = pkBytes.slice(1, 33);
const y = pkBytes.slice(33, 65);
return Cbor.encode(new Map([
[1, 2], // kty: EC2
[3, -7], // alg: ES256
[-1, 1], // crv: P-256
[-2, x], // x coordinate
[-3, y], // y coordinate
]));
}
/**
* Converts a CBOR-encoded COSE_Key to a P256 {@link ox#PublicKey.PublicKey}.
*
* Accepts the COSE key as either a hex string or raw bytes. When the
* `returnByteLength` or `returnDecoded` option is set, the function returns an
* object containing the public key plus the consumed byte length and/or the
* decoded CBOR map. This is useful for parsing CBOR streams (such as WebAuthn
* `authenticatorData`) where the COSE key is followed by trailing data.
*
* @example
* ```ts twoslash
* import { CoseKey, P256 } from 'ox'
*
* const { publicKey } = P256.createKeyPair()
* const coseKey = CoseKey.fromPublicKey(publicKey)
*
* const publicKey2 = CoseKey.toPublicKey(coseKey)
* ```
*
* @example
* ### With Byte Length
*
* ```ts twoslash
* import { CoseKey, P256 } from 'ox'
*
* const { publicKey } = P256.createKeyPair()
* const coseKey = CoseKey.fromPublicKey(publicKey)
*
* const { publicKey: pk, byteLength } = CoseKey.toPublicKey(
* coseKey,
* {
* returnByteLength: true
* }
* )
* ```
*
* @param coseKey - The CBOR-encoded COSE_Key as hex or bytes.
* @param options - Decoding options.
* @returns The P256 public key, optionally with byte length and decoded CBOR.
*/
export function toPublicKey(coseKey, options = {}) {
const bytes = typeof coseKey === 'string' ? Bytes.fromHex(coseKey) : coseKey;
const cursor = Cursor.create(bytes);
const decoded = Cbor.decode(bytes);
// After Cbor.decode, the cursor's position would advance, but Cbor.decode
// creates its own cursor internally. Walk the bytes here to compute consumed
// byte length without re-encoding (which loses optional COSE members).
cursor.position = 0;
cborSkip(cursor);
const byteLength = cursor.position;
// Validate COSE_Key header per RFC 9053 (kty=2 EC2, alg=-7 ES256, crv=1 P-256).
if (decoded['1'] !== 2 || decoded['3'] !== -7 || decoded['-1'] !== 1)
throw new InvalidCoseKeyError();
const x = decoded['-2'];
const y = decoded['-3'];
if (!(x instanceof Uint8Array) ||
x.length !== 32 ||
!(y instanceof Uint8Array) ||
y.length !== 32)
throw new InvalidCoseKeyError();
const raw = new Uint8Array(65);
raw[0] = 0x04;
raw.set(x, 1);
raw.set(y, 33);
const publicKey = PublicKey.from(raw);
if (!options.returnByteLength && !options.returnDecoded)
return publicKey;
const result = { publicKey };
if (options.returnByteLength)
result.byteLength = byteLength;
if (options.returnDecoded)
result.decoded = decoded;
return result;
}
/**
* Walks one CBOR element on the cursor, advancing `cursor.position` past it.
* Supports the major types used by COSE keys: unsigned int (0), negative int
* (1), byte string (2), text string (3), array (4), map (5).
*
* @internal
*/
function cborSkip(cursor) {
const initialByte = cursor.readUint8();
const majorType = initialByte >> 5;
const additionalInfo = initialByte & 0b00011111;
const length = cborReadLength(cursor, additionalInfo);
switch (majorType) {
case 0:
case 1:
// value already consumed by `cborReadLength`.
return;
case 2:
case 3:
// skip `length` bytes of byte/text string content
cursor.readBytes(length);
return;
case 4:
for (let i = 0; i < length; i++)
cborSkip(cursor);
return;
case 5:
for (let i = 0; i < length; i++) {
cborSkip(cursor);
cborSkip(cursor);
}
return;
case 7:
return;
default:
throw new InvalidCoseKeyError();
}
}
/** @internal */
function cborReadLength(cursor, ai) {
if (ai < 24)
return ai;
if (ai === 24)
return cursor.readUint8();
if (ai === 25)
return cursor.readUint16();
if (ai === 26)
return cursor.readUint32();
throw new InvalidCoseKeyError();
}
/** Thrown when a COSE_Key does not contain valid P256 public key coordinates. */
export class InvalidCoseKeyError extends Errors.BaseError {
name = 'CoseKey.InvalidCoseKeyError';
constructor() {
super('COSE_Key does not contain valid P256 public key coordinates.');
}
}
//# sourceMappingURL=CoseKey.js.map