Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/roundtrip/init-temp-keys.sh
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ openssl req -x509 -nodes -newkey RSA:2048 -subj "/CN=kas" -keyout "$opt_output/k
openssl ecparam -name prime256v1 >ecparams.tmp
openssl req -x509 -nodes -newkey ec:ecparams.tmp -subj "/CN=kas" -keyout "$opt_output/kas-ec-private.pem" -out "$opt_output/kas-ec-cert.pem" -days 365

# ML-KEM KAS keys (768 & 1024)
script_dir="$(cd "$(dirname "$0")" >/dev/null && pwd)"
if [ -d "${script_dir}/platform/service" ]; then
GOWORK="${script_dir}/platform/go.work" \
go run "${script_dir}/platform/service/cmd/keygen" -output "$opt_output" || exit 1
else
go run github.com/opentdf/platform/service/cmd/keygen@latest -output "$opt_output" || exit 1
fi
Comment thread
dmihalcik-virtru marked this conversation as resolved.

if [ "$opt_hsm" = true ]; then
pkcs11-tool --module "${OPENTDF_SERVER_CRYPTOPROVIDER_HSM_MODULEPATH}" --login --pin "${OPENTDF_SERVER_CRYPTOPROVIDER_HSM_PIN}" --write-object kas-private.pem --type privkey --label "${OPENTDF_SERVER_CRYPTOPROVIDER_HSM_KEYS_RSA_LABEL}"
pkcs11-tool --module "${OPENTDF_SERVER_CRYPTOPROVIDER_HSM_MODULEPATH}" --login --pin "${OPENTDF_SERVER_CRYPTOPROVIDER_HSM_PIN}" --write-object kas-cert.pem --type cert --label "${OPENTDF_SERVER_CRYPTOPROVIDER_HSM_KEYS_RSA_LABEL}"
Expand Down
15 changes: 15 additions & 0 deletions .github/workflows/roundtrip/opentdf.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ logger:
# password: changeme
services:
kas:
preview:
ec_tdf_enabled: true
mlkem_tdf_enabled: true
keyring:
- kid: e1
alg: ec:secp256r1
Expand All @@ -21,6 +24,10 @@ services:
- kid: r1
alg: rsa:2048
legacy: true
- kid: mlkem768
alg: mlkem:768
- kid: mlkem1024
alg: mlkem:1024
entityresolution:
url: http://localhost:65432/auth
log_level: info
Expand Down Expand Up @@ -90,4 +97,12 @@ server:
alg: ec:secp256r1
private: kas-ec-private.pem
cert: kas-ec-cert.pem
- kid: mlkem768
alg: mlkem:768
private: kas-mlkem768-private.pem
cert: kas-mlkem768-public.pem
- kid: mlkem1024
alg: mlkem:1024
private: kas-mlkem1024-private.pem
cert: kas-mlkem1024-public.pem
port: 8080
1 change: 1 addition & 0 deletions CLAUDE.md
59 changes: 58 additions & 1 deletion cli/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

57 changes: 57 additions & 0 deletions lib/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions lib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
"dependencies": {
"@connectrpc/connect": "^2.0.2",
"@connectrpc/connect-web": "^2.0.2",
"@noble/post-quantum": "^0.6.1",
"buffer-crc32": "^1.0.0",
"jose": "6.0.8",
"json-canonicalize": "^1.0.6",
Expand Down
7 changes: 7 additions & 0 deletions lib/src/access.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ export const rewrapAdditionalContextHeader = (
return base64.encode(JSON.stringify(context));
};

// The supported key algorithms are defined in one place, `crypto/declarations.ts`.
// These public aliases preserve the historic `access.ts` API surface (name, tuple
// order, and guard behavior) while delegating to that single source of truth.
export const PUBLIC_KEY_ALGORITHMS = KEY_ALGORITHMS;

export type KasPublicKeyAlgorithm = KeyAlgorithm;
Expand Down Expand Up @@ -142,6 +145,10 @@ export const publicKeyAlgorithmToJwa = (a: KasPublicKeyAlgorithm): string => {
return 'ES384';
case 'ec:secp521r1':
return 'ES512';
case 'mlkem:768':
return 'ML-KEM-768+A192KW';
case 'mlkem:1024':
return 'ML-KEM-1024+A256KW';
default:
throw new Error(`unsupported public key algorithm: ${a}`);
}
Expand Down
3 changes: 3 additions & 0 deletions lib/src/crypto/pemPublicToCrypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ export const EC_OID = '06072a8648ce3d0201';
export const P256_OID = '06082a8648ce3d030107';
export const P384_OID = '06052b81040022';
export const P521_OID = '06052b81040023';
// NIST CSRC OID arc 2.16.840.1.101.3.4.4.{2,3} = id-alg-ml-kem-{768,1024}
export const ML_KEM_768_OID = '0609608648016503040402';
export const ML_KEM_1024_OID = '0609608648016503040403';
const SHA_512 = 'SHA-512';
const SPKI = 'spki';
const CERT_BEGIN = '-----BEGIN CERTIFICATE-----';
Expand Down
5 changes: 4 additions & 1 deletion lib/tdf3/src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { ConfigurationError } from '../../../src/errors.js';
import { AesGcmCipher } from '../ciphers/aes-gcm-cipher.js';
import {
isEcKeyAlgorithm,
isMlKemKeyAlgorithm,
isRsaKeyAlgorithm,
type KeyPair,
type SymmetricKey,
Expand Down Expand Up @@ -729,7 +730,7 @@ export class Client {
encryptionInformation.keyAccess = await Promise.all(
splitPlan.map(async ({ kas, kid, pem, sid }) => {
const algorithm = await algorithmFromPEM(pem, this.cryptoService);
if (algorithm !== wrappingKeyAlgorithm) {
if (wrappingKeyAlgorithm && algorithm !== wrappingKeyAlgorithm) {
console.warn(
`Mismatched wrapping key algorithm: [${algorithm}] is not requested type, [${wrappingKeyAlgorithm}]`
);
Expand All @@ -739,6 +740,8 @@ export class Client {
type = 'wrapped';
} else if (isEcKeyAlgorithm(algorithm)) {
type = 'ec-wrapped';
} else if (isMlKemKeyAlgorithm(algorithm)) {
type = 'mlkem-wrapped';
} else {
throw new ConfigurationError(`Unsupported algorithm ${algorithm}`);
}
Expand Down
74 changes: 74 additions & 0 deletions lib/tdf3/src/crypto/core/asn1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Generic ASN.1 / DER primitives shared by the SPKI and ML-KEM codecs.
// Definite-length BER/DER only; that is all X.509 SubjectPublicKeyInfo needs.

export function encodeLength(len: number): Uint8Array {
if (!Number.isSafeInteger(len) || len < 0) {
throw new Error(`Invalid ASN.1 length: ${len}`);
}
if (len < 0x80) return new Uint8Array([len]);
if (len < 0x100) return new Uint8Array([0x81, len]);
if (len < 0x10000) return new Uint8Array([0x82, (len >> 8) & 0xff, len & 0xff]);
throw new Error(`ASN.1 length too large: ${len}`);
}

export function decodeLength(
bytes: Uint8Array,
offset: number
): { length: number; bytesConsumed: number } {
const first = bytes[offset];
if (first < 0x80) return { length: first, bytesConsumed: 1 };
const numOctets = first & 0x7f;
if (numOctets === 0 || numOctets > 3) {
throw new Error(`Unsupported ASN.1 length octets: ${numOctets}`);
}
// Reject truncated encodings and non-minimal long form (leading zero octet).
if (offset + 1 + numOctets > bytes.length || bytes[offset + 1] === 0) {
throw new Error('Invalid ASN.1 length encoding');
}
let length = 0;
for (let i = 0; i < numOctets; i++) {
length = (length << 8) | bytes[offset + 1 + i];
}
// Long form must not encode a value that fits in short form.
if (length < 0x80) {
throw new Error('Non-canonical DER length encoding');
}
return { length, bytesConsumed: 1 + numOctets };
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

export type Tlv = {
/** Identifier octet (tag). */
tag: number;
/** Index of the first content byte. */
contentStart: number;
/** Index one past the last content byte. */
contentEnd: number;
/** Start of the following TLV; equals contentEnd for definite-length values. */
next: number;
};

/**
* Read a single definite-length TLV (tag-length-value) starting at `offset`.
* Throws if the declared length runs past the end of `bytes`.
*/
export function readTlv(bytes: Uint8Array, offset: number): Tlv {
if (offset >= bytes.length) {
throw new Error(`ASN.1 read past end of buffer at offset ${offset}`);
}
const tag = bytes[offset];
const { length, bytesConsumed } = decodeLength(bytes, offset + 1);
const contentStart = offset + 1 + bytesConsumed;
const contentEnd = contentStart + length;
if (contentEnd > bytes.length) {
throw new Error('ASN.1 TLV length exceeds buffer size');
}
return { tag, contentStart, contentEnd, next: contentEnd };
}

export function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return false;
}
return true;
}
Loading
Loading