All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.soulwing.jwt.api.jca.JcaJsonWebKey Maven / Gradle / Ivy

There is a newer version: 1.7.5
Show newest version
/*
 * File created on Apr 13, 2021
 *
 * Copyright (c) 2021 Carl Harris, Jr
 * and others as noted
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.soulwing.jwt.api.jca;

import java.math.BigInteger;
import java.security.Key;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.security.interfaces.ECPublicKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.ECParameterSpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.crypto.SecretKey;
import javax.json.Json;
import javax.json.JsonArrayBuilder;
import javax.json.JsonObject;
import javax.json.JsonObjectBuilder;
import javax.json.stream.JsonCollectors;

import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.soulwing.jwt.api.JWK;

/**
 *  A {@link JWK} implemented using the JCA API (with a little Bouncy Castle).
 *
 * @author Carl Harris
 */
public class JcaJsonWebKey implements JWK {

  private static final Map OID_TO_CURVE = new HashMap<>();

  /*
   * These are the curves that are identified in the spec, plus another that
   * is commonly used. More could be added as needed, by including the OID
   * used to identify the curve in the public key and specifying the name to
   * use in the `crv:` property of the JWK. When adding, be sure to update
   * the test in JcaJsonWebKeyTest to validate it.
   */
  static {
    OID_TO_CURVE.put("1.2.840.10045.3.1.7", "P-256");
    OID_TO_CURVE.put("1.3.132.0.34", "P-384");
    OID_TO_CURVE.put("1.3.132.0.35", "P-521");
    OID_TO_CURVE.put("1.3.132.0.10", "secp256k1");
  }

  private final Key key;
  private final JsonObject delegate;

  private JcaJsonWebKey(Key key, JsonObject delegate) {
    this.key = key;
    this.delegate = delegate;
  }

  static class Builder implements JWK.Builder {

    private String id;
    private String type;
    private String algorithm;
    private Use use;
    private Set ops = new HashSet<>();
    private Key key;
    private List certificates = new ArrayList<>();

    @Override
    public JWK.Builder id(String id) {
      this.id = id;
      return this;
    }

    @Override
    public JWK.Builder type(String type) {
      this.type = type;
      return this;
    }

    @Override
    public JWK.Builder algorithm(String algorithm) {
      this.algorithm = algorithm;
      return this;
    }

    @Override
    public JWK.Builder use(Use use) {
      this.use = use;
      return this;
    }

    @Override
    public JWK.Builder ops(KeyOp... ops) {
      return ops(Arrays.asList(ops));
    }

    @Override
    public JWK.Builder ops(Collection ops) {
      this.ops.addAll(ops);
      return this;
    }

    @Override
    public JWK.Builder key(Key key) {
      this.key = key;
      return this;
    }

    @Override
    public JWK.Builder certificates(X509Certificate... certificates) {
      return certificates(Arrays.asList(certificates));
    }

    @Override
    public JWK.Builder certificates(List certificates) {
      this.certificates.addAll(certificates);
      return this;
    }

    private void defaultType() {
      if (this.type == null) {
        if (this.key instanceof SecretKey) {
          this.type = "oct";
        }
        else if (this.key instanceof ECPublicKey) {
          this.type = "EC";
        }
        else if (this.key instanceof RSAPublicKey) {
          this.type = "RSA";
        }
        else {
          this.type = this.key.getAlgorithm();
        }
      }
    }

    private void defaultKey() {
      if (this.key == null) {
        this.key = this.certificates.get(0).getPublicKey();
      }
    }

    @Override
    public JWK build() {
      validateKey();
      defaultKey();
      defaultType();
      return new JcaJsonWebKey(key, createJwk());
    }

    private JsonObject createJwk() {
      final JsonObjectBuilder builder = Json.createObjectBuilder();
      if (id != null) {
        builder.add("kid", id);
      }
      if (type != null) {
        builder.add("kty", type);
      }
      final Base64.Encoder encoder = Base64.getUrlEncoder().withoutPadding();
      if (key instanceof SecretKey) {
        describeKey((SecretKey) key, encoder, builder);
      }
      else if (key instanceof RSAPublicKey) {
        describeKey((RSAPublicKey) key, encoder, builder);
      }
      else if (key instanceof ECPublicKey) {
        describeKey((ECPublicKey) key, encoder, builder);
      }
      else if (key != null) {
        throw new IllegalArgumentException(
            "unsupported key type: " + key.getAlgorithm());
      }
      if (!certificates.isEmpty()) {
        describeCertificates(certificates, encoder, builder);
      }
      if (algorithm != null) {
        builder.add("alg", algorithm);
      }
      if (use != null) {
        builder.add("use", use.toString());
      }
      if (!ops.isEmpty()) {
        builder.add("key_ops", ops.stream()
            .map(KeyOp::toString)
            .map(Json::createValue)
            .collect(JsonCollectors.toJsonArray()));
      }
      return builder.build();
    }

    private void validateKey() {
      if (this.key == null && this.certificates.isEmpty()) {
        throw new IllegalArgumentException("either a key or certificate is required");
      }
    }

  }

  public static Builder builder() {
    return new Builder();
  }

  @Override
  public Key getKey() {
    return key;
  }

  @Override
  public JsonObject toJson() {
    return delegate;
  }

  @Override
  public String toString() {
    return delegate.toString();
  }

  private static void describeKey(SecretKey secretKey, Base64.Encoder encoder,
      JsonObjectBuilder builder) {
    builder.add("k", encoder.encodeToString(secretKey.getEncoded()));
  }

  private static void describeKey(RSAPublicKey publicKey, Base64.Encoder encoder,
      JsonObjectBuilder builder) {
    final byte[] exponent = unsignedRaw(publicKey.getPublicExponent());
    final byte[] modulus = unsignedRaw(publicKey.getModulus());
    builder.add("n", encoder.encodeToString(modulus));
    builder.add("e", encoder.encodeToString(exponent));
  }

  private static void describeKey(ECPublicKey publicKey, Base64.Encoder urlEncoder,
      JsonObjectBuilder builder) {
    final ECParameterSpec params = publicKey.getParams();

    final int length = bitsToOctets(params.getCurve().getField().getFieldSize());
    final byte[] x = ecPad(publicKey.getW().getAffineX(), length);
    final byte[] y = ecPad(publicKey.getW().getAffineY(), length);

    builder.add("crv", ecCurve(publicKey));
    builder.add("x", urlEncoder.encodeToString(x));
    builder.add("y", urlEncoder.encodeToString(y));
  }

  private static void describeCertificates(List certificates,
      Base64.Encoder urlEncoder, JsonObjectBuilder builder) {
    try {
      final X509Certificate certificate = certificates.get(0);
      final JsonArrayBuilder chain = Json.createArrayBuilder();
      for (final X509Certificate cert : certificates) {
        chain.add(Base64.getEncoder().encodeToString(cert.getEncoded()));
      }
      builder.add("x5c", chain);
      builder.add("x5t",
          urlEncoder.encodeToString(certThumbprint(certificate, "SHA")));
      builder.add("x5t#s256",
          urlEncoder.encodeToString(certThumbprint(certificate, "SHA-256")));
    }
    catch (CertificateEncodingException ex) {
      throw new RuntimeException(ex);
    }
  }

  static byte[] certThumbprint(Certificate certificate,
      String algorithm) {
    try {
      final MessageDigest md = MessageDigest.getInstance(algorithm);
      return md.digest(certificate.getEncoded());
    }
    catch (NoSuchAlgorithmException | CertificateEncodingException ex) {
      throw new RuntimeException(ex);
    }
  }

  /**
   * Converts a bit count to an integral number of octets needed to represent
   * that quantity of bits.
   * @param bits number of bits
   * @return corresponding number of bytes
   */
  static int bitsToOctets(int bits) {
    int octets = bits / 8;
    if (bits % 8 != 0) {
      octets++;
    }
    return octets;
  }

  /**
   * Zero pads an EC affine coordinate to an integral number of bytes that
   * correponds to the size of the underlying field in bits as required by
   * RFC 7517.
   *
   * @param value the coordinate to pad
   * @param length the number of bytes needed to represent all possible
   *    coordinates in the underlying field using unsigned two's complement
   *    representation
   * @return zero-padded two's complement representation of the coordinate
   */
  static byte[] ecPad(BigInteger value, int length) {
    byte[] buf = unsignedRaw(value);
    if (buf.length < length) {
      final byte[] pad = new byte[length];
      Arrays.fill(pad, (byte) 0);
      System.arraycopy(buf, 0, pad, length - buf.length, buf.length);
      buf = pad;
    }
    return buf;
  }

  /**
   * Gets the name to use for the JWK {@code crv:} property for a given
   * EC public key. Only named curves are supported by JWK. The OID that
   * identifies the curve in the public key is used to find the corresponding
   * name in the {@link #OID_TO_CURVE} map.
   *
   * @param publicKey the subject EC public key
   * @return curve name
   * @throws IllegalArgumentException if the curve OID is not recognized
   */
  static String ecCurve(ECPublicKey publicKey) {
    final SubjectPublicKeyInfo spki = SubjectPublicKeyInfo.getInstance(
        ASN1Sequence.getInstance(publicKey.getEncoded()));

    final AlgorithmIdentifier algorithmId = spki.getAlgorithm();

    final ASN1ObjectIdentifier oid = (ASN1ObjectIdentifier)
        algorithmId.getParameters();

    final String curve = OID_TO_CURVE.get(oid.toString());
    if (curve == null) {
      throw new IllegalArgumentException("unsupported EC curve");
    }
    return curve;
  }

  /**
   * Converts a non-negative big integer value to an unsigned two's complement
   * representation in a byte array.
   *
   * @param value the value to convert
   * @return two's complement representation
   * @throws IllegalArgumentException if {@code value} is negative.
   */
  static byte[] unsignedRaw(BigInteger value) {
    if (value.signum() < 0) {
      throw new IllegalArgumentException("value must be non-negative");
    }
    byte[] buf = value.toByteArray();
    if (value.signum() >= 0 && buf.length > 1 && buf[0] == 0) {
      buf = Arrays.copyOfRange(buf, 1, buf.length);
    }
    return buf;
  }


}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy