FFFF feat: RFC 9421 HTTP Message Signatures + shared crypto layer + JWT processor by kawasima · Pull Request #166 · enkan/enkan · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
1bb8af4
feat(crypto): add enkan.security.crypto — shared cryptographic signin…
kawasima Apr 5, 2026
ef153d2
feat(jwt): add enkan.web.jwt — stateless JWT signing and verification
kawasima Apr 5, 2026
e03e7ad
feat(rfc9421): add SignatureAlgorithm, SignatureComponent, and Signat…
kawasima Apr 5, 2026
b2b6389
feat(rfc9421): add HttpMessageSignatures sign/verify utility
kawasima Apr 5, 2026
ad35de7
feat(rfc9421): add SignatureVerificationMiddleware with Accept-Signat…
kawasima Apr 5, 2026
05fde4e
fix: address review findings — security, correctness, and test coverage
kawasima Apr 5, 2026
84bc8d8
fix(security): harden DER INTEGER length parsing and JWT number parsing
kawasima Apr 5, 2026
6c51822
fix: address code review findings from PR #166
kawasima Apr 5, 2026
d38fc6e
fix: replace IllegalArgumentException with MisconfigurationException …
kawasima Apr 5, 2026
143dad0
fix: pre-emptive hardening of design, performance, and security issues
kawasima Apr 5, 2026
25140e4
fix: second review pass — RFC 9421 multi-value query params, JWT esca…
kawasima Apr 5, 2026
2365d48
fix: RFC 9421 compliance — header name case-folding and expires/creat…
kawasima Apr 5, 2026
3ab2e39
fix: RFC 9421 §2.2.8 — query-param multi-value as separate lines, per…
kawasima Apr 5, 2026
035ca78
fix: ECDSA key type validation in JcaSigner, prefer Host header for @…
kawasima Apr 5, 2026
ba509cb
fix: address Copilot PR review comments
kawasima Apr 5, 2026
fecacc7
fix(signature): address second-round Copilot review findings
kawasima Apr 5, 2026
0baefd4
fix(crypto,signature): address remaining review findings
kawasima Apr 5, 2026
da2e507
test(signature,jwt): add final edge-case coverage
kawasima Apr 5, 2026
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package enkan.security.crypto;

import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.MGF1ParameterSpec;
import java.security.spec.PSSParameterSpec;

/**
* Cryptographic algorithms with JCA mappings.
*
* <p>Covers all algorithms needed for RFC 9421 HTTP Message Signatures and
* JWS (RFC 7515). Each constant maps to a JCA algorithm name and indicates
* whether it is symmetric (HMAC) or asymmetric (digital signature).
*
* @author kawasima
*/
public enum CryptoAlgorithm {

HMAC_SHA256("HmacSHA256", Type.SYMMETRIC, false),
HMAC_SHA384("HmacSHA384", Type.SYMMETRIC, false),
HMAC_SHA512("HmacSHA512", Type.SYMMETRIC, false),

ED25519("Ed25519", Type.ASYMMETRIC, false),

ECDSA_P256_SHA256("SHA256withECDSA", Type.ASYMMETRIC, true),
ECDSA_P384_SHA384("SHA384withECDSA", Type.ASYMMETRIC, true),
ECDSA_P521_SHA512("SHA512withECDSA", Type.ASYMMETRIC, true),

RSA_PSS_SHA256("RSASSA-PSS", Type.ASYMMETRIC, false),
RSA_PSS_SHA384("RSASSA-PSS", Type.ASYMMETRIC, false),
RSA_PSS_SHA512("RSASSA-PSS", Type.ASYMMETRIC, false),

RSA_V1_5_SHA256("SHA256withRSA", Type.ASYMMETRIC, false),
RSA_V1_5_SHA384("SHA384withRSA", Type.ASYMMETRIC, false),
RSA_V1_5_SHA512("SHA512withRSA", Type.ASYMMETRIC, false);

public enum Type { SYMMETRIC, ASYMMETRIC }

private final String jcaName;
private final Type type;
private final boolean ecdsa;

CryptoAlgorithm(String jcaName, Type type, boolean ecdsa) {
this.jcaName = jcaName;
this.type = type;
this.ecdsa = ecdsa;
}

/** Returns the JCA algorithm name for use with {@code Mac} or {@code Signature}. */
public String jcaName() {
return jcaName;
}

public Type type() {
return type;
}

/** Returns {@code true} if this is an ECDSA algorithm requiring DER↔P1363 conversion. */
public boolean isEcdsa() {
return ecdsa;
}

/**
* Returns the {@link AlgorithmParameterSpec} required by this algorithm,
* or {@code null} if the JCA name alone is sufficient.
*
* <p>RSA-PSS variants require a {@link PSSParameterSpec} to select the
* correct hash and MGF1 configuration.
*/
public AlgorithmParameterSpec parameterSpec() {
return switch (this) {
case RSA_PSS_SHA256 -> new PSSParameterSpec("SHA-256", "MGF1",
MGF1ParameterSpec.SHA256, 32, 1);
case RSA_PSS_SHA384 -> new PSSParameterSpec("SHA-384", "MGF1",
MGF1ParameterSpec.SHA384, 48, 1);
case RSA_PSS_SHA512 -> new PSSParameterSpec("SHA-512", "MGF1",
MGF1ParameterSpec.SHA512, 64, 1);
default -> null;
};
}
}
157 changes: 157 additions & 0 deletions enkan-core/src/main/java/enkan/security/crypto/EcdsaUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package enkan.security.crypto;

/**
* Converts ECDSA signatures between DER encoding (used by JCA) and
* IEEE P1363 / raw R||S format (used by JWS RFC 7518 §3.4 and other protocols).
*
* <p>All methods are pure byte-array operations with no external dependencies.
*
* <h2>DER structure reference</h2>
* <pre>
* SEQUENCE (tag=0x30, length)
* INTEGER (tag=0x02, length, [0x00 pad if high bit set], r bytes)
* INTEGER (tag=0x02, length, [0x00 pad if high bit set], s bytes)
* </pre>
* The 0x00 pad is required by DER when the first byte of r or s has its high bit set,
* to avoid being interpreted as a negative number. P1363 format simply concatenates
* r and s, each zero-padded on the left to {@code (keyBits+7)/8} bytes.
*
* @author kawasima
*/
public final class EcdsaUtils {

private EcdsaUtils() {}

/**
* Converts a DER-encoded ECDSA signature to raw R||S (IEEE P1363) format.
*
* <p>Each component (R, S) is zero-padded to {@code (keyBits+7)/8} bytes.
*
* @param der the DER-encoded signature (SEQUENCE { INTEGER r, INTEGER s })
* @param keyBits the EC key size in bits (e.g. 256, 384, 521)
* @return the raw R||S signature, or {@code null} if the input is malformed
*/
public static byte[] derToP1363(byte[] der, int keyBits) {
int componentLen = (keyBits + 7) / 8;
if (der == null || der.length < 8) return null;
if (der[0] != 0x30) return null; // must be SEQUENCE tag

int pos = 1;
int seqLenByte = der[pos++] & 0xff;
if ((seqLenByte & 0x80) != 0) {
int lenLen = seqLenByte & 0x7f;
if (pos + lenLen > der.length) return null;
pos += lenLen;
}
if (pos + 2 > der.length) return null;

// Parse r
if (pos >= der.length || der[pos] != 0x02) return null; // must be INTEGER tag
pos++;
if (pos >= der.length) return null;
int rLenByte = der[pos++] & 0xff;
int rLen;
if ((rLenByte & 0x80) != 0) {
int lenLen = rLenByte & 0x7f;
if (lenLen == 0 || pos + lenLen > der.length) return null;
rLen = 0;
for (int k = 0; k < lenLen; k++) rLen = (rLen << 8) | (der[pos++] & 0xff);
} else {
rLen = rLenByte;
}
if (pos + rLen + 2 > der.length) return null;
int rSrc = pos;
pos += rLen;

// Parse s
if (pos >= der.length || der[pos] != 0x02) return null; // must be INTEGER tag
pos++;
if (pos >= der.length) return null;
int sLenByte = der[pos++] & 0xff;
int sLen;
if ((sLenByte & 0x80) != 0) {
int lenLen = sLenByte & 0x7f;
if (lenLen == 0 || pos + lenLen > der.length) return null;
sLen = 0;
for (int k = 0; k < lenLen; k++) sLen = (sLen << 8) | (der[pos++] & 0xff);
} else {
sLen = sLenByte;
}
if (pos + sLen > der.length) return null;
int sSrc = pos;

byte[] result = new byte[componentLen * 2];

// Copy r right-aligned, stripping leading 0x00 padding
int rSkip = rLen > componentLen ? rLen - componentLen : 0;
for (int j = 0; j < rSkip; j++) {
if (der[rSrc + j] != 0x00) return null;
}
int rCopy = rLen - rSkip;
System.arraycopy(der, rSrc + rSkip, result, componentLen - rCopy, rCopy);

// Copy s right-aligned, stripping leading 0x00 padding
int sSkip = sLen > componentLen ? sLen - componentLen : 0;
for (int j = 0; j < sSkip; j++) {
if (der[sSrc + j] != 0x00) return null;
}
int sCopy = sLen - sSkip;
System.arraycopy(der, sSrc + sSkip, result, componentLen * 2 - sCopy, sCopy);
return result;
}

/**
* Converts a raw R||S (IEEE P1363) ECDSA signature to DER encoding.
*
* @param p1363 the raw R||S signature (must have even length)
* @return the DER-encoded signature, or {@code null} if the input is malformed
*/
public static byte[] p1363ToDer(byte[] p1363) {
if (p1363 == null || p1363.length == 0 || (p1363.length % 2) != 0) {
return null;
}
int half = p1363.length / 2;

// Strip leading zeros and compute DER integer lengths
int rOff = 0;
while (rOff < half - 1 && p1363[rOff] == 0) rOff++;
boolean rPad = (p1363[rOff] & 0x80) != 0;
int rDerLen = half - rOff + (rPad ? 1 : 0);

int sOff = half;
while (sOff < p1363.length - 1 && p1363[sOff] == 0) sOff++;
boolean sPad = (p1363[sOff] & 0x80) != 0;
int sDerLen = p1363.length - sOff + (sPad ? 1 : 0);

int contentLen = 2 + rDerLen + 2 + sDerLen;
// DER length encoding: short-form (0x00-0x7f), or long-form 0x81/<1 byte> or 0x82/<2 bytes>
int lenFieldSize = contentLen <= 0x7f ? 1 : contentLen <= 0xff ? 2 : 3;
byte[] der = new byte[1 + lenFieldSize + contentLen];

int i = 0;
der[i++] = 0x30; // SEQUENCE tag
if (contentLen <= 0x7f) {
der[i++] = (byte) contentLen;
} else if (contentLen <= 0xff) {
der[i++] = (byte) 0x81;
der[i++] = (byte) contentLen;
} else {
der[i++] = (byte) 0x82;
der[i++] = (byte) (contentLen >> 8);
der[i++] = (byte) contentLen;
}

der[i++] = 0x02; // INTEGER tag for r
der[i++] = (byte) rDerLen;
if (rPad) der[i++] = 0x00;
System.arraycopy(p1363, rOff, der, i, half - rOff);
i += half - rOff;

der[i++] = 0x02; // INTEGER tag for s
der[i++] = (byte) sDerLen;
if (sPad) der[i++] = 0x00;
System.arraycopy(p1363, sOff, der, i, p1363.length - sOff);

return der;
}
}
85 changes: 85 additions & 0 deletions enkan-core/src/main/java/enkan/security/crypto/JcaSigner.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package enkan.security.crypto;

import enkan.exception.MisconfigurationException;

import javax.crypto.Mac;
import javax.crypto.SecretKey;
import java.security.*;
import java.security.interfaces.ECKey;
import java.security.spec.AlgorithmParameterSpec;

/**
* JCA-based {@link Signer} implementation.
*
* <p>Supports HMAC (symmetric) and digital signature (asymmetric) algorithms.
* For ECDSA, the JCA-produced DER-encoded signature is converted to the raw
* R||S (IEEE P1363) format commonly used by protocols such as JWS and HTTP
* Message Signatures.
*
* @author kawasima
*/
public final class JcaSigner implements Signer {

private final CryptoAlgorithm algorithm;
private final Key key;

/**
* Creates a signer.
*
* @param algorithm the cryptographic algorithm
* @param key a {@link javax.crypto.SecretKey} for HMAC or a {@link PrivateKey} for asymmetric
* @throws MisconfigurationException if the key type is incompatible with the algorithm
*/
public JcaSigner(CryptoAlgorithm algorithm, Key key) {
if (algorithm.type() == CryptoAlgorithm.Type.SYMMETRIC && !(key instanceof SecretKey)) {
throw new MisconfigurationException("crypto.INCOMPATIBLE_KEY_TYPE", algorithm, key.getClass().getSimpleName());
}
if (algorithm.type() == CryptoAlgorithm.Type.ASYMMETRIC && !(key instanceof PrivateKey)) {
throw new MisconfigurationException("crypto.INCOMPATIBLE_KEY_TYPE", algorithm, key.getClass().getSimpleName());
}
if (algorithm.isEcdsa() && !(key instanceof ECKey)) {
throw new MisconfigurationException("crypto.INCOMPATIBLE_KEY_TYPE", algorithm, key.getClass().getSimpleName());
}
this.algorithm = algorithm;
this.key = key;
}

@Override
public byte[] sign(byte[] input) {
try {
if (algorithm.type() == CryptoAlgorithm.Type.SYMMETRIC) {
return signHmac(input);
} else {
return signAsymmetric(input);
}
} catch (GeneralSecurityException e) {
throw new IllegalStateException("Signing failed for " + algorithm, e);
}
}

private byte[] signHmac(byte[] input) throws GeneralSecurityException {
Mac mac = Mac.getInstance(algorithm.jcaName());
mac.init((SecretKey) key);
return mac.doFinal(input);
}

private byte[] signAsymmetric(byte[] input) throws GeneralSecurityException {
Signature sig = Signature.getInstance(algorithm.jcaName());
AlgorithmParameterSpec paramSpec = algorithm.parameterSpec();
if (paramSpec != null) {
sig.setParameter(paramSpec);
}
sig.initSign((PrivateKey) key);
sig.update(input);
byte[] raw = sig.sign();

if (algorithm.isEcdsa()) {
int keyBits = ((ECKey) key).getParams().getOrder().bitLength();
raw = EcdsaUtils.derToP1363(raw, keyBits);
if (raw == null) {
throw new SignatureException("Failed to convert ECDSA DER to P1363");
}
}
return raw;
}
}
Loading
Loading
0