Ed255192018Suite.java

/*
 * Copyright 2024 Red Hat, Inc. and/or its affiliates
 * and other contributors as indicated by the @author tags.
 *
 * 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.keycloak.protocol.oid4vc.issuance.signing.vcdm;


import com.apicatalog.jsonld.JsonLd;
import com.apicatalog.jsonld.JsonLdError;
import com.apicatalog.jsonld.document.JsonDocument;
import com.apicatalog.jsonld.http.DefaultHttpClient;
import com.apicatalog.jsonld.http.media.MediaType;
import com.apicatalog.jsonld.json.JsonUtils;
import com.apicatalog.jsonld.loader.HttpLoader;
import com.apicatalog.rdf.Rdf;
import com.apicatalog.rdf.RdfDataset;
import com.apicatalog.rdf.io.RdfWriter;
import com.apicatalog.rdf.io.error.RdfWriterException;
import com.apicatalog.rdf.io.error.UnsupportedContentException;
import com.fasterxml.jackson.core.JsonProcessingException;
import io.setl.rdf.normalization.RdfNormalize;
import jakarta.json.JsonObject;
import jakarta.json.JsonValue;
import org.keycloak.crypto.SignatureSignerContext;
import org.keycloak.protocol.oid4vc.issuance.signing.SigningServiceException;
import org.keycloak.protocol.oid4vc.model.VerifiableCredential;
import org.keycloak.util.JsonSerialization;

import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Optional;

/**
 * Implementation of an LD-Crypto Suite for Ed25519Signature2018
 * {@see https://w3c-ccg.github.io/ld-cryptosuite-registry/#ed25519signature2018}
 * <p>
 * Canonicalization Algorithm: https://w3id.org/security#URDNA2015
 * Digest Algorithm: http://w3id.org/digests#sha256
 * Signature Algorithm: http://w3id.org/security#ed25519
 *
 * @author <a href="https://github.com/wistefan">Stefan Wiedemann</a>
 */
public class Ed255192018Suite implements LinkedDataCryptographicSuite {

    private final SignatureSignerContext signerContext;

    public static final String PROOF_TYPE = "Ed25519Signature2018";

    public Ed255192018Suite(SignatureSignerContext signerContext) {
        this.signerContext = signerContext;
    }

    @Override
    public byte[] getSignature(VerifiableCredential verifiableCredential) {
        byte[] transformedData = transform(verifiableCredential);
        byte[] hashedData = digest(transformedData);
        return sign(hashedData);
    }

    private byte[] transform(VerifiableCredential verifiableCredential) {

        try {
            String credentialString = JsonSerialization.mapper.writeValueAsString(verifiableCredential);

            var credentialDocument = JsonDocument.of(new StringReader(credentialString));

            var expandedDocument = JsonLd.expand(credentialDocument)
                    .loader(new HttpLoader(DefaultHttpClient.defaultInstance()))
                    .get();
            Optional<JsonObject> documentObject = Optional.empty();
            if (JsonUtils.isArray(expandedDocument)) {
                documentObject = expandedDocument.asJsonArray().stream().filter(JsonUtils::isObject).map(JsonValue::asJsonObject).findFirst();
            } else if (JsonUtils.isObject(expandedDocument)) {
                documentObject = Optional.of(expandedDocument.asJsonObject());
            }
            if (documentObject.isPresent()) {

                RdfDataset rdfDataset = JsonLd.toRdf(JsonDocument.of(documentObject.get())).get();
                RdfDataset canonicalDataset = RdfNormalize.normalize(rdfDataset);

                StringWriter writer = new StringWriter();
                RdfWriter rdfWriter = Rdf.createWriter(MediaType.N_QUADS, writer);
                rdfWriter.write(canonicalDataset);

                return writer.toString()
                        .getBytes(StandardCharsets.UTF_8);
            } else {
                throw new SigningServiceException("Was not able to get the expanded json.");
            }
        } catch (JsonProcessingException e) {
            throw new SigningServiceException("Was not able to serialize the credential", e);
        } catch (JsonLdError e) {
            throw new SigningServiceException("Was not able to create a JsonLD Document from the serialized string.", e);
        } catch (UnsupportedContentException | IOException | RdfWriterException e) {
            throw new SigningServiceException("Was not able to canonicalize the json-ld.", e);
        }

    }

    private byte[] digest(byte[] transformedData) {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            return md.digest(transformedData);
        } catch (NoSuchAlgorithmException e) {
            throw new SigningServiceException("Algorithm SHA-256 not supported.", e);
        }
    }

    private byte[] sign(byte[] hashData) {
        return signerContext.sign(hashData);
    }


    @Override
    public String getProofType() {
        return PROOF_TYPE;
    }
}