document generation, and signing are usable now

This commit is contained in:
Ali Arya 2025-07-10 14:42:25 +03:30
parent 77ea548a50
commit cee1fa8ece
9 changed files with 240 additions and 30 deletions

View File

@ -34,3 +34,10 @@ IRALEX_AUTH_PASSWORD=account_password
# overwrites it if specified. If a port is not explicitly specified, then a
# default port is used.
PORT=8000
# Optional
# Whether to enable the debugging features or not, such as extra logging,
# different code path execution, etc. Any one of the following values is legal:
# 1 = t = T = true = True = TRUE = yes
# 0 = f = F = false = False = FALSE = no
DEBUG=true

View File

@ -2,8 +2,6 @@
package config
const (
DEBUG = true
PRODUCTION_URL = "https://api.myinvois.hasil.gov.my"
SANDBOX_URL = "https://preprod-api.myinvois.hasil.gov.my"
BASE_URL = PRODUCTION_URL

View File

@ -2,8 +2,8 @@ package core
import (
"fmt"
"iralex-einvoice/config"
"os"
"strconv"
"strings"
"github.com/joho/godotenv"
@ -16,9 +16,10 @@ var (
CLIENT_ID string
CLIENT_SECRET_1 string
CLIENT_SECRET_2 string
CERTIFICATE_FILENAME string // path to a PKCS12 file e.g. cert.p12, static/1f92.p12, etc
CERTIFICATE_PASSWORD string // the PIN of the aforementioned PKCS12 file
PORT string // the port the program listens to
CERTIFICATE_FILENAME string // path to a PKCS12 file e.g. cert.p12, static/1f92.p12, etc
CERTIFICATE_PASSWORD string // the PIN of the aforementioned PKCS12 file
PORT string // the port the program listens to
DEBUG bool = false // whether to use the debug or production environment
IRALEX_BACKEND_BASE_URL string
IRALEX_AUTH_EMAIL string // email address used to get an access token from the Iralex backend
IRALEX_AUTH_PASSWORD string // password used to get an access token from the Iralex backend
@ -50,6 +51,7 @@ func SetUpEnvironmentVariables() {
CERTIFICATE_PASSWORD = os.Getenv("CERTIFICATE_PASSWORD") // required
PORT = os.Getenv("PORT")
_DEBUG := os.Getenv("DEBUG")
IRALEX_BACKEND_BASE_URL = os.Getenv("IRALEX_BACKEND_BASE_URL") // required
IRALEX_AUTH_EMAIL = os.Getenv("IRALEX_AUTH_EMAIL") // optional
@ -89,7 +91,15 @@ func SetUpEnvironmentVariables() {
}
// Debug-only Required Environment Variables
if config.DEBUG {
if _DEBUG != "" {
DEBUG, err = strconv.ParseBool(_DEBUG)
if err != nil {
fmt.Printf("\033[1;31mError: The environment variable DEBUG is specified but its value is incorrect.\033[0m\n")
mustTerminate = true
}
}
if DEBUG {
if IRALEX_BACKEND_BASE_URL == "" {
fmt.Printf("\033[1;31mError: Missing required environment variable: IRALEX_BACKEND_BASE_URL\033[0m\n")
mustTerminate = true
@ -107,7 +117,7 @@ func SetUpEnvironmentVariables() {
}
// Production-only Required Environment Variables
if !config.DEBUG {
if !DEBUG {
// add more here...
}

View File

@ -247,12 +247,6 @@ func Generate(bill common.IralexBill) UBLInvoiceRoot {
}
totalExcludingTax := amount.Sub(discountAmount)
fmt.Println("Amount", amount)
fmt.Println("Discount", discountAmount)
fmt.Println("Tax Rate", decimal.NewFromInt(int64(bill.FirmSetting.Setting.Tax.Rate)))
fmt.Println("amount - discount", amount.Sub(discountAmount))
fmt.Println("amount - discount * tax rate", amount.Sub(discountAmount).Mul(decimal.NewFromInt(int64(bill.FirmSetting.Setting.Tax.Rate))))
fmt.Println("amount - discount * tax rate / 100", amount.Sub(discountAmount).Mul(decimal.NewFromInt(int64(bill.FirmSetting.Setting.Tax.Rate))).Div(decimal.NewFromInt(100)))
taxedAmount := amount.Sub(discountAmount).Mul(decimal.NewFromInt(int64(bill.FirmSetting.Setting.Tax.Rate))).Div(decimal.NewFromInt(100))
doc.Invoice[0].InvoiceLine[index].ID = []UBLText{{Value: strconv.Itoa(item.ItemNo)}}

View File

@ -15,6 +15,7 @@ import (
"os"
"strconv"
"strings"
"time"
orderedmap "github.com/wk8/go-ordered-map/v2"
"golang.org/x/crypto/pkcs12"
@ -22,15 +23,201 @@ import (
// Signs a document, populting its Signature and UBLExtensions sections. Returns
// the signed document.
func Sign(doc UBLInvoiceRoot) UBLInvoiceRoot {
//
// Pre-conditon:
// 1) The specified document must be transformed, i.e. the `UBLExtensions` and
// `Signature` keys must be removed, if they already exist, which will be
// populated by this function.
// 2) The specified document must have an Invoice[0] already, or you get paniced.
func Sign(doc UBLInvoiceRoot) (UBLInvoiceRoot, error) {
// -------------------------------------------------------------------------
// Minify the transformed document and calculate the document digest.
docJson, _ := json.MarshalIndent(doc, "", " ")
var docJsonMinified = &bytes.Buffer{}
if err := json.Compact(docJsonMinified, docJson); err != nil {
// Unexpected Error - This should never happen
panic(fmt.Sprintf("Couldn't minify the document - %s\n", err.Error()))
}
fmt.Printf("Generated Document:\n%s\n", docJsonMinified)
if core.DEBUG {
core.LogInfo.Printf("INFO: Minified Transformed Document:\n%s\n", docJsonMinified)
}
return doc
hashedMinifiedDoc := sha256.Sum256(docJsonMinified.Bytes())
var docDigest string = b64.StdEncoding.EncodeToString(hashedMinifiedDoc[:])
if core.DEBUG {
core.LogInfo.Printf("INFO: Document Digest:\n%s\n", docDigest)
}
// -------------------------------------------------------------------------
// Open the certificate file and sign the document digest with it.
privateKey, certificate, err := loadPKCS12(core.CERTIFICATE_FILENAME, core.CERTIFICATE_PASSWORD)
if err != nil {
return UBLInvoiceRoot{}, fmt.Errorf("cannot load the certificate file: %w", err)
}
sign, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hashedMinifiedDoc[:])
if err != nil {
return UBLInvoiceRoot{}, fmt.Errorf("cannot sign the doc digest with KCS1v15: %w", err)
}
var cert_b64 string = b64.StdEncoding.EncodeToString(certificate.Raw)
if core.DEBUG {
core.LogInfo.Printf("INFO: Certificate Raw:\n%s\n", cert_b64)
}
var cert_tbs_b64 string = b64.StdEncoding.EncodeToString(certificate.RawTBSCertificate)
if core.DEBUG {
core.LogInfo.Printf("INFO: Certificate RAW TBS:\n%s\n", cert_tbs_b64)
}
var signature string = b64.StdEncoding.EncodeToString(sign)
if core.DEBUG {
core.LogInfo.Printf("INFO: Siganture:\n%s\n", signature)
}
certificate_hash := sha256.Sum256(certificate.RawTBSCertificate)
certDigest := b64.StdEncoding.EncodeToString(certificate_hash[:])
if core.DEBUG {
core.LogInfo.Printf("INFO: Certificate Digest:\n%s\n", certDigest)
}
// -------------------------------------------------------------------------
// Populating the signed properties section, minifying it, and calculating
// its digest,
var subjectName = ""
for _, atv := range certificate.Subject.Names {
// parse the subject name
oid := atv.Type.String()
value := atv.Value.(string)
switch oid {
case "2.5.4.6": // country
subjectName += fmt.Sprint("C=", value, ",")
case "2.5.4.10": // organization name
subjectName += fmt.Sprint("O=", value, ",")
case "2.5.4.3": // common name
subjectName += fmt.Sprint("CN=", value, ",")
case "2.5.4.5": // serial number
subjectName += fmt.Sprint("SERIALNUMBER=", value, ",")
case "2.5.4.97": // organization identifier (tin)
subjectName += fmt.Sprint("OID.2.5.4.97=", value, ",")
case "1.2.840.113549.1.9.1": // email address
subjectName += fmt.Sprint("E=", value, ",")
}
}
var qualifyingProperties = []UBLQualifyingProperties{{
Target: "signature",
SignedProperties: []UBLSignedProperties{{
Id: "id-xades-signed-props",
SignedSignatureProperties: []UBLSignedSignatureProperties{{
SigningTime: []UBLText{{Value: time.Now().UTC().Format(time.RFC3339)}},
SigningCertificate: []UBLSigningCertificate{{
Cert: []UBLCert{{
CertDigest: []UBLCertDigest{{
DigestMethod: []UBLDigestMethod{{
Value: "",
Algorithm: "http://www.w3.org/2001/04/xmlenc#sha256",
}},
DigestValue: []UBLText{{
Value: certDigest,
}},
}},
IssuerSerial: []UBLIssuerSerial{{
X509IssuerName: []UBLText{{Value: certificate.Issuer.String()}},
X509SerialNumber: []UBLText{{Value: certificate.SerialNumber.String()}},
}},
}},
}},
}},
}},
}}
minifiedSignedProps, err := json.Marshal(qualifyingProperties)
if err != nil {
// Unexpected Error - This should never happen
panic(fmt.Sprintf("Couldn't minify signed props - %s\n", err.Error()))
}
if core.DEBUG {
core.LogInfo.Printf("INFO: Minified Signed Properties:\n%s\n", string(minifiedSignedProps))
}
hashedMinifiedSignedProps := sha256.Sum256(minifiedSignedProps)
var signedPropsDigest string = b64.StdEncoding.EncodeToString(hashedMinifiedSignedProps[:])
if core.DEBUG {
core.LogInfo.Printf("INFO: Signed Props Digest:\n%s\n", signedPropsDigest)
}
// -------------------------------------------------------------------------
// Populate the UBLExtensions and Signature sections of the document.
doc.Invoice[0].UBLExtensions = []UBLExtensionsWrapper{{
UBLExtension: []UBLExtension{{
ExtensionURI: []UBLText{{Value: "urn:oasis:names:specification:ubl:dsig:enveloped:xades"}},
ExtensionContent: []UBLExtensionContent{{
UBLDocumentSignatures: []UBLDocumentSignatures{{
SignatureInformation: []UBLSignatureInformation{{
ID: []UBLText{{Value: "urn:oasis:names:specification:ubl:signature:1"}},
ReferencedSignatureID: []UBLText{{Value: "urn:oasis:names:specification:ubl:signature:Invoice"}},
Signature: []UBLSignatureObj{{
Id: "signature",
Object: []UBLSignatureObject{{
QualifyingProperties: qualifyingProperties,
}},
KeyInfo: []UBLKeyInfo{{
X509Data: []UBLX509Data{{
X509Certificate: []UBLText{{Value: cert_b64}},
X509SubjectName: []UBLText{{Value: subjectName[:len(subjectName)-1]}},
X509IssuerSerial: []UBLIssuerSerial{{
X509IssuerName: []UBLText{{Value: certificate.Issuer.String()}},
X509SerialNumber: []UBLText{{Value: certificate.SerialNumber.String()}},
}},
}},
}},
SignatureValue: []UBLText{{Value: signature}},
SignedInfo: []UBLSignedInfo{{
SignatureMethod: []UBLSignatureMethod{{
Value: "",
Algorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
}},
Reference: []UBLReference{
{
Type: "http://uri.etsi.org/01903/v1.3.2#SignedProperties",
URI: "#id-xades-signed-props",
DigestMethod: []UBLDigestMethod{{
Value: "",
Algorithm: "http://www.w3.org/2001/04/xmlenc#sha256",
}},
DigestValue: []UBLText{{Value: signedPropsDigest}},
},
{
Type: "",
URI: "",
DigestMethod: []UBLDigestMethod{{
Value: "",
Algorithm: "http://www.w3.org/2001/04/xmlenc#sha256",
}},
DigestValue: []UBLText{{Value: docDigest}},
},
},
}},
}},
}},
}},
}},
}},
}}
doc.Invoice[0].Signature = []UBLSignature{{
ID: []UBLText{{Value: "urn:oasis:names:specification:ubl:signature:Invoice"}},
SignatureMethod: []UBLText{{Value: "urn:oasis:names:specification:ubl:dsig:enveloped:xades"}},
}}
// -------------------------------------------------------------------------
// Return the signed document.
return doc, nil
}
// Signs the specified document.
@ -51,8 +238,6 @@ func GenerateDocument(doc UBLInvoiceRoot) []byte {
// -------------------------------------------------------------------------
// Transform the document.
// The `UBLExtensions` and `Signature` keys must be removed, if they already
// exist.
// var transformed_doc map[string]interface{}
// _ = json.Unmarshal(doc_string, &transformed_doc)

View File

@ -31,17 +31,17 @@ func SubmitDocument(accessToken string, doc document.UBLInvoiceRoot) {
fmt.Println("Sending Request (/submit-document):\n-----------------------------------")
// Minify and convert the document to base64.
docJson, _ := json.MarshalIndent(doc, "", " ")
var minified_doc = &bytes.Buffer{}
if err := json.Compact(minified_doc, docJson); err != nil {
panic(err)
minified_doc, err := json.Marshal(doc)
if err != nil {
// Unexpected Error - This should never happen
panic(fmt.Sprintf("Couldn't minify signed props - %s\n", err.Error()))
}
var sEnc string = b64.StdEncoding.EncodeToString(minified_doc.Bytes())
var sEnc string = b64.StdEncoding.EncodeToString(minified_doc)
// Create a SHA256 hash of the document.
hash := sha256.New()
hash.Write(minified_doc.Bytes())
hash.Write(minified_doc)
body, err := json.Marshal(requestBody{
Documents: []requestDocument{

View File

@ -151,10 +151,12 @@ const (
// -------------------------------------------------------------------------
// Monetary Calculation Errors
// add here...
// -------------------------------------------------------------------------
// Miscellaneous Errors
_ERR_MISSING_FIRM_NAME = 550
_ERR_MISSING_FIRM_NAME = 33
_ERR_MISSING_FIRM_NAME_NAME = "Missing Firm Name"
_ERR_MISSING_FIRM_NAME_MSG = "Your firm's name is not specified. Please add it from your settings."
)

View File

@ -106,7 +106,16 @@ func HandleSubmitDebug(w http.ResponseWriter, r *http.Request) {
}
doc := document.Generate(bill)
doc = document.Sign(doc)
doc, err = document.Sign(doc)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, http.StatusText(http.StatusInternalServerError))
core.LogInfo.Printf("ERROR: Cannot sign the document - %v", err)
return
}
docJson, _ := json.MarshalIndent(doc, "", " ")
fmt.Println(string(docJson))
return
@ -171,7 +180,13 @@ func HandleSubmit(w http.ResponseWriter, r *http.Request) {
}
doc := document.Generate(bill)
doc = document.Sign(doc)
doc, err = document.Sign(doc)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, http.StatusText(http.StatusInternalServerError))
core.LogInfo.Printf("ERROR: Cannot sign the document - %v", err)
return
}
accessToken := route.AuthenticateFormData(core.CLIENT_ID, core.CLIENT_SECRET_1, core.TIN)
route.SubmitDocument(accessToken, doc)

View File

@ -2,7 +2,6 @@ package server
import (
"fmt"
"iralex-einvoice/config"
"iralex-einvoice/core"
"log"
"net/http"
@ -31,7 +30,7 @@ func Run() {
http.HandleFunc("/ping", HandlePing)
if config.DEBUG {
if core.DEBUG {
http.HandleFunc("/submit", HandleSubmitDebug)
} else {
http.HandleFunc("/submit", HandleSubmit)