From 3cf9f4e58f5062914005ce893815a2ea859a79dd Mon Sep 17 00:00:00 2001 From: Niels Thykier Date: Mon, 9 Dec 2024 13:12:31 +0100 Subject: [PATCH 1/4] SD-1822: Use certificates rather than public keys As a side-effect of this, we can now support EC keys instead of only RSA keys (without having to manually parse and understand the DER/ASN.1 format). --- .../ebl/crypto/PayloadSignerFactory.java | 177 +++++++++++------- .../ebl/crypto/impl/RSAPayloadSigner.java | 23 --- .../crypto/impl/X509BackedPayloadSigner.java | 22 +++ 3 files changed, 130 insertions(+), 92 deletions(-) delete mode 100644 ebl/src/main/java/org/dcsa/conformance/standards/ebl/crypto/impl/RSAPayloadSigner.java create mode 100644 ebl/src/main/java/org/dcsa/conformance/standards/ebl/crypto/impl/X509BackedPayloadSigner.java diff --git a/ebl/src/main/java/org/dcsa/conformance/standards/ebl/crypto/PayloadSignerFactory.java b/ebl/src/main/java/org/dcsa/conformance/standards/ebl/crypto/PayloadSignerFactory.java index 6b03b8ee..f2aab652 100644 --- a/ebl/src/main/java/org/dcsa/conformance/standards/ebl/crypto/PayloadSignerFactory.java +++ b/ebl/src/main/java/org/dcsa/conformance/standards/ebl/crypto/PayloadSignerFactory.java @@ -3,33 +3,53 @@ import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.JWSObject; import com.nimbusds.jose.JWSVerifier; +import com.nimbusds.jose.crypto.ECDSASigner; import com.nimbusds.jose.crypto.ECDSAVerifier; import com.nimbusds.jose.crypto.RSASSASigner; import com.nimbusds.jose.crypto.RSASSAVerifier; -import lombok.SneakyThrows; -import org.bouncycastle.util.io.pem.PemObject; -import org.bouncycastle.util.io.pem.PemReader; -import org.bouncycastle.util.io.pem.PemWriter; -import org.dcsa.conformance.core.UserFacingException; -import org.dcsa.conformance.standards.ebl.crypto.impl.RSAPayloadSigner; - +import java.io.ByteArrayInputStream; import java.io.StringReader; import java.io.StringWriter; +import java.math.BigInteger; import java.security.KeyFactory; import java.security.KeyPair; import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.cert.CertificateFactory; +import java.security.interfaces.ECPrivateKey; import java.security.interfaces.ECPublicKey; import java.security.interfaces.RSAPrivateCrtKey; +import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.RSAPublicKeySpec; -import java.security.spec.X509EncodedKeySpec; import java.util.Base64; +import java.util.Date; +import javax.security.auth.x500.X500Principal; +import lombok.SneakyThrows; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.openssl.PEMKeyPair; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.util.io.pem.PemObject; +import org.bouncycastle.util.io.pem.PemWriter; +import org.dcsa.conformance.core.UserFacingException; +import org.dcsa.conformance.standards.ebl.crypto.impl.X509BackedPayloadSigner; public class PayloadSignerFactory { + private static final SecureRandom SECURE_RANDOM = new SecureRandom(); + // Generated with `openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 4 -subj "/C=US/ST=Delaware/L=Delaware/O=SELFSIGNED/CN=foo" -nodes` // Contents in the `key.pem` + @SuppressWarnings("secrets:S6706") // It is a non-issue to us that the key is "leaked". private static final String CTK_SENDER_PRIVATE_KEY_PEM = """ -----BEGIN PRIVATE KEY----- MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCXTD3XOeBMYVZS @@ -61,6 +81,7 @@ public class PayloadSignerFactory { -----END PRIVATE KEY----- """; + @SuppressWarnings("secrets:S6706") // It is a non-issue to us that the key is "leaked". private static final String CTK_CARRIER_PRIVATE_RSA_KEY_PEM = """ -----BEGIN PRIVATE KEY----- MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC588633cONawxd @@ -92,47 +113,21 @@ public class PayloadSignerFactory { -----END PRIVATE KEY----- """; + @SuppressWarnings("secrets:S6706") // It is a non-issue to us that the key is "leaked". private static final String CTK_RECEIVER_PRIVATE_KEY_PEM = """ - -----BEGIN PRIVATE KEY----- - MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC/rvvQhg5xRz+u - 92MsFlPac5TeheeoGREmi/KTT4V2TbhgdwZ3MnYt4/mp2G1DFf+nqVHOAKTpix91 - saNAY5WE+gvxVyCWV3xXTfP35RsEmltJpqJ3DcPGYQiTm5EXnaDvplE9+yjSgXGg - S6QmjLNLMTLsg1vCYQGyulg6tJvNJajhVDVpQwDoH6X4h2vLBtT/tsXKhoqT3ZNI - XVjC9A7uS4pn59NiGbK/B9ulx0zUzCWS/D92laUdJ0fJYSAReERzENq5qbjUwmvL - oiz3vRsyRRZlKMLPV2hAGXCmFQz9iKdQrpqnfmM9suGrfrmI774xVFGfh9J6XV74 - SLBexhYZAgMBAAECggEABrGFEJCF05XR1vnDiEgVUIUFt0mEv910OFzdsSAvQGTR - Yej2HFZyQwL5dmFc22Faxo+GkEN8fr1BcXotAbQYhga3QQuyUx2l9WR+9vKUoXIE - awt7E94yrmw4APOHOwRhmMy9fIUXNVaY0aiiiEgUgLUsmo6xtxVtGkEgkJg68oxl - HAf3UCQg6Bka1ZRNzu0ZRhEy2+AdF1L09HRiGdSsGOr/OKsTVk3fxGu1Uq0COjFc - 6C3PrRQ0PcHFLHBNXOVnNjMIwyaICeONfkxL9ai/p1TStyfUgC7NW+7/aK4NJUBc - PFjsdtBJGSa2w1TV0Q8Vj5an0hJQIKcPot4HGLV6UwKBgQDrU0pcYa5bj/BwloI0 - O3DCfIj563XI1ac2qDj84XMnlqTs42R+l6NR4/3ykqhu8jgLZtr7qt9xgd4mJxes - Inm+fE3ngOaJX1qqvKfBRR9/3jOaCo9yl1jc0IrGWY8YEBo8dc1x09Txw7/xj+du - uKNLtnlxyWtBhBtVmHKLl1Sf6wKBgQDQhiTEtuCm4p1KOZDyg+goa0yRMMjxrv5X - xPaZt7Ef/cIBnfwWYmfT/H0aTsNC4BuAheBJyvEsgfKAMPjD8yjLsw52XHhT0r7M - GAG4DLLTa0hZgsC6UemtanvHI88w6JCJidEEKjdoenkvFPrj6TMKbWg5aeyqlSOi - Idh/McLlCwKBgG/9unTOk+DFVqLuLdbXtukHxVRS50IF08ciNcS7MkdT3PdTnF7W - oYX2X8OSYhAyu9NJRsvgXOgy6trzXcOwwImTtKuI363esFJy588Fq2D6CUq03eGl - /0dPA8wzkPLdru65DWWvbzcDdpRqbLR3sFb250LsnVuXmD6bB2BBS6ezAoGBALYf - 8408jQo1c1uY29h1DRgAX2eQTHGKfer6xMeNgM6IPCJdcge6+yRTqpCHqlOGmX6v - by4EapCNDtiX7S53+nGvejo2mYHc13g6n4W40ZeGZDKJ2PrjAE3Oaz2LMTNubI80 - J7KTjMFb9uwATwEwdLvuwtEiiuqSSAUbupOdSrPxAoGBANtVFaBAvt6DwYm5TfPJ - TXoIcUmuf0FrqQg2CarBFTzgBV759c7Dk1P2TnWAeB+grh/AcU5WFBpJIrvenKrw - u1IYO4pbMvjwiL2yzMruF7/GcoFONaCy2TMKdzu7ftpiCvXOcU9pb3IO5vUPssZ2 - 5fqQLI2XCduGjbk9bpe+pwat - -----END PRIVATE KEY----- + -----BEGIN EC PRIVATE KEY----- + MHcCAQEEIJTPoxr2hvrglK9q4L8UUBZk1QYm9Yv4wstC5BKPaxPYoAoGCCqGSM49 + AwEHoUQDQgAEBHrpbOJO5f60HZIq0p8Ia/Xp5SA+xQf6xk0JfVNi6Ny7bjCHy7bK + 0eQ2k/puDGgQiT0nzfW5SC0LwTGc712uZw== + -----END EC PRIVATE KEY----- """; private static final KeyPair CTK_SENDER_RSA_KEY_PAIR = parsePEMPrivateRSAKey(CTK_SENDER_PRIVATE_KEY_PEM); private static final KeyPair CTK_CARRIER_RSA_KEY_PAIR = parsePEMPrivateRSAKey(CTK_CARRIER_PRIVATE_RSA_KEY_PEM); - private static final KeyPair CTK_RECEIVER_RSA_KEY_PAIR = parsePEMPrivateRSAKey(CTK_RECEIVER_PRIVATE_KEY_PEM); + private static final KeyPair CTK_RECEIVER_EC_KEY_PAIR = parsePEMPrivateECKey(CTK_RECEIVER_PRIVATE_KEY_PEM); private static final PayloadSignerWithKey CTK_SENDER_KEY_PAYLOAD_SIGNER = rsaBasedPayloadSigner(CTK_SENDER_RSA_KEY_PAIR); private static final PayloadSignerWithKey CTK_SENDER_INCORRECT_KEY_PAYLOAD_SIGNER = rsaBasedPayloadSigner(CTK_CARRIER_RSA_KEY_PAIR); - private static final PayloadSignerWithKey CTK_RECEIVER_KEY_PAYLOAD_SIGNER = rsaBasedPayloadSigner(CTK_RECEIVER_RSA_KEY_PAIR); - - public static String getCarrierPublicKeyInPemFormat() { - return pemEncodeKey((RSAPublicKey) CTK_CARRIER_RSA_KEY_PAIR.getPublic()); - } + private static final PayloadSignerWithKey CTK_RECEIVER_KEY_PAYLOAD_SIGNER = ecBasedPayloadSigner(CTK_RECEIVER_EC_KEY_PAIR); public static PayloadSignerWithKey senderPayloadSigner() { return CTK_SENDER_KEY_PAYLOAD_SIGNER; @@ -147,15 +142,26 @@ public static PayloadSignerWithKey carrierPayloadSigner() { } private static PayloadSignerWithKey rsaBasedPayloadSigner(KeyPair keyPair) { - return new RSAPayloadSigner( + return new X509BackedPayloadSigner( new JWSSignerDetails( JWSAlgorithm.PS256, new RSASSASigner(keyPair.getPrivate()) ), - (RSAPublicKey) keyPair.getPublic() + generateSelfSignedCertificateSecret(keyPair) ); } + @SneakyThrows + private static PayloadSignerWithKey ecBasedPayloadSigner(KeyPair keyPair) { + return new X509BackedPayloadSigner( + new JWSSignerDetails( + JWSAlgorithm.ES256, + new ECDSASigner((ECPrivateKey) keyPair.getPrivate()) + ), + generateSelfSignedCertificateSecret(keyPair) + ); + } + @SneakyThrows private static KeyPair parsePEMPrivateRSAKey(String pem) { String privKeyPEM = pem.replace("-----BEGIN PRIVATE KEY-----", "") @@ -171,27 +177,41 @@ private static KeyPair parsePEMPrivateRSAKey(String pem) { return new KeyPair(publicKey, privateKey); } + + @SneakyThrows + private static KeyPair parsePEMPrivateECKey(String pem) { + ECPrivateKey privateKey; + try (var pemParser = new PEMParser(new StringReader(pem))) { + var privateKeyInfo = (PEMKeyPair)pemParser.readObject(); + privateKey = (ECPrivateKey) new JcaPEMKeyConverter().getPrivateKey(privateKeyInfo.getPrivateKeyInfo()); + var publicKey = (ECPublicKey) new JcaPEMKeyConverter().getPublicKey(privateKeyInfo.getPublicKeyInfo()); + return new KeyPair(publicKey, privateKey); + } + } + @SneakyThrows public static SignatureVerifier verifierFromPublicKey(PublicKey publicKey) { if (publicKey instanceof RSAPublicKey rsaPublicKey) { - return new SingleExportableKeySignatureVerifier(new RSASSAVerifier(rsaPublicKey), rsaPublicKey); + return new SingleKeySignatureVerifier(new RSASSAVerifier(rsaPublicKey)); } if (publicKey instanceof ECPublicKey ecPublicKey) { return new SingleKeySignatureVerifier(new ECDSAVerifier(ecPublicKey)); } - throw new IllegalArgumentException("Unsupported public key; must be a RSAPublicKey or an ECPublicKey."); + throw new UserFacingException("Unsupported public key; must be a RSAPublicKey or an ECPublicKey."); } @SneakyThrows public static SignatureVerifier verifierFromPemEncodedPublicKey(String publicKeyPem) { - try (var reader = new PemReader(new StringReader(publicKeyPem))) { - var encoded = reader.readPemObject().getContent(); - var keySpec = new X509EncodedKeySpec(encoded); - KeyFactory kf = KeyFactory.getInstance("RSA"); - var rsaPublicKey = (RSAPublicKey)kf.generatePublic(keySpec); - return new SingleKeySignatureVerifier(new RSASSAVerifier(rsaPublicKey)); + try (var reader = new PEMParser(new StringReader(publicKeyPem))) { + var parsedObject = reader.readObject(); + if (parsedObject instanceof X509CertificateHolder x509CertificateHolder) { + var cert = CertificateFactory.getInstance("X.509") + .generateCertificate(new ByteArrayInputStream(x509CertificateHolder.getEncoded())); + return verifierFromPublicKey(cert.getPublicKey()); + } + throw new UserFacingException("The provided PEM object was a X509 encoded certificate. Please provide a CERTIFICATE instead"); } catch (Exception e) { - throw new UserFacingException("Could not parse provided string as an X509 PEM encoded RSA public key"); + throw new UserFacingException("Could not parse the PEM content string as an X509 encoded PEM certificate"); } } @@ -204,26 +224,45 @@ public boolean verifySignature(JWSObject jwsObject) { } } - private record SingleExportableKeySignatureVerifier(JWSVerifier jwsVerifier, RSAPublicKey rsaPublicKey) implements SignatureVerifierWithKey { - - @SneakyThrows - @Override - public boolean verifySignature(JWSObject jwsObject) { - return jwsObject.verify(jwsVerifier); - } - - @Override - public String getPublicKeyInPemFormat() { - return pemEncodeKey(this.rsaPublicKey); - } - } - @SneakyThrows - public static String pemEncodeKey(RSAPublicKey key) { + public static String pemEncodeCertificate(X509CertificateHolder x509CertificateHolder) { var w = new StringWriter(); try (var pemWriter = new PemWriter(w)) { - pemWriter.writeObject(new PemObject("PUBLIC KEY", key.getEncoded())); + pemWriter.writeObject(new PemObject("CERTIFICATE", x509CertificateHolder.getEncoded())); } return w.getBuffer().toString(); } + + private static X509CertificateHolder generateSelfSignedCertificateSecret(KeyPair keyPair) { + X500Principal subject = new X500Principal("CN=DCSA-Conformance-Toolkit"); + + long notBefore = System.currentTimeMillis(); + // 2500 days (several) years should be sufficient. + long notAfter = notBefore + (1000L * 3600L * 24 * 2500); + byte[] serialBytes = new byte[16]; + SECURE_RANDOM.nextBytes(serialBytes); + X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder( + subject, + new BigInteger(serialBytes), + new Date(notBefore), + new Date(notAfter), + subject, + keyPair.getPublic() + ); + + try { + certBuilder.addExtension(Extension.basicConstraints, true, new BasicConstraints(false)); + certBuilder.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment)); + + var algo = switch (keyPair.getPrivate()) { + case ECPrivateKey ignored -> "SHA256withECDSA"; + case RSAPrivateKey ignored -> "SHA256withRSA"; + default -> throw new UnsupportedOperationException("Unsupported key"); + }; + final ContentSigner signer = new JcaContentSignerBuilder(algo).build(keyPair.getPrivate()); + return certBuilder.build(signer); + } catch (Exception e) { + throw new UserFacingException("Error while generating a self-certificate: " + e.getMessage(), e); + } + } } diff --git a/ebl/src/main/java/org/dcsa/conformance/standards/ebl/crypto/impl/RSAPayloadSigner.java b/ebl/src/main/java/org/dcsa/conformance/standards/ebl/crypto/impl/RSAPayloadSigner.java deleted file mode 100644 index 546b5c20..00000000 --- a/ebl/src/main/java/org/dcsa/conformance/standards/ebl/crypto/impl/RSAPayloadSigner.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.dcsa.conformance.standards.ebl.crypto.impl; - -import org.dcsa.conformance.standards.ebl.crypto.JWSSignerDetails; -import org.dcsa.conformance.standards.ebl.crypto.PayloadSignerWithKey; - -import java.security.interfaces.RSAPublicKey; - -import static org.dcsa.conformance.standards.ebl.crypto.PayloadSignerFactory.pemEncodeKey; - -public class RSAPayloadSigner extends DefaultPayloadSigner implements PayloadSignerWithKey { - - private final RSAPublicKey rsaPublicKey; - - public RSAPayloadSigner(JWSSignerDetails jwsSignerDetails, RSAPublicKey rsaPublicKey) { - super(jwsSignerDetails); - this.rsaPublicKey = rsaPublicKey; - } - - @Override - public String getPublicKeyInPemFormat() { - return pemEncodeKey(this.rsaPublicKey); - } -} diff --git a/ebl/src/main/java/org/dcsa/conformance/standards/ebl/crypto/impl/X509BackedPayloadSigner.java b/ebl/src/main/java/org/dcsa/conformance/standards/ebl/crypto/impl/X509BackedPayloadSigner.java new file mode 100644 index 00000000..4953b8f1 --- /dev/null +++ b/ebl/src/main/java/org/dcsa/conformance/standards/ebl/crypto/impl/X509BackedPayloadSigner.java @@ -0,0 +1,22 @@ +package org.dcsa.conformance.standards.ebl.crypto.impl; + +import org.bouncycastle.cert.X509CertificateHolder; +import org.dcsa.conformance.standards.ebl.crypto.JWSSignerDetails; +import org.dcsa.conformance.standards.ebl.crypto.PayloadSignerWithKey; + +import static org.dcsa.conformance.standards.ebl.crypto.PayloadSignerFactory.pemEncodeCertificate; + +public class X509BackedPayloadSigner extends DefaultPayloadSigner implements PayloadSignerWithKey { + + private final X509CertificateHolder x509Cert; + + public X509BackedPayloadSigner(JWSSignerDetails jwsSignerDetails, X509CertificateHolder x509Cert) { + super(jwsSignerDetails); + this.x509Cert = x509Cert; + } + + @Override + public String getPublicKeyInPemFormat() { + return pemEncodeCertificate(this.x509Cert); + } +} From e31403f228226c52d4766847e5b500093f0bdba3 Mon Sep 17 00:00:00 2001 From: palatsangeetha <138600841+palatsangeetha@users.noreply.github.com> Date: Tue, 10 Dec 2024 15:47:30 +0100 Subject: [PATCH 2/4] SD-350 Update placeofissue-ebl (#242) --- .../eblissuance/schemas/EBL_ISS_v3.0.0.yaml | 38 +++++++++---------- .../standards/ebl/schemas/EBL_v3.0.0.yaml | 4 +- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/ebl-issuance/src/main/resources/standards/eblissuance/schemas/EBL_ISS_v3.0.0.yaml b/ebl-issuance/src/main/resources/standards/eblissuance/schemas/EBL_ISS_v3.0.0.yaml index ff1f1115..b8a459f6 100644 --- a/ebl-issuance/src/main/resources/standards/eblissuance/schemas/EBL_ISS_v3.0.0.yaml +++ b/ebl-issuance/src/main/resources/standards/eblissuance/schemas/EBL_ISS_v3.0.0.yaml @@ -6,11 +6,11 @@ info: version: 3.0.0 title: DCSA eBL Issuance API description: | -

DCSA OpenAPI specification for the issuance of an electronic Bill of Lading (eBL) via an eBL Solution Provider

+

DCSA OpenAPI specification for the issuance of an electronic Bill of Lading (referred to as `eBL`) via an eBL Solution Provider

- This API is intended as an API between a carrier and a EBL Solution Platform. + This API is intended as an API between a carrier and a eBL Solution Platform. - The EBL Solution Provider is to implement + The eBL Solution Provider is to implement PUT /v3/ebl-issuance-requests @@ -18,9 +18,9 @@ info: POST /v3/ebl-issuance-responses - for the EBL Solution Provider to call. + for the eBL Solution Provider to call. - When the document is to be surrendered, it should happen via a version of the [DCSA EBL Surrender](https://app.swaggerhub.com/apis-docs/dcsaorg/DCSA_EBL_SUR/3.0.0) API. + When the document is to be surrendered, it should happen via a version of the [DCSA eBL Surrender](https://app.swaggerhub.com/apis-docs/dcsaorg/DCSA_EBL_SUR/3.0.0) API. ### API Design & Implementation Principles This API follows the guidelines defined in version 2.1 of the API Design & Implementation Principles which can be found on the [DCSA Developer page](https://developer.dcsa.org/api_design) @@ -29,20 +29,20 @@ info: Please look at the following implementation guide for how to create [Digital Signatures](https://developer.dcsa.org/implementing-digital-signatures-for-transport-documents). ### Changelog and GitHub - For a changelog, please click [here](https://github.com/dcsaorg/DCSA-OpenAPI/tree/master/ebl/v3/issuance#v300). Please [create a GitHub issue](https://github.com/dcsaorg/DCSA-OpenAPI/issues/new) if you have any questions/comments. + For a changelog, please click [here](https://github.com/dcsaorg/DCSA-OpenAPI/tree/master/ebl/v3/issuance#v300). If you have any questions, feel free to [Contact Us](https://dcsa.org/get-involved/contact-us). API specification issued by [DCSA.org](https://dcsa.org/). license: name: Apache 2.0 - url: 'http://www.apache.org/licenses/LICENSE-2.0.html' + url: 'https://www.apache.org/licenses/LICENSE-2.0.html' contact: name: Digital Container Shipping Association (DCSA) url: 'https://dcsa.org' email: info@dcsa.org tags: - - name: Issuance EBL + - name: Issuance eBL description: | - Issuance EBL **implemented** by EBL Solution Platform + Issuance eBL **implemented** by eBL Solution Platform - name: Issuance Response description: | Issuance Response **implemented** by carrier @@ -50,13 +50,13 @@ paths: /v3/ebl-issuance-requests: put: tags: - - Issuance EBL + - Issuance eBL operationId: put-ebl-issuance-requests - summary: Request issuance of an EBL + summary: Request issuance of an eBL description: | - Submit a transport document (EBL) for issuance + Submit a transport document (eBL) for issuance - **This endPoint is to be implemented by an EBL Solution Provider for the carrier to call** + **This endPoint is to be implemented by an eBL Solution Provider for the carrier to call** parameters: - $ref: '#/components/parameters/Api-Version-Major' requestBody: @@ -78,7 +78,7 @@ paths: description: | An Issuance Request is made with a `Transport Document Reference` (TDR), that was used previously to request the issuance of a `Transport Document` (TD). The document is either already issued or an TD with the same TDR. - The eBL platform will inform the carrier when the carrier needs to act on this document again. If the issuance is pending, then the carrier will be notified via the DCSA_EBL_ISS_RSP API once the issuance process completes. If the issuance has already succeeded, the eBL platform will notify the carrier when there is a surrender request via the DCSA_EBL_SUR API. + The eBL platform will inform the carrier when the carrier needs to act on this document again. If the issuance is pending, then the carrier will be notified via the `/v3/ebl-issuance-responses` endPoint once the issuance process completes. If the issuance has already succeeded, the eBL platform will notify the carrier when there is a surrender request via the DCSA_EBL_SUR API. headers: API-Version: $ref: '#/components/headers/API-Version' @@ -124,7 +124,7 @@ paths: description: | Submit a response to a carrier issuance request. - **This endPoint is to be implemented by a carrier for the EBL Solution Provider to call** + **This endPoint is to be implemented by a carrier for the eBL Solution Provider to call** parameters: - $ref: '#/components/parameters/Api-Version-Major' requestBody: @@ -402,7 +402,7 @@ components: pattern: ^\S+$ maxLength: 4 description: | - The EBL platform of the transaction party. + The eBL platform of the transaction party. The value **MUST** be one of: - `WAVE` (Wave) - `CARX` (CargoX) @@ -3235,7 +3235,7 @@ components: issuanceResponseCode: type: string description: | - The platforms verdict on the issuance of the EBL identified by the `transportDocumentReference` + The platforms verdict on the issuance of the eBL identified by the `transportDocumentReference` Options are: - `ISSU`: The document was successfully `ISSU` and successfully delivered to the initial possessor. @@ -3274,7 +3274,7 @@ components: description: | An object to capture where the original Transport Document (`Bill of Lading`) will be issued. - The location can be specified as one of `UN Location Code` or `CountryCode`. + **Condition:** The location can be specified as one of `UN Location Code` or `CountryCode`, but not both. properties: locationName: type: string @@ -3450,7 +3450,7 @@ components: pattern: ^\S(?:.*\S)?$ maxLength: 35 description: | - Reference number assigned to an `Import License` or permit, issued by countries exercising import controls that athorizes the importation of the articles stated in the license. The `Import License` must be valid at time of arrival. + Reference number assigned to an `Import License` or permit, issued by countries exercising import controls that authorizes the importation of the articles stated in the license. The `Import License` must be valid at time of arrival. example: EMC007123 issueDate: type: string diff --git a/ebl/src/main/resources/standards/ebl/schemas/EBL_v3.0.0.yaml b/ebl/src/main/resources/standards/ebl/schemas/EBL_v3.0.0.yaml index 2fa70d4d..f1e99802 100644 --- a/ebl/src/main/resources/standards/ebl/schemas/EBL_v3.0.0.yaml +++ b/ebl/src/main/resources/standards/ebl/schemas/EBL_v3.0.0.yaml @@ -8511,7 +8511,7 @@ components: ########################################## DocumentPartiesShippingInstructions: type: object - title: Document Parties + title: Document Parties (Shipping Instructions) description: | All `Parties` with associated roles. @@ -8553,7 +8553,7 @@ components: ############################ DocumentPartiesHouseBL: type: object - title: Document Parties + title: Document Parties (House B/L) description: | All `Parties` with associated roles for this `House Bill of Lading`. From 71f94475208aba4413f043a9f1df924dbfb1ebe3 Mon Sep 17 00:00:00 2001 From: preetamnpr <128618622+preetamnpr@users.noreply.github.com> Date: Thu, 12 Dec 2024 13:05:38 +0100 Subject: [PATCH 3/4] SD-650 Check cargoGrossWeight conditions in RE and its subsequent commodities (#241) --- .../booking/checks/BookingChecks.java | 29 ++++++ .../booking/checks/BookingChecksTest.java | 96 +++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 booking/src/test/java/org/dcsa/conformance/standards/booking/checks/BookingChecksTest.java diff --git a/booking/src/main/java/org/dcsa/conformance/standards/booking/checks/BookingChecks.java b/booking/src/main/java/org/dcsa/conformance/standards/booking/checks/BookingChecks.java index 75062c54..ef02e31a 100644 --- a/booking/src/main/java/org/dcsa/conformance/standards/booking/checks/BookingChecks.java +++ b/booking/src/main/java/org/dcsa/conformance/standards/booking/checks/BookingChecks.java @@ -508,6 +508,34 @@ private static JsonNode getShipmentLocationTypeCode(JsonNode body, @NonNull Stri return issues; }); + static final JsonContentCheck CHECK_CARGO_GROSS_WEIGHT_CONDITIONS = + JsonAttribute.allIndividualMatchesMustBeValid( + "Check Cargo Gross Weight conditions", + mav -> mav.submitAllMatching("requestedEquipments.*"), + (nodeToValidate, contextPath) -> { + var issues = new LinkedHashSet(); + var cargoGrossWeight = nodeToValidate.path("cargoGrossWeight"); + if (cargoGrossWeight.isMissingNode() || cargoGrossWeight.isNull()) { + var commodities = nodeToValidate.path("commodities"); + if (!(commodities.isMissingNode() || commodities.isNull()) && commodities.isArray()) { + AtomicInteger commodityCounter = new AtomicInteger(0); + StreamSupport.stream(commodities.spliterator(), false) + .forEach( + commodity -> { + var commodityGrossWeight = commodity.path("cargoGrossWeight"); + int currentCommodityCount = commodityCounter.getAndIncrement(); + if (commodityGrossWeight.isMissingNode() + || commodityGrossWeight.isNull()) { + issues.add( + "The '%s' must have cargo gross weight at commodities position %s" + .formatted(contextPath, currentCommodityCount)); + } + }); + } + } + return issues; + }); + private static Supplier delayedValue(Supplier cspSupplier, Function field) { return () -> { var csp = cspSupplier.get(); @@ -611,6 +639,7 @@ private static void generateScenarioRelatedChecks(List checks, VALIDATE_SHIPPER_MINIMUM_REQUEST_FIELDS, VALIDATE_DOCUMENT_PARTY, NATIONAL_COMMODITY_TYPE_CODE_VALIDATION, + CHECK_CARGO_GROSS_WEIGHT_CONDITIONS, JsonAttribute.atLeastOneOf( JsonPointer.compile("/expectedDepartureDate"), JsonPointer.compile("/expectedArrivalAtPlaceOfDeliveryStartDate"), diff --git a/booking/src/test/java/org/dcsa/conformance/standards/booking/checks/BookingChecksTest.java b/booking/src/test/java/org/dcsa/conformance/standards/booking/checks/BookingChecksTest.java new file mode 100644 index 00000000..676850f2 --- /dev/null +++ b/booking/src/test/java/org/dcsa/conformance/standards/booking/checks/BookingChecksTest.java @@ -0,0 +1,96 @@ +package org.dcsa.conformance.standards.booking.checks; + +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.dcsa.conformance.core.check.JsonContentCheck; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.dcsa.conformance.core.toolkit.JsonToolkit.OBJECT_MAPPER; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class BookingChecksTest { + + private ObjectNode booking; + private ObjectNode requestedEquipment; + private ArrayNode requestedEquipments; + private ObjectNode commodity; + private ArrayNode commodities; + + @BeforeEach + void setUp() { + booking = OBJECT_MAPPER.createObjectNode(); + requestedEquipment = OBJECT_MAPPER.createObjectNode(); + requestedEquipments = OBJECT_MAPPER.createArrayNode(); + commodity = OBJECT_MAPPER.createObjectNode(); + commodities = OBJECT_MAPPER.createArrayNode(); + } + + @Test + void testCargoGrossWeightPresentAtRequestedEquipment_valid() { + requestedEquipment.set("cargoGrossWeight", OBJECT_MAPPER.createObjectNode()); + requestedEquipments.add(requestedEquipment); + booking.set("requestedEquipments", requestedEquipments); + JsonContentCheck check = BookingChecks.CHECK_CARGO_GROSS_WEIGHT_CONDITIONS; + Set error = check.validate(booking); + assertTrue(error.isEmpty()); + } + + @Test + void testCargoGrossWeightMissingAtRequestedEquipmentNoCommodities_valid() { + + booking.set("requestedEquipments", requestedEquipments); + JsonContentCheck check = BookingChecks.CHECK_CARGO_GROSS_WEIGHT_CONDITIONS; + Set errors = check.validate(booking); + assertTrue(errors.isEmpty()); + } + + @Test + void testCargoGrossWeightMissingAtRequestedEquipmentPresentAtCommodities_valid() { + commodity.set("cargoGrossWeight", OBJECT_MAPPER.createObjectNode()); + commodities.add(commodity); + requestedEquipment.set("commodities", commodities); + requestedEquipments.add(requestedEquipment); + booking.set("requestedEquipments", requestedEquipments); + JsonContentCheck check = BookingChecks.CHECK_CARGO_GROSS_WEIGHT_CONDITIONS; + Set errors = check.validate(booking); + assertTrue(errors.isEmpty()); + } + + @Test + void testCargoGrossWeightMissingInRequestedEquipmentsAndAlsoInCommodities_invalid() { + + commodities.add(commodity); + requestedEquipment.set("commodities", commodities); + requestedEquipments.add(requestedEquipment); + booking.set("requestedEquipments", requestedEquipments); + + JsonContentCheck check = BookingChecks.CHECK_CARGO_GROSS_WEIGHT_CONDITIONS; + Set errors = check.validate(booking); + assertEquals(1, errors.size()); + assertTrue( + errors.contains( + "The 'requestedEquipments[0]' must have cargo gross weight at commodities position 0")); + } + + @Test + void testCargoGrossWeightMissingInRequestedEquipmentsAndInOneOfTheCommodities_invalid() { + + ObjectNode commodity2 = OBJECT_MAPPER.createObjectNode(); + commodity.set("cargoGrossWeight", OBJECT_MAPPER.createObjectNode()); + commodities.add(commodity2); + commodities.add(commodity); + requestedEquipment.set("commodities", commodities); + requestedEquipments.add(requestedEquipment); + booking.set("requestedEquipments", requestedEquipments); + JsonContentCheck check = BookingChecks.CHECK_CARGO_GROSS_WEIGHT_CONDITIONS; + Set errors = check.validate(booking); + assertEquals(1, errors.size()); + assertTrue( + errors.contains( + "The 'requestedEquipments[0]' must have cargo gross weight at commodities position 0")); + } +} From a41b072b251df7a07c4c25b7834de53aa30830e1 Mon Sep 17 00:00:00 2001 From: palatsangeetha <138600841+palatsangeetha@users.noreply.github.com> Date: Thu, 12 Dec 2024 13:43:49 +0100 Subject: [PATCH 4/4] SD-603 Update PartyFunction EBL (#243) --- .../standards/ebl/checks/EBLChecks.java | 20 ++++++++++- .../standards/ebl/checks/EblDatasets.java | 4 +++ .../ebl/messages/ebl-api-3.0.0-request.json | 2 +- .../standards/ebl/checks/EBLChecksTest.java | 34 +++++++++++++++++++ 4 files changed, 58 insertions(+), 2 deletions(-) diff --git a/ebl/src/main/java/org/dcsa/conformance/standards/ebl/checks/EBLChecks.java b/ebl/src/main/java/org/dcsa/conformance/standards/ebl/checks/EBLChecks.java index 5c846917..451529ce 100644 --- a/ebl/src/main/java/org/dcsa/conformance/standards/ebl/checks/EBLChecks.java +++ b/ebl/src/main/java/org/dcsa/conformance/standards/ebl/checks/EBLChecks.java @@ -5,6 +5,8 @@ import static org.dcsa.conformance.standards.ebl.checks.EblDatasets.EXEMPT_PACKAGE_CODES; import static org.dcsa.conformance.standards.ebl.checks.EblDatasets.MODE_OF_TRANSPORT; import static org.dcsa.conformance.standards.ebl.checks.EblDatasets.NATIONAL_COMMODITY_CODES; +import static org.dcsa.conformance.standards.ebl.checks.EblDatasets.PARTY_FUNCTION_CODE; +import static org.dcsa.conformance.standards.ebl.checks.EblDatasets.PARTY_FUNCTION_CODE_HBL; import static org.dcsa.conformance.standards.ebl.checks.EblDatasets.REQUESTED_CARRIER_CLAUSES; @@ -666,6 +668,19 @@ private static Consumer allDg(Consumer mav.submitAllMatching("documentParties.other.*.partyFunction"), + JsonAttribute.matchedMustBeDatasetKeywordIfPresent(PARTY_FUNCTION_CODE)); + + static final JsonRebaseableContentCheck VALID_PARTY_FUNCTION_HBL = + JsonAttribute.allIndividualMatchesMustBeValid( + "The partyFunction in OtherDocumentParty of houseBillOfLadings is valid", + mav -> + mav.submitAllMatching("houseBillOfLadings.*.documentParties.other.*.partyFunction"), + JsonAttribute.matchedMustBeDatasetKeywordIfPresent(PARTY_FUNCTION_CODE_HBL)); + private static final List STATIC_SI_CHECKS = Arrays.asList( JsonAttribute.mustBeDatasetKeywordIfPresent( SI_REQUEST_SEND_TO_PLATFORM, @@ -684,6 +699,8 @@ private static Consumer allDg(Consumer allDg(Consumer invalidErrors = VALID_CONSIGMENT_ITEMS_REFERENCE_TYPES.validate(rootNode); assertEquals(1, invalidErrors.size()); } + + @Test + void testValidPartyFunction() { + ObjectNode documentParties = rootNode.putObject("documentParties"); + ArrayNode otherParties = documentParties.putArray("other"); + ObjectNode otherParty = otherParties.addObject(); + otherParty.put("partyFunction", "SCO"); + + Set errors = VALID_PARTY_FUNCTION.validate(rootNode); + assertEquals(0, errors.size()); + + otherParty.put("partyFunction", "SSS"); + errors = VALID_PARTY_FUNCTION.validate(rootNode); + assertEquals(1, errors.size()); + } + + @Test + void testValidPartyFunctionHBL() { + ArrayNode houseBillOfLadings = rootNode.putArray("houseBillOfLadings"); + ObjectNode hbl = houseBillOfLadings.addObject(); + ObjectNode documentParties = hbl.putObject("documentParties"); + ArrayNode otherParties = documentParties.putArray("other"); + ObjectNode otherParty = otherParties.addObject(); + otherParty.put("partyFunction", "CS"); + + Set errors = VALID_PARTY_FUNCTION_HBL.validate(rootNode); + assertEquals(0, errors.size()); + + otherParty.put("partyFunction", "SSS"); + errors = VALID_PARTY_FUNCTION_HBL.validate(rootNode); + assertEquals(1, errors.size()); + } }