четверг, 25 мая 2017 г.

Использование BouncyCastle вместо TumarCSP

Казахская криптография очень похожа на российскую, те же алгоритмы, те же ГОСТы, все отличие - в OID и форматах приватных ключей.
К примеру, ГОСТ 34.310-2004 соответсвует ГОСТ Р 34.10-2001, ГОСТ 34.311-95 соответсвует ГОСТ Р 34.11-94.
Так что если есть задача реализовать вызов WS-Security веб-сервиса с подписью по алгоритму  http://www.w3.org/2001/04/xmldsig-more#gost34310-gost34311 и хэшом по алгоритму http://www.w3.org/2001/04/xmldsig-more#gost34311 ( AvangardPlat к примеру ), то можно обойтись стандартными средствами типа BouncyCastle ( проверял на версии 1.57, последней на данный момент ), по крайней мере для ряда алгоритмов.

Задача состоит по большому счету из 3 частей - разбор плючевой пары из некоего проприетарного формата TumarCSP, выбор другого набора S-блоков для функции хэширования, регистрация алгоритмов подписи и функции хэширования для xmlsec.

К сожалению, придется писать свой собственный маленький криптопровайдер, который будет большую часть своей работы делегировать BouncyCastle.

Начнем с OID, у компании, которая разрабатывает TumarCSP есть свой набор идентификаторов, которые конечно же никто в BouncyCastle или OpenSSL не добавит. Частично посмотреть можно здесь http://www.oid-info.com/get/1.3.6.1.4.1.6801.1

смотрим сертификат с помощью dumpasn1, публичный ключ 80 байт, и первые 16 какие-то подозрительные 06 02 00 00 3A AA 00 00 00 45 43 31 00 02 00 00 если смотреть в ascii., то 45 43 31 это EC1, а 0xAA3A - внутренний идентификатор Tumar для алгоритма 1.3.6.1.4.1.6801.1.5.8. То есть тут продублирована некая описательная часть ключа, которая нам уже известна, нам интересны следуюшие 64 байта, которые представляют собой координаты точки на эллиптической кривой.

285  99:             SEQUENCE {
287  14:               SEQUENCE {
289  10:                 OBJECT IDENTIFIER '1 3 6 1 4 1 6801 1 5 8'
301   0:                 NULL
       :                 }
303  81:               BIT STRING
       :                 06 02 00 00 3A AA 00 00 00 45 43 31 00 02 00 00
       :                 59 5A 9B 09 B7 6E AA 2C 3E F6 37 1C D0 15 42 18
       :                 39 EF 5B 15 D1 D8 11 E4 D7 E7 84 4C 00 2A 11 19
       :                 61 C5 C3 E5 F1 44 98 46 93 0D 62 96 44 F6 E7 E7
       :                 82 4A EF 5D B2 12 A9 8C F6 2D 31 56 27 8F 7A 6F
       :               }

ищем что такое 1.3.6.1.4.1.6801.1.5.8, находим, что это все равно что CryptoProObjectIdentifiers.gostR3410_2001_CryptoPro_A (1.2.643.2.2.35.1) прописываем идентификатор.

public class GammaOID {
    static private final ASN1ObjectIdentifier GOST_id = new ASN1ObjectIdentifier("1.3.6.1.4.1.6801.1");
    static private final ASN1ObjectIdentifier GOST_34310_2004 = GOST_id.branch("5");
    static final ASN1ObjectIdentifier GOST_34310_2004_parameter_A = GOST_34310_2004.branch("8");
}

Теперь можно создавать провайдера, прописав ему кастомную KeyFactory для алгоритма 1.3.6.1.4.1.6801.1.5.8

public class KzGostProvider extends Provider {

    public KzGostProvider() {
        super("GOST", 1.0, "Little kazakh provider");
        put("Alg.Alias.KeyFactory." + GammaOID.GOST_34310_2004_parameter_A, "KZGOST");
        put("KeyFactory.KZGOST", "com.gost.jce.KzGostKeyFactorySpi");
    }
}

в KzGostKeyFactorySpi нужно реализовать логику по разбору ключей. Мы будем разбирать только тип ключей GammaOID.GOST_34310_2004_parameter_A, это позволит немного упростить код. Возможно другие типы точно так же просто разбираются, не проверял.

public class KzGostKeyFactorySpi extends org.bouncycastle.jcajce.provider.asymmetric.ecgost.KeyFactorySpi {
    public PrivateKey generatePrivate(PrivateKeyInfo keyInfo) throws IOException {
        ASN1ObjectIdentifier algOid = keyInfo.getPrivateKeyAlgorithm().getAlgorithm();
        if (algOid.equals(GammaOID.GOST_34310_2004_parameter_A)) {
            return KzGostKeyUtils.populateFromPrivKeyInfo(keyInfo);
        } else {
            throw new IOException("algorithm identifier " + algOid + " in key not recognised");
        }
    }

    public PublicKey generatePublic(SubjectPublicKeyInfo keyInfo) throws IOException {
        ASN1ObjectIdentifier algOid = keyInfo.getAlgorithm().getAlgorithm();
        if (algOid.equals(GammaOID.GOST_34310_2004_parameter_A)) {
            return KzGostKeyUtils.populateFromPubKeyInfo(keyInfo);
        } else {
            throw new IOException("algorithm identifier " + algOid + " in key not recognised");
        }
    }
}

Вот где вся логика по чтению ключей, она большей частью взята из BouncyCastle, различие только в представлении публичного ключа - пришлось выкинуть первые 16 байт заголовка. Важно! x и y в публичном ключе нужно разворачивать, а в приватном - d не нужно. И BouncyCastle проверяет публичный ключ, но не делает этого для приватного. В приватник можно скормить все что угодно, оно якобы будет работать, но по факту подпись не будет проходить проверку.

public class KzGostKeyUtils {

    private static ASN1ObjectIdentifier OID = CryptoProObjectIdentifiers.gostR3410_2001_CryptoPro_A;

    public static BCECGOST3410PrivateKey populateFromPrivKeyInfo(PrivateKeyInfo kryInfo) throws IOException {

        ECNamedCurveParameterSpec spec = ECGOST3410NamedCurveTable.getParameterSpec(OID.toString());
        ECCurve var4 = spec.getCurve();
        EllipticCurve curve = EC5Util.convertCurve(var4, spec.getSeed());
        ECParameterSpec ecSpec = new ECNamedCurveSpec(ECGOST3410NamedCurves.getName(OID), curve, new ECPoint(spec.getG().getAffineXCoord().toBigInteger(), spec.getG().getAffineYCoord().toBigInteger()), spec.getN(), spec.getH());
        ASN1Encodable privateKey = kryInfo.parsePrivateKey();

        ASN1Sequence sequence = (ASN1Sequence) privateKey;
        // 0 - integer !
        // 1 - private key
        DEROctetString key = (DEROctetString) sequence.getObjectAt(1);
        byte[] keyOctets = key.getOctets();
        BigInteger d = new BigInteger(1, keyOctets);
        ECPrivateKeySpec keySpec = new ECPrivateKeySpec(d, ecSpec);
        return new BCECGOST3410PrivateKey(keySpec);

    }

    public static BCECGOST3410PublicKey populateFromPubKeyInfo(SubjectPublicKeyInfo info) throws IOException {
        DERBitString bits = info.getPublicKeyData();

        String algorithm = "ECGOST3410";
        byte[] keyEnc = new byte[64];

        // skip first 16 byte - some type of header, not related.
        System.arraycopy(bits.getBytes(), 16, keyEnc, 0, 64);
        byte[] x = new byte[32];
        byte[] y = new byte[32];

        for (int i = 0; i != x.length; i++) {
            x[i] = keyEnc[32 - 1 - i];
        }

        for (int i = 0; i != y.length; i++) {
            y[i] = keyEnc[64 - 1 - i];
        }

        ECNamedCurveParameterSpec spec = ECGOST3410NamedCurveTable.getParameterSpec(OID.toString());
        ECCurve curve = spec.getCurve();
        EllipticCurve ellipticCurve = EC5Util.convertCurve(curve, spec.getSeed());
        org.bouncycastle.math.ec.ECPoint q = curve.createPoint(new BigInteger(1, x), new BigInteger(1, y));
        ECPublicKeyParameters publicKeyParameters = new ECPublicKeyParameters(q, ECUtil.getDomainParameters(null, spec));
        ECPoint p = new ECPoint(spec.getG().getAffineXCoord().toBigInteger(), spec.getG().getAffineYCoord().toBigInteger());
        ECNamedCurveSpec ecSpec = new ECNamedCurveSpec(
                spec.getName(),
                ellipticCurve,
                p, spec.getN(), spec.getH());

        return new BCECGOST3410PublicKey(algorithm, publicKeyParameters, ecSpec);
    }

Можно тестировать загрузку ключей из pkcs12

    @Before
    public void setUp() throws Exception {
        Security.addProvider(new BouncyCastleProvider());
        Security.addProvider(new KzGostProvider());
        XmlSecAlgorithmRegistrator.register(); //это будет описано далее
    }

    @Test
    public void testReadTumarKey() throws Exception{

        Set<Object> keySet = Security.getProvider("GOSTKZ").keySet();
        for(Object o : keySet){
            if(o.toString().contains("1.3.6.1.4.1.6801.1.5.8")) {
                System.out.println(o + " -> " + Security.getProvider("GOSTKZ").get(o));
            }
        }

        String keyPath = "key/GOST/pkcs12_sign.p12";
        KeyStore ks = KeyStore.getInstance("PKCS12");
        ks.load(new FileInputStream(keyPath), password.toCharArray());
        Assert.assertNotNull(ks);
        Enumeration<String> aliases = ks.aliases();
        String a;
        PrivateKey privateKey = null;
        X509Certificate cert = null;
        while (aliases.hasMoreElements()){
            a = aliases.nextElement();
            System.out.println(a);
            if(ks.isKeyEntry(a)) {
                privateKey = (PrivateKey) ks.getKey(a, password.toCharArray());
                Assert.assertNotNull(privateKey);
                cert = (X509Certificate) ks.getCertificate(a);
                Assert.assertNotNull(cert);
            }
        }
   }

Но это только треть работы. Приступаем к формированию подписи с помошью org.apache.santuario.xmlsec (2.0.8) последней на данный момент версии.
Нам нужно зарестрировать алгоритмы для xmlsec

public class XmlSecAlgorithmRegistrator {

    public static final String signatureUrl = "http://www.w3.org/2001/04/xmldsig-more#gost34310-gost34311";
    public static final String digestUrl = "http://www.w3.org/2001/04/xmldsig-more#gost34311";

    public static void register() throws ClassNotFoundException, AlgorithmAlreadyRegisteredException, XMLSignatureException, ParserConfigurationException {
        Init.init();
        DocumentBuilderFactory documentbuilderfactory = DocumentBuilderFactory.newInstance();
        documentbuilderfactory.setNamespaceAware(true);

        SignatureAlgorithm.register(signatureUrl, "com.gost.xmlsec.XmlSecSignatureGost34310$Gost34310Gost34311XmlSec");
        SignatureAlgorithm.register(digestUrl, "com.gost.xmlsec.XmlSecSignatureGost34310$Gost34311XmlSec");


        String throwable = "http://www.xmlsecurity.org/NS/#configuration";
        Document document = documentbuilderfactory.newDocumentBuilder().newDocument();
        {
            Element element = document.createElementNS(throwable, "Algorithm");
            element.setAttribute("URI", signatureUrl);
            element.setAttribute("Description", "GOST 34310 Digital Signature Algorithm with GOST 34311 Digest");
            element.setAttribute("AlgorithmClass", "Signature");
            element.setAttribute("RequirementLevel", "OPTIONAL");
            element.setAttribute("JCEName", "ECGOST34310");
            JCEMapper.Algorithm algorithm = new JCEMapper.Algorithm(element);
            JCEMapper.register(signatureUrl, algorithm);
        }
        {
            Element element = document.createElementNS(throwable, "Algorithm");
            element.setAttribute("URI", digestUrl);
            element.setAttribute("Description", "GOST 34311 Digest");
            element.setAttribute("AlgorithmClass", "MessageDigest");
            element.setAttribute("RequirementLevel", "OPTIONAL");
            element.setAttribute("JCEName", "ECGOST34311");
            JCEMapper.Algorithm algorithm = new JCEMapper.Algorithm(element);
            JCEMapper.register(digestUrl, algorithm);
        }

    }

}

тут примечательны строки 
        SignatureAlgorithm.register(signatureUrl, "com.gost.xmlsec.XmlSecSignatureGost34310$Gost34310Gost34311XmlSec");
        SignatureAlgorithm.register(digestUrl, "com.gost.xmlsec.XmlSecSignatureGost34310$Gost34311XmlSec");

в которых происходит регистрация классов оберток для реализаций алгоритмов, и 
            element.setAttribute("JCEName", "ECGOST34310"); 
            element.setAttribute("JCEName", "ECGOST34311");
в которых происходит указание на то. какой класс из JCE провайдера использовать.

public abstract class XmlSecSignatureGost34310 extends org.apache.xml.security.algorithms.SignatureAlgorithmSpi {
    private Signature signatureAlgorithm = null;

    public abstract String engineGetURI();

    public XmlSecSignatureGost34310() throws XMLSignatureException {
        String s = JCEMapper.translateURItoJCEID(this.engineGetURI());
        try {
            this.signatureAlgorithm = Signature.getInstance(s);
        } catch (NoSuchAlgorithmException var4) {
            throw new XMLSignatureException("algorithms.NoSuchAlgorithm", new Object[]{s, var4.getLocalizedMessage()});
        }
    }

    protected void engineSetParameter(AlgorithmParameterSpec spec) throws XMLSignatureException {
        try {
            this.signatureAlgorithm.setParameter(spec);
        } catch (InvalidAlgorithmParameterException e) {
            throw new XMLSignatureException(e);
        }
    }
    .... куча методов - оберток, делегирующих методы signatureAlgorithm

    protected String engineGetJCEAlgorithmString() {
        return this.signatureAlgorithm.getAlgorithm();
    }

    protected String engineGetJCEProviderName() {
        return this.signatureAlgorithm.getProvider().getName();
    }

    protected void engineSetHMACOutputLength(int i) throws XMLSignatureException {
        throw new XMLSignatureException("algorithms.HMACOutputLengthOnlyForHMAC");
    }

    protected void engineInitSign(Key key, AlgorithmParameterSpec algorithmparameterspec) throws XMLSignatureException {
        throw new XMLSignatureException("algorithms.CannotUseAlgorithmParameterSpecOnSignatureGost34310");
    }

    public static class Gost34310Gost34311XmlSec extends XmlSecSignatureGost34310 {

        public String engineGetURI() {
            return "http://www.w3.org/2001/04/xmldsig-more#gost34310-gost34311";
        }

        public Gost34310Gost34311XmlSec() throws XMLSignatureException {
        }
    }
    public static class Gost34311XmlSec extends XmlSecSignatureGost34310 {

        public String engineGetURI() {
            return "http://www.w3.org/2001/04/xmldsig-more#gost34311";
        }

        public Gost34311XmlSec() throws XMLSignatureException {
        }
    }
}

и в провайдер добавляются строки 

        put("Signature.ECGOST34310", "com.gost.jce.ECGOST34310SignatureSpi"); // signature ECGOST3410 with digest GOST3411 with D-Test SBlock
        put("MessageDigest.ECGOST34311", "com.gost.jce.ECGOST34311Digest"); // hash GOST3411 with D-Test SBlock

которые отсылают к конкретной реализации подписи и хэша. Если бы Tumar использовал для хэширования те же самые S-блоки, что и BouncyCastle (D-A) то создавать отдельных файлов не потребовалось бы, а так приходится заменять digest GOST3411Digest другим экземпляром, созданным с правильными S-блоками, иначе другое значение хэша и подпись не пройдет проверку.

Тут через рефлекшн подменяется поле digest.
public class ECGOST34310SignatureSpi extends org.bouncycastle.jcajce.provider.asymmetric.ecgost.SignatureSpi {

    public ECGOST34310SignatureSpi() throws Exception {
            final Field digest = SignatureSpi.class.getDeclaredField("digest");
            digest.setAccessible(true);
            digest.set(this, new GOST3411Digest(GOST28147Engine.getSBox("D-test")));
        }catch (NoSuchFieldException ex){
            logger.error("tested with BouncyCastle 1.57, check private field  `digest` in org.bouncycastle.jcajce.provider.asymmetric.ecgost.SignatureSpi", ex);
           throw ex;
        }catch (SecurityException | IllegalAccessException ex){
            logger.error(ex);
            throw ex;
        }
    }
}

А тут просто тривиальная реализация с правильным digest.
public class ECGOST34311Digest extends MessageDigest {
    private GOST3411Digest digest = new GOST3411Digest(GOST28147Engine.getSBox("D-test"));

    public ECGOST34311Digest() {
        super("GOST3411");
    }

    protected ECGOST34311Digest(String algorithm) {
        super(algorithm);
    }

    @Override
    protected void engineUpdate(byte input) {
        digest.update(input);
    }

    @Override
    protected void engineUpdate(byte[] input, int offset, int len) {
        digest.update(input, offset, len);
    }

    @Override
    protected byte[] engineDigest() {
        byte[] hash = new byte[digest.getDigestSize()];
        digest.doFinal(hash, 0);
        return hash;
    }

    @Override
    protected void engineReset() {
        digest.reset();
    }
}

В принципе, теперь почти все готово для тестирования, нужно только написать классы-обертки для формирования и проверки подписи.

public class XmlSecRequestHelper {

    public static String signRequest(String requestXml, X509Certificate cert, PrivateKey privateKey) throws Exception {
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        dbf.setNamespaceAware(true);
        DocumentBuilder documentBuilder = dbf.newDocumentBuilder();
        Document doc = documentBuilder.parse(new ByteArrayInputStream(requestXml.getBytes(UTF_8)));

        Element tbsEl = (Element) doc.getElementsByTagName("body").item(0);
        tbsEl.setIdAttribute("id", true);

        XMLSignature xmlSignature = new XMLSignature(doc, "", XmlSecAlgorithmRegistrator.signatureUrl);

        Transforms transforms = new Transforms(doc);
        transforms.addTransform(TRANSFORM_ENVELOPED_SIGNATURE);
        transforms.addTransform(TRANSFORM_C14N_WITH_COMMENTS);

        doc.getElementsByTagName("security").item(0).appendChild(xmlSignature.getElement());

        xmlSignature.addDocument("#signedContent", transforms, XmlSecAlgorithmRegistrator.digestUrl);
        xmlSignature.addKeyInfo(cert);
        xmlSignature.sign(privateKey);

        StringWriter os = new StringWriter();
        TransformerFactory tf = TransformerFactory.newInstance();
        Transformer trans = tf.newTransformer();
        trans.transform(new DOMSource(doc), new StreamResult(os));
        os.close();
        return os.toString();
    }


    public static boolean checkSignature(String requestXml) throws XMLSecurityException, IOException, SAXException, ParserConfigurationException {

        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        dbf.setNamespaceAware(true);
        DocumentBuilder documentBuilder = dbf.newDocumentBuilder();
        Document doc = documentBuilder.parse(new ByteArrayInputStream(requestXml.getBytes(UTF_8)));
        Element sigElement = (Element) doc.getElementsByTagName("ds:Signature").item(0);
        ((Element) doc.getElementsByTagName("body").item(0)).setIdAttribute("id", true);
        XMLSignature signature = new XMLSignature(sigElement, "");
        KeyInfo ki = signature.getKeyInfo();
        return signature.checkSignatureValue(ki.getX509Certificate());
    }
}

и остаток теста

          String request ="<request><header><security/></header><body id=\"signedContent\"><serviceId>753</serviceId><accountId>7688</accountId><amount>123</amount><commission>10</commission></body></request>";

        String signedRequest = XmlSecRequestHelper.signRequest(request, cert, privateKey );
        System.out.println(signedRequest);

        String terminalId = "000";
        String userId = "111";
        String password = "xxx";

        String url = "https://xxx.xxx.xxx.xxx:8080/";
        HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection();
        connection.setHostnameVerifier(new HostnameVerifier() {
            @Override
            public boolean verify(String s, SSLSession sslSession) {
                return true;
            }
        });
        connection.setSSLSocketFactory(getSSLContext().getSocketFactory()); // getSSLContext возвращает кастомный SSL контекст, поскольку сервер использует самоподписанный сертификат
        connection.setDoOutput(true);
        connection.setRequestMethod("POST");
        connection.setRequestProperty("Content-Type", "application/xml");
        connection.setRequestProperty("ProtocolVersion", "1");
        connection.setRequestProperty("OperationType", "1");
        connection.setRequestProperty("Authorization", "Basic " + java.util.Base64.getEncoder().encodeToString((terminalId + "|" + userId + ":" + password).getBytes(UTF_8)));

        try( DataOutputStream wr = new DataOutputStream( connection.getOutputStream())) {
            wr.write( signedRequest.getBytes(UTF_8) );
            wr.flush();
        }
        Reader in = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));
        Map<String, List<String>> headers = connection.getHeaderFields();

        for(Map.Entry<String, List<String>> entry : headers.entrySet()){
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
        System.out.println("--------------------------------------------");
        StringBuffer sb = new StringBuffer();

        for (int c; (c = in.read()) >= 0;) {
            System.out.print((char) c);
            sb.append((char)c);
        }
        String prolog = "<?xml version=\"1.0\" encoding=\"utf-8\"?>";
        int ind = sb.indexOf(prolog);
        sb.insert(ind + prolog.length(), "\n"); // если это топо, но работает, то это не так уж и тупо?
        System.out.println("signature valid : " + XmlSecRequestHelper.checkSignature(sb.toString()));

Как результат - запрос отправляется, подпись валидна, TumarCSP не используется.


понедельник, 15 мая 2017 г.

Разбор публичного ключа в фомате ssh-rsa

Иногда приходится работать в java с фоматами ключей, для которых не написано никаких дополнительных библиотек, т возможность использовать openssl нет, либо не хочется. К примеру - публичные ssh ключи, которые генерит putty.exe Их формат описан в RFC 4253 в разделе 6.6


The "ssh-rsa" key format has the following specific encoding:

  string    "ssh-rsa"
  mpint     e
  mpint     n

Here the 'e' and 'n' parameters form the signature key blob.
Смотрим что внутри файла, предварительно декодиров его из base64


Выглядит довольно просто, непонятно только что такое mpint, ну и string похоже не так прост. Описание находим в RFC 4251 в разделе 5.
 
  string

      Arbitrary length binary string.  Strings are allowed to contain
      arbitrary binary data, including null characters and 8-bit
      characters.  They are stored as a uint32 containing its length
      (number of bytes that follow) and zero (= empty string) or more
      bytes that are the value of the string.  Terminating null
      characters are not used.

      Strings are also used to store text.  In that case, US-ASCII is
      used for internal names, and ISO-10646 UTF-8 for text that might
      be displayed to the user.  The terminating null character SHOULD
      NOT normally be stored in the string.  For example: the US-ASCII
      string "testing" is represented as 00 00 00 07 t e s t i n g.  The
      UTF-8 mapping does not alter the encoding of US-ASCII characters.

   mpint

      Represents multiple precision integers in two's complement format,
      stored as a string, 8 bits per byte, MSB first.  Negative numbers
      have the value 1 as the most significant bit of the first byte of
      the data partition.  If the most significant bit would be set for
      a positive number, the number MUST be preceded by a zero byte.
      Unnecessary leading bytes with the value 0 or 255 MUST NOT be
      included.  The value zero MUST be stored as a string with zero
      bytes of data.

      By convention, a number that is used in modular computations in
      Z_n SHOULD be represented in the range 0 <= x < n.
      Examples:

         value (hex)        representation (hex)
         -----------        --------------------
         0                  00 00 00 00
         9a378f9b2e332a7    00 00 00 08 09 a3 78 f9 b2 e3 32 a7
         80                 00 00 00 02 00 80
         -1234              00 00 00 02 ed cc
         -deadbeef          00 00 00 05 ff 21 52 41 11

Теперь можно писать конвертер.
Напишем функцию для чтения байтов, которая сначала читает 4 байта длины, а затем остальное значение.


private byte[] readBytes(ByteBuffer buffer, AtomicInteger pos){
    int len = buffer.getInt(pos.get());
    byte buff[] = new byte[len];
    for(int i = 0; i < len; i++) {
        buff[i] = buffer.get(i + pos.get() + SIZEOF_INT);
    }
    pos.set(pos.get() + SIZEOF_INT + len);
    return buff;
}
потом обертки для чтения строк и BigInteger


private BigInteger readMpint(ByteBuffer buffer, AtomicInteger pos){
    byte[] bytes = readBytes(buffer, pos);
    if(bytes.length == 0){
        return BigInteger.ZERO;
    }
    return new BigInteger(bytes);
}

private String readString(ByteBuffer buffer, AtomicInteger pos){
    byte[] bytes = readBytes(buffer, pos);
    if(bytes.length == 0){
        return "";
    }
    return new String(bytes, StandardCharsets.US_ASCII);}
Далее все тривиально, читаем и декодируем из base64 ключ, проверяем, что алгоритм ssh-rsa и считываем exponent и modulus
private static int SIZEOF_INT = 4;
private static String key1 = "AAAAB3NzaC1yc2EAAAADAQABAAABAQClAxT5S/WuX04OXBt9R59WcL45OmaU3M5U063lfyja7ovqaVR7/2kHtLF/LoCQCXSZMny8RTCGDjoXD7G/tGsyHFDHCI//Y1VDLE06AlDzrlu69DQY91+6gkhGjH3SF6us5hXlihrbSFLfAlSdkEs8gwSrspVQyuaOf+39dnMddhEDYYg+z0ce82ta/n8xPBWCp60nDEDayNjOsRgzDJKSujNfngjQTL1x6qKJj8BW/P5lLJE1nbMm9BQD9G7glJk86qh1I/tJCnij6On0m6KcdzVz8cU3sBgNeB433kGjJtpxXXmJB6Vuu5IverhyfpiB4hP9WlKa/LSzW+ZIdvl/";

@Test
public void convertkey() throws Exception {
    byte[] decoded = java.util.Base64.getDecoder().decode(key1);

    try {
        ByteBuffer byteBuffer = ByteBuffer.wrap(decoded);

        AtomicInteger position = new AtomicInteger();
        //first read algorithm, should be ssh-rsa
        String algorithm = readString(byteBuffer, position);
        System.out.println(algorithm);
        assert "ssh-rsa".equals(algorithm);
        // than read exponent
        BigInteger publicExponent = readMpint(byteBuffer, position);
        System.out.println("publicExponent = " + publicExponent);
        // than read modulus
        BigInteger modulus = readMpint(byteBuffer, position);
        System.out.println("modulus = " + modulus);
        RSAPublicKeySpec keySpec = new RSAPublicKeySpec(modulus, publicExponent);
        KeyFactory kf = KeyFactory.getInstance("RSA");
        PublicKey publicKey = kf.generatePublic(keySpec);

        System.out.printf("%s, is RSAPublicKey: %b%n", publicKey.getClass().getName(), publicKey instanceof RSAPublicKey);
    } catch (Exception e) {
        e.printStackTrace();
        throw e;
    }

}
Дальше с публичным ключом модно делать все что угодно, использую стандартные средства java.