Skip to content

feat(pq-key-encoder): add post-quantum key encoding utilities#3

Merged
eacet merged 4 commits intomainfrom
feature/ENG-1208
Feb 10, 2026
Merged

feat(pq-key-encoder): add post-quantum key encoding utilities#3
eacet merged 4 commits intomainfrom
feature/ENG-1208

Conversation

@eacet
Copy link
Copy Markdown
Member

@eacet eacet commented Feb 2, 2026

Implement key encoding/decoding for NIST PQC algorithms (ML-KEM, ML-DSA, SLH-DSA).

Supported formats:

  • DER (SPKI for public keys, PKCS8 for private keys)
  • PEM (standard PUBLIC KEY / PRIVATE KEY labels)
  • JWK (custom kty: 'PQC' with base64url-encoded key material)

Features:

  • ASN.1 DER encoding/decoding with proper TLV parsing
  • AlgorithmIdentifier with OID and NULL parameters
  • Key size validation against pq-oid metadata
  • Nested private key extraction for OpenSSL compatibility
  • Custom error types (KeySizeMismatchError, InvalidEncodingError, etc.)

Tested with real OpenSSL-generated keys (OQS provider):

  • ML-KEM-512, ML-KEM-768, ML-KEM-1024
  • ML-DSA-44, ML-DSA-65
  • SLH-DSA-SHA2-128s

@linear
Copy link
Copy Markdown

linear bot commented Feb 2, 2026

ENG-1208 `pq-key-encoder`

  • ENG-1208
  • Implement pq-key-encoder package

@eacet eacet requested a review from timurguvenkaya February 2, 2026 07:44
@eacet eacet added the merge-when-ready Adds to the graphite merge queue once PR is ready to merge label Feb 2, 2026
@eacet eacet requested review from rubenmarcus, snwfdhmp and timurguvenkaya and removed request for timurguvenkaya February 2, 2026 07:45
@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Feb 2, 2026

Greptile Overview

Greptile Summary

Adds comprehensive key encoding/decoding utilities for NIST post-quantum cryptography algorithms (ML-KEM, ML-DSA, SLH-DSA) with support for DER, PEM, and JWK formats.

Key Changes

  • pq-oid package: Implemented OID encoding/decoding, algorithm metadata registry with key sizes and security levels for all 18 NIST PQC algorithms
  • pq-key-encoder package: Built complete ASN.1 DER encoder/decoder with TLV parsing, SPKI/PKCS8 support, PEM encoding, and custom PQC JWK format
  • OpenSSL compatibility: Added nested private key extraction to handle OpenSSL-generated keys with wrapped structures
  • Validation: Implemented strict key size validation using metadata from pq-oid
  • Test coverage: Added comprehensive unit tests and integration tests with real OpenSSL-generated keys

Notable Implementation Details

  • ASN.1 length encoding enforces DER minimal encoding rules
  • OID decoding correctly handles multi-byte base-128 encoding with continuation bits
  • JWK format uses custom kty: 'PQC' with base64url-encoded key material
  • Base64 encoding includes fallbacks for both browser (btoa/atob) and Node.js (Buffer) environments
  • Private key normalization searches for correct-sized OCTET STRING in nested structures

Issues Found

  • Unreachable error check in packages/pq-oid/ts/src/encoding.ts at lines 119-122 and 159-161 (code is functionally correct but has confusing logic)

Confidence Score: 4/5

  • safe to merge with one minor logic issue that doesn't affect functionality
  • well-structured implementation with comprehensive test coverage including real OpenSSL-generated keys, proper DER validation, and thorough error handling; the unreachable error check doesn't impact correctness
  • packages/pq-oid/ts/src/encoding.ts has unreachable error checks that could be clarified or removed

Important Files Changed

Filename Overview
packages/pq-oid/ts/src/encoding.ts implemented OID encoding/decoding with base-128 encoding for DER, has unreachable error check in decoding logic
packages/pq-key-encoder/ts/src/asn1/parse.ts implemented TLV parsing and algorithm identifier extraction with proper DER validation
packages/pq-key-encoder/ts/src/asn1/length.ts implemented DER length encoding/decoding with proper validation for minimal encoding
packages/pq-key-encoder/ts/src/pkcs8.ts implemented PKCS8 encoding/decoding with nested private key extraction for OpenSSL compatibility
packages/pq-key-encoder/ts/src/jwk.ts implemented custom PQC JWK format with base64url encoding and proper validation
packages/pq-oid/ts/src/algorithm.ts defined algorithm metadata for all 18 NIST PQC algorithms with key sizes and security levels

Sequence Diagram

sequenceDiagram
    participant User
    participant fromPEM
    participant fromDER
    participant parseAlgo
    participant fromPKCS8/SPKI
    participant validation
    participant pqOID

    User->>fromPEM: PEM string
    fromPEM->>fromPEM: Parse PEM headers
    fromPEM->>fromPEM: Base64 decode
    fromPEM->>fromDER: DER bytes
    
    fromDER->>parseAlgo: parseAlgorithmAndKeyWithType()
    parseAlgo->>parseAlgo: readTLV (outer SEQUENCE)
    parseAlgo->>parseAlgo: Check for version INTEGER
    parseAlgo->>parseAlgo: readTLV (AlgorithmIdentifier)
    parseAlgo->>parseAlgo: decodeObjectIdentifier()
    parseAlgo->>parseAlgo: readTLV (key data)
    parseAlgo-->>fromDER: {oid, keyBytes, keyType}
    
    fromDER->>pqOID: detectAlgorithmFromOid(oid)
    pqOID->>pqOID: OID.toName(oid)
    pqOID-->>fromDER: algorithm name
    
    fromDER->>fromPKCS8/SPKI: normalizePrivateKeyBytes()
    fromPKCS8/SPKI->>fromPKCS8/SPKI: Extract nested OCTET STRING
    fromPKCS8/SPKI-->>fromDER: normalized key bytes
    
    fromDER->>validation: assertKeyData()
    validation->>pqOID: getAlgorithmInfo(alg)
    pqOID-->>validation: {publicKeySize, privateKeySize}
    validation->>validation: validateKeySize()
    validation-->>fromDER: validation passed
    
    fromDER-->>User: {alg, type, bytes}
Loading

Copy link
Copy Markdown

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines +119 to +122

// Check if we ended in the middle of a multi-byte value
if (i > 0 && (bytes[i - 1] & 0x80) !== 0) {
throw new Error('Invalid OID bytes: incomplete multi-byte encoding');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unreachable error check - the loop above breaks when (byte & 0x80) === 0, so bytes[i - 1] will always have bit 7 clear at this point

the check on line 159 has the same issue

Suggested change
// Check if we ended in the middle of a multi-byte value
if (i > 0 && (bytes[i - 1] & 0x80) !== 0) {
throw new Error('Invalid OID bytes: incomplete multi-byte encoding');
// Loop breaks when high bit is clear, so this check is redundant
// The incomplete encoding case is already handled by the loop structure
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/pq-oid/ts/src/encoding.ts
Line: 119:122

Comment:
unreachable error check - the loop above breaks when `(byte & 0x80) === 0`, so `bytes[i - 1]` will always have bit 7 clear at this point

the check on line 159 has the same issue

```suggestion
  // Loop breaks when high bit is clear, so this check is redundant
  // The incomplete encoding case is already handled by the loop structure
```

How can I resolve this? If you propose a fix, please make it concise.

@eacet
Copy link
Copy Markdown
Member Author

eacet commented Feb 10, 2026

@codex review

@chatgpt-codex-connector
Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@eacet
Copy link
Copy Markdown
Member Author

eacet commented Feb 10, 2026

@claude review

@eacet
Copy link
Copy Markdown
Member Author

eacet commented Feb 10, 2026

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3fd1d93290

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +144 to +145
if (tlv.tag !== TAG_CONTEXT_0 && tlv.tag !== TAG_CONTEXT_1) {
throw new InvalidEncodingError('Unexpected trailing data in key sequence.');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Accept IMPLICIT [1] publicKey tag in OneAsymmetricKey

scanOptionalTrailingFields only recognizes context tag 0xa1 for the optional publicKey field, but RFC 5958 defines OneAsymmetricKey in an IMPLICIT TAGS module, so [1] PublicKey is encoded as an IMPLICIT BIT STRING tag (0x81) in DER. With the current check, standards-compliant v1 PKCS#8 keys that include publicKey are rejected with Unexpected trailing data in key sequence, which breaks interoperability for valid private key encodings.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The feedback is incorrect. Here's why:

The claim: RFC 5958 uses IMPLICIT TAGS, so [1] PublicKey should be encoded as 0x81 (implicit), making the current 0xa1 check wrong.

Why it's wrong:

  1. Extension additions override module tagging. The publicKey field in OneAsymmetricKey is defined inside double brackets [[2: publicKey [1] PublicKey OPTIONAL ]], which marks it as an extension addition (added
    in version 2 of the structure). Per ASN.1 rules (X.680), extension additions always use EXPLICIT tagging regardless of the module's default tagging environment. This ensures backward compatibility.
  2. The current code is correct. The tag 0xa1 (context-specific, constructed, tag 1) is the right encoding for an explicitly tagged [1] BIT STRING. An implicit encoding would use 0x81 (context-specific,
    primitive, tag 1) and would directly replace the BIT STRING tag — but that's not what happens here.
  3. Real-world implementations agree. OpenSSL and other major libraries encode the OneAsymmetricKey publicKey field using 0xa1 (explicit), not 0x81.

Bottom line: The scanOptionalTrailingFields function at lines 144-145 correctly checks for TAG_CONTEXT_0 (0xa0) and TAG_CONTEXT_1 (0xa1). No change is needed. Standards-compliant v1 PKCS#8 keys with a publicKey
field will use 0xa1 and will be accepted by this code.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3fd1d93290

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +142 to +145
while (currentOffset < sequence.length) {
const tlv = readTLV(sequence, currentOffset);
if (tlv.tag !== TAG_CONTEXT_0 && tlv.tag !== TAG_CONTEXT_1) {
throw new InvalidEncodingError('Unexpected trailing data in key sequence.');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Enforce ordered, single-occurrence trailing PKCS#8 fields

This loop accepts any number of 0xa0/0xa1 trailing elements in any order, so malformed OneAsymmetricKey encodings (for example [1] publicKey before [0] attributes, or duplicate [1] fields) are treated as valid when parsing untrusted DER. Because these are SEQUENCE members, they should appear at most once and in declaration order; otherwise corrupted/ambiguous private-key wrappers can bypass the parser’s DER validation.

Useful? React with 👍 / 👎.

@eacet eacet merged commit 296cb3e into main Feb 10, 2026
6 checks passed
@eacet eacet deleted the feature/ENG-1208 branch February 10, 2026 13:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

claude-code-assisted merge-when-ready Adds to the graphite merge queue once PR is ready to merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants