Partial implementation of the XML DSig and XAdES standards for Go. Accepts certificates in .p12/.pfx format and generates signatures typically used with UBL invoice documents or similar local standards.
The library supports multiple configuration options. It's possible to specify options such as:
- whether to attach QualifyingProperties element (XAdES) or not (XML DSig but without XAdES)
- what canonicalizers to use
- what hashes to use
- whether to include reference to KeyInfo in SignedInfo (some APIs require it, some don't)
- whether to include the public key value (RSA or ECDSA) in KeyInfo (some APIs require it, some don't)
These options are passed to the library by creating structs of type xmldsig.XMLDSigConfig and xmldsig.XAdESConfig, and passing them to xmldsig.WithXMLDSigConfig and xmldsig.WithXAdES respectively. Settings in these structs will override default settings.
For convenience, there are predefined option builders:
facturae.XMLDSigConfig()andfacturae.XAdESConfig()for Spanish FacturaEksef.XAdESConfig()for Polish KSeF (no need to override XMLDSig defaults)
xmlConfig := xmldsig.XMLDSigConfig{
DataCanonicalizer: dsig.MakeC14N10RecCanonicalizer(), // Canonicalize the XML that is signed
DataHash: crypto.SHA512, // Hash algorithm for the signed XML
SignedInfoCanonicalizer: dsig.MakeC14N10RecCanonicalizer(), // Canonicalization algorithm for SignedInfo
SignedInfoHash: crypto.SHA256, // Hash algorithm for SignedInfo
IncludeKeyValue: false, // Whether to include the public key in KeyInfo
ReferenceKeyInfoInSignedInfo: true, // Whether SignedInfo should reference KeyInfo
KeyInfoCanonicalizer: dsig.MakeC14N10RecCanonicalizer(),
KeyInfoHash: crypto.SHA512,
}
xadesConfig := xmldsig.XAdESConfig{
TimestampFormatter: customTimestampFormatter, // Timestamp formatter for SigningTime
IssuerSerializer: nil, // Serializer for issuer names, nil for default
SignedPropertiesCanonicalizer: dsig.MakeC14N10RecCanonicalizer(),
SignedPropertiesHash: crypto.SHA512,
SigningCertificateHash: crypto.SHA512,
}
signature, err := xmldsig.Sign(data,
xmldsig.WithCertificate(cert),
xmldsig.WithXMLDSigConfig(xmlConfig),
xmldsig.WithXAdES(&xadesConfig),
)Example of a custom timestamp formatter:
func customTimestampFormatter(t time.Time) string {
return t.UTC().Format("2006-01-02T15:04:05.0000000+00:00")
}This example shows how to sign a document using the XAdES standard with Polish KSeF predefined settings. In KSeF, signing an XML is used when logging into the API.
type AuthTokenRequest struct {
XMLName xml.Name `xml:"AuthTokenRequest"`
XMLNamespace string `xml:"xmlns,attr"`
XSI string `xml:"xmlns:xsi,attr"`
XSD string `xml:"xmlns:xsd,attr"`
Challenge string `xml:"Challenge"`
ContextIdentifier *ContextIdentifier `xml:"ContextIdentifier"`
Signature *xmldsig.Signature `xml:"ds:Signature,omitempty"` // Add signature object!
}
func main() {
authTokenRequest := &AuthTokenRequest{
// ... fill in the rest of the fields as needed ...
}
data, _ := xml.Marshal(authTokenRequest)
cert, _ := xmldsig.LoadCertificate("./invopop.p12", "invopop")
authTokenRequest.Signature, _ = xmldsig.Sign(data,
xmldsig.WithCertificate(cert),
xmldsig.WithXAdES(ksef.XAdESConfig()),
)
// Now output the data
out, _ := xml.Marshal(authTokenRequest)
fmt.Println(string(out))
}This example shows how to sign a document using the XAdES standard with Spanish FacturaE predefined settings. Note that this system requires additional configuration parameters to generate additional elements in the signature.
type SampleDoc struct {
XMLName xml.Name `xml:"test:SampleDoc"`
TestNamespace string `xml:"xmlns:test,attr"`
Title string
Signature *xmldsig.Signature `xml:"ds:Signature,omitempty"` // Add signature object!
}
func main() {
doc := &SampleDoc{
TestNamespace: "http://invopop.com/xml/test",
Title: "This is a test",
}
// Using XAdES FacturaE example policy config
facturaeConfig := facturae.XAdESConfig(xmldsig.XAdESConfig{
Role: xmldsig.XAdESSignerRole("third party"),
Description: "test",
Policy: &xmldsig.XAdESPolicyConfig{
URL: "http://www.facturae.es/politica_de_firma_formato_facturae/politica_de_firma_formato_facturae_v3_1.pdf",
Description: "Política de Firma FacturaE v3.1",
Algorithm: "http://www.w3.org/2000/09/xmldsig#sha1",
Hash: "Ohixl6upD6av8N7pEvDABhEL6hM=",
},
})
data, _ := xml.Marshal(doc)
cert, _ := xmldsig.LoadCertificate("./invopop.p12", "invopop")
doc.Signature, _ = xmldsig.Sign(data,
xmldsig.WithCertificate(cert),
xmldsig.WithXMLDSigConfig(facturae.XMLDSigConfig()),
xmldsig.WithXAdES(&facturaeConfig),
)
// Now output the data
out, _ := xml.Marshal(doc)
fmt.Println(string(out))
}Support is also included for using a Time Stamp Authority (TSA). Simply add the following to the Sign options with the URL of the service you want to use:
xmldsig.WithTimestamp(xmldsig.TimestampFreeTSA) // uses https://freetsa.org/tsrUsing this option requires XAdES support to be enabled (by calling WithXAdES), as the timestamp is added to QualifyingProperties > UnsignedProperties > SignatureTimestamp.
Signing and certificates can be overwhelming. OpenSSL is the tool to use for clarifying what the situation is and this page has a useful set of commands: https://www.sslshopper.com/article-most-common-openssl-commands.html
This library requires certificates in PKCS12 DER format (.pfx or .p12 extension). If you don't have something like that, use the OpenSSL tools to convert between X509 (.pem) format and PKCS12.
The order of certificates is important, the main certificate must come first. You can check order using the following command:
openssl pkcs12 -info -in keyStore.p12
It might be a good idea to try exporting and re-creating your existing PKCS12 files if in doubt. First extract to pem:
openssl pkcs12 -in invopop.p12 -out invopop.pem -nodes
Split the resulting .pem file into multiple parts for the key, certificate, and CA certificate(s) using your text editor. Then rebuild:
openssl pkcs12 -export -out invopop.p12 -inkey invopop.key -in invopop.crt -certfile invopop.ca
Before this change, the library was performing canonicalization on the signed data and SignedProperties elements, but was not adding appropriate Transform elements, describing the canonicalization method, to the SignedInfo element.
- As FacturaE-specific options were previously hardcoded and now were moved to API-specific configuration,
xmldsig.WithXAdESnow must be combined withxmldsig.WithXAdES(facturae.XAdESConfig(...)).
This project is developed and maintained under the Apache 2.0 Open Source license by Invopop.
Copyright 2021-2023 Invopop Ltd.