Documentation
¶
Overview ¶
Package xmlenc1 implements W3C XML Encryption 1.1, naming each deliberate departure from the specification and its reason.
Covered: AES block encryption, AES key wrapping, RSA-OAEP key transport, ECDH-ES key agreement with ConcatKDF, same-document ds:RetrievalMethod, and xenc:CipherReference — same-document by default, and external through Decryptor.CipherReferenceResolver.
The one construct xmlenc-core1 marks REQUIRED and this package does not implement is Triple DES: the algorithms #tripledes-cbc and #kw-tripledes are refused deliberately, because Triple DES is a 64-bit block cipher that Sweet32 (CVE-2016-2183) applies to. README.md's Conformance scope section owns the detail.
Index ¶
- Constants
- Variables
- type ConcatKDFParams
- type Decryptor
- func (d Decryptor) AllowUnauthenticatedCBC(v bool) Decryptor
- func (d Decryptor) BlockAlgorithm(uri string) Decryptor
- func (d Decryptor) CipherReferenceResolver(r ReferenceResolver) Decryptor
- func (d Decryptor) Decrypt(ctx context.Context, elem *helium.Element) ([]helium.Node, error)
- func (d Decryptor) DecryptBytes(ctx context.Context, elem *helium.Element) ([]byte, error)
- func (d Decryptor) ECPrivateKey(key *ecdsa.PrivateKey) Decryptor
- func (d Decryptor) KeyEncryptionKey(kek []byte) Decryptor
- func (d Decryptor) MaxCipherValueBytes(n int) Decryptor
- func (d Decryptor) MaxEncryptedKeyBytes(n int) Decryptor
- func (d Decryptor) MaxEncryptedKeys(n int) Decryptor
- func (d Decryptor) PrivateKey(key *rsa.PrivateKey) Decryptor
- func (d Decryptor) SessionKey(key []byte) Decryptor
- func (d Decryptor) StrictPKCS7Padding(v bool) Decryptor
- type Encryptor
- func (e Encryptor) AllowLegacyCBC(v bool) Encryptor
- func (e Encryptor) BlockAlgorithm(uri string) Encryptor
- func (e Encryptor) EncryptBytes(ctx context.Context, doc *helium.Document, plaintext []byte) (*helium.Element, error)
- func (e Encryptor) EncryptContent(ctx context.Context, elem *helium.Element) (*helium.Element, error)
- func (e Encryptor) EncryptElement(ctx context.Context, elem *helium.Element) (*helium.Element, error)
- func (e Encryptor) KeyDerivationParams(params *ConcatKDFParams) Encryptor
- func (e Encryptor) KeyEncryptionKey(kek []byte) Encryptor
- func (e Encryptor) KeyTransportAlgorithm(uri string) Encryptor
- func (e Encryptor) KeyWrapAlgorithm(uri string) Encryptor
- func (e Encryptor) OAEPDigest(uri string) Encryptor
- func (e Encryptor) OAEPMGF(uri string) Encryptor
- func (e Encryptor) OAEPParams(params []byte) Encryptor
- func (e Encryptor) RecipientECPublicKey(key *ecdsa.PublicKey) Encryptor
- func (e Encryptor) RecipientPublicKey(key *rsa.PublicKey) Encryptor
- func (e Encryptor) SessionKey(key []byte) Encryptor
- type KeySizeError
- type ReferenceResolver
- type UnsupportedAlgorithmError
Constants ¶
const ( // NamespaceXMLEnc is the XML Encryption namespace. NamespaceXMLEnc = "http://www.w3.org/2001/04/xmlenc#" // NamespaceXMLEnc11 is the XML Encryption 1.1 namespace. NamespaceXMLEnc11 = "http://www.w3.org/2009/xmlenc11#" // NamespaceDSig is the XML Digital Signatures namespace (for KeyInfo). NamespaceDSig = "http://www.w3.org/2000/09/xmldsig#" // NamespaceDSigMore contains the additional XML Digital Signature // algorithm identifiers used by XML Encryption. NamespaceDSigMore = "http://www.w3.org/2001/04/xmldsig-more#" // NamespaceDSig11 is the XML Digital Signature 1.1 namespace. NamespaceDSig11 = "http://www.w3.org/2009/xmldsig11#" )
const ( AES128CBC = NamespaceXMLEnc + "aes128-cbc" AES256CBC = NamespaceXMLEnc + "aes256-cbc" AES128GCM = NamespaceXMLEnc + "aes128-gcm" AES256GCM = NamespaceXMLEnc + "aes256-gcm" // AES128GCM11 and related constants are XML Encryption 1.1 GCM // identifiers, the only namespace in which a W3C XML Security // specification defines AES-GCM (xmlenc-core1 §5.2). They are distinct // from the two 2001-namespace GCM identifiers above, which no XML // Security specification defines. AES128GCM11 = NamespaceXMLEnc11 + "aes128-gcm" AES192GCM11 = NamespaceXMLEnc11 + "aes192-gcm" AES256GCM11 = NamespaceXMLEnc11 + "aes256-gcm" )
Block encryption algorithm URIs.
const ( RSAOAEP = NamespaceXMLEnc + "rsa-oaep-mgf1p" RSAOAEP11 = NamespaceXMLEnc11 + "rsa-oaep" )
Key transport algorithm URIs.
const ( AES128KeyWrap = NamespaceXMLEnc + "kw-aes128" AES192KeyWrap = NamespaceXMLEnc + "kw-aes192" AES256KeyWrap = NamespaceXMLEnc + "kw-aes256" )
Key wrapping algorithm URIs.
const ( DigestSHA1 = NamespaceDSig + "sha1" // DigestSHA224 is the XMLDSig-more SHA-224 URI. DigestSHA224 = NamespaceDSigMore + "sha224" DigestSHA256 = NamespaceXMLEnc + "sha256" DigestSHA384 = NamespaceXMLEnc + "sha384" // DigestSHA384DSigMore is the XMLDSig-more SHA-384 URI. DigestSHA384DSigMore = NamespaceDSigMore + "sha384" DigestSHA512 = NamespaceXMLEnc + "sha512" )
Digest algorithm URIs (for RSA-OAEP 1.1).
const ( MGFSHA1 = NamespaceXMLEnc11 + "mgf1sha1" MGFSHA224 = NamespaceXMLEnc11 + "mgf1sha224" MGFSHA256 = NamespaceXMLEnc11 + "mgf1sha256" MGFSHA384 = NamespaceXMLEnc11 + "mgf1sha384" MGFSHA512 = NamespaceXMLEnc11 + "mgf1sha512" )
MGF algorithm URIs.
const ( ECDHES = NamespaceXMLEnc11 + "ECDH-ES" ConcatKDF = NamespaceXMLEnc11 + "ConcatKDF" )
Key agreement and key derivation algorithm URIs.
const ( TypeElement = NamespaceXMLEnc + "Element" TypeContent = NamespaceXMLEnc + "Content" )
Encryption type URIs.
const DefaultBlockAlgorithm = AES256GCM11
DefaultBlockAlgorithm is the block encryption algorithm an Encryptor uses when no BlockAlgorithm is set. It is authenticated AES-256-GCM under the XML Encryption 1.1 identifier AES256GCM11, the only namespace in which a W3C XML Security specification defines AES-GCM (xmlenc-core1 §5.2), so the default output is what a conforming peer recognizes.
const DefaultMaxCipherValueBytes = 10 << 20
DefaultMaxCipherValueBytes bounds the decoded EncryptedData CipherValue payload when Decryptor.MaxCipherValueBytes is not set. It matches helium's default maximum size for an individual XML content node and prevents a payload split across CDATA nodes from bypassing that parser-level limit.
const DefaultMaxEncryptedKeyBytes = 64 << 10
DefaultMaxEncryptedKeyBytes bounds the total decoded <EncryptedKey> ciphertext of one EncryptedData when Decryptor.MaxEncryptedKeyBytes is not set, which owns what the budget covers and when it is charged.
64 KiB fits DefaultMaxEncryptedKeys recipients at 512 bytes each — an RSA-4096 wrapped key, the largest in ordinary use — and still leaves 14 KiB spare. Real wrapped keys are usually far smaller: 24 to 40 bytes for AES key wrap, 256 bytes for RSA-2048.
const DefaultMaxEncryptedKeys = 100
DefaultMaxEncryptedKeys bounds how many <EncryptedKey> candidates a Decryptor will trial-decrypt for a single EncryptedData when MaxEncryptedKeys is not set. An unbounded count is a CPU amplification (DoS) vector. Decryptor.MaxEncryptedKeys documents the per-candidate cost this bounds. The default mirrors jwx's WithMaxRecipients (100), which is generous for real multi-recipient documents yet caps amplification.
const ( // TypeEncryptedKey is the Type a ds:RetrievalMethod states to link to the // xenc:EncryptedKey holding the key needed to decrypt the CipherData of // the EncryptedData or EncryptedKey whose ds:KeyInfo carries it. A // RetrievalMethod stating it must name an xenc:EncryptedKey; naming // anything else is refused as ErrMalformedEncrypted. TypeEncryptedKey = NamespaceXMLEnc + "EncryptedKey" )
Type URIs a ds:RetrievalMethod inside a ds:KeyInfo may declare (xmlenc-core1 §3.5.3).
Variables ¶
var ( // ErrDecryptionFailed is returned when decryption fails. ErrDecryptionFailed = errors.New("xmlenc1: decryption failed") // ErrEncryptionFailed is returned when encryption fails. ErrEncryptionFailed = errors.New("xmlenc1: encryption failed") // ErrMissingKey is returned when no decryption key is available. ErrMissingKey = errors.New("xmlenc1: no decryption key available") // ErrUnsupportedKeyDerivation is returned when an EncryptedData offers its // session key ONLY through an xenc11:DerivedKey (xmlenc-core1 §3.5.2), // which tells the recipient to derive the content key from master key // material it already holds. This package implements no key derivation from // a master key, so it cannot decrypt such a document. // // It is deliberately NOT ErrMissingKey. That sentinel says no decryption key // is available, which sends a caller to audit the keys it configured — and no // key it configures can help, because the document asked for a facility this // package does not have. A caller must be able to tell "I set this up wrong" // from "helium cannot read this document at all". Match with errors.Is. // // It covers both ways a ds:KeyInfo offers the construct: an xenc11:DerivedKey // carried inline, and a ds:RetrievalMethod whose Type names one. It is raised // only when derivation was the ONLY option: an EncryptedData that ALSO // carries a usable xenc:EncryptedKey decrypts under that key, since the // unimplemented construct costs a document nothing it could otherwise do. // // A pre-shared [Decryptor.SessionKey] never reaches it. That early return // precedes key resolution entirely, so a caller holding the session key // decrypts a document whose ds:KeyInfo this package cannot read. ErrUnsupportedKeyDerivation = errors.New("xmlenc1: key derivation from master key material is not supported") // ErrTooManyEncryptedKeys is returned when an EncryptedData carries more // EncryptedKey candidates than the Decryptor's effective limit, which // guards against CPU amplification (DoS). Decryptor.MaxEncryptedKeys owns // the cap: the per-candidate cost it bounds and the effective-limit // rules. See also DefaultMaxEncryptedKeys. ErrTooManyEncryptedKeys = errors.New("xmlenc1: too many EncryptedKey candidates") // ErrEncryptedKeyBytesExceeded is returned when the EncryptedKey // candidates of one EncryptedData carry more ciphertext together than // the Decryptor's effective byte budget, which guards against memory // amplification (DoS). Decryptor.MaxEncryptedKeyBytes owns the budget: // what it covers, when it is charged, and the effective-limit rules. // See also DefaultMaxEncryptedKeyBytes. // // It is distinct from ErrTooManyEncryptedKeys because the two bound // different things and are raised or lifted by different setters: too // many candidates is a count, too much ciphertext is a size, and a // caller handling one must be able to tell which limit to raise. ErrEncryptedKeyBytesExceeded = errors.New("xmlenc1: EncryptedKey ciphertext exceeds the byte budget") // ErrCipherValueBytesExceeded is returned when an EncryptedData payload // CipherValue exceeds the Decryptor's effective byte budget. The payload // may arrive as many text or CDATA nodes, so this limit is separate from // the parser's per-node content limit. Decryptor.MaxCipherValueBytes owns // the budget, including its effective-limit rules. See also // DefaultMaxCipherValueBytes. ErrCipherValueBytesExceeded = errors.New("xmlenc1: EncryptedData CipherValue exceeds the byte budget") // ErrReferenceNotFound is returned when a ds:RetrievalMethod or an // xenc:CipherReference names something this package will not resolve: a // same-document reference matching no element in the document the // EncryptedData belongs to, or a URI that is not a same-document reference // at all and cannot be dereferenced. The shipped FSReferenceResolver wraps // it for every URI shape it refuses and for a resource it cannot read. // // The two constructs differ in whether an external URI can ever be // resolved: // // - a ds:RetrievalMethod naming another resource is refused whatever it // names, with no setting that lifts the refusal. Only the // same-document form is REQUIRED (xmlenc-core1 §3.5), no external form // is mandated anywhere, and an external key location decides which key // material the recipient trial-decrypts — not a decision a document // gets to make for a caller. // - an xenc:CipherReference naming another resource is refused until the // caller supplies a Decryptor.CipherReferenceResolver. §3.3.1 imports // XMLDSig's dereferencing model, which makes the same-document forms // MUSTs and HTTP dereferencing RECOMMENDED, so the external form is an // opt-in capability, and no obligation. // // The reference is resolved while the document is read, so this precedes // the Decryptor.SessionKey early return: a pre-shared session key does not // decrypt past a reference that was refused. A ds:RetrievalMethod whose // Type this package does not implement is never resolved at all and so // never reaches this error. Match with errors.Is. ErrReferenceNotFound = errors.New("xmlenc1: reference not found") // ErrAmbiguousReference is returned when a same-document // ds:RetrievalMethod or xenc:CipherReference URI matches more than one // element. // // This is XML Signature Wrapping applied to encryption. An attacker who // can inject an element carrying an Id already in use would otherwise // choose which of the two the recipient resolves, and so which key it // unwraps and trial-decrypts with, or which octets it takes as the cipher // text. Resolution therefore collects every match and refuses on more than // one, taking neither. Match with errors.Is. ErrAmbiguousReference = errors.New("xmlenc1: ambiguous reference") // ErrKeyUnwrapFailed is returned when AES key unwrap integrity check // fails. It is always wrapped in ErrDecryptionFailed, so a caller that // tests only for ErrDecryptionFailed catches a failed key unwrap the // same way it catches a failed RSA key transport. ErrKeyUnwrapFailed = errors.New("xmlenc1: AES key unwrap integrity check failed") // ErrMalformedEncrypted is returned when an EncryptedData element is malformed. ErrMalformedEncrypted = errors.New("xmlenc1: malformed EncryptedData element") // ErrOpaquePayload is returned by Decryptor.Decrypt when the // EncryptedData's @Type does not declare XML content: it is absent, empty, // or a URI other than TypeElement and TypeContent. Decrypt is the XML // path, so it refuses such a payload, and every message wrapping this // sentinel names Decryptor.DecryptBytes, which returns the plaintext // octets without parsing them. // // @Type sits OUTSIDE the ciphertext and is authenticated by nothing, not // even AES-GCM. Treating an absent or unrecognized value as TypeElement // would let anyone who can edit the document delete the attribute and have // an opaque octet stream parsed as XML and handed back as nodes to graft // into a tree — type confusion decided by an attribute the recipient // cannot verify. xmlenc-core1 §4.2 asks a decryptor to take an unknown or // empty Type as a signal that the cleartext is an opaque octet stream, and // §3.1 puts the absent case in the same position. // // It is deliberately NOT ErrMalformedEncrypted: such a document is // well-formed, and a caller must be able to tell "this payload is opaque, // retry with DecryptBytes" from a document it should reject outright. // DecryptBytes never returns it — it does not read @Type at all. Match // with errors.Is. ErrOpaquePayload = errors.New("xmlenc1: EncryptedData payload is not XML") // ErrMissingConfig is returned when required encryption config is missing. ErrMissingConfig = errors.New("xmlenc1: missing required configuration") // ErrConflictingKeyConfig is returned when an Encryptor configures two of // the three ways to protect the session key: RSA key transport // (KeyTransportAlgorithm + RecipientPublicKey), ECDH-ES key agreement // (KeyWrapAlgorithm + RecipientECPublicKey), and AES key wrapping // (KeyWrapAlgorithm + KeyEncryptionKey). Any pair of them fails with this // error, naming both of the configured things so the caller knows which // two to choose between. A SessionKey alongside a single mechanism is // not a pair: it supplies the key that mechanism protects. // // An EncryptedData carries a single EncryptedKey here, so honoring one // mechanism means silently discarding the other — and a recipient // holding only the discarded key then fails to decrypt with an error // that points nowhere near the real mistake. The caller must pick one. ErrConflictingKeyConfig = errors.New("xmlenc1: conflicting key protection configured") // ErrConflictingBlockAlgorithm is returned when an EncryptedData declares a // block algorithm in its EncryptionMethod and the Decryptor was given a // different one through Decryptor.BlockAlgorithm. The message names both // URIs so the caller knows which of the two to change. // // The two are matched on purpose, and never ordered. Decryptor.BlockAlgorithm // exists for an EncryptedData that carries no EncryptionMethod at all, where // the algorithm is known out of band (W3C xmlenc-core1 §3.1, §4.4); letting a // document's declaration win over a caller who stated the algorithm would let // the document choose the cipher the recipient runs, which is exactly the // algorithm confusion the setter must not introduce. Under a strict match, // setting it can only narrow what a decrypt accepts. ErrConflictingBlockAlgorithm = errors.New("xmlenc1: conflicting block algorithm") // ErrCBCRequiresOptIn is returned when a Decryptor is asked to // decrypt an AES-CBC ciphertext but the caller has not opted in // to unauthenticated CBC via Decryptor.AllowUnauthenticatedCBC(true). // // AES-CBC under XML Encryption 1.0 is unauthenticated and is // vulnerable to padding-oracle attacks (Jager/Somorovsky 2011). // XML Encryption 1.1 deprecated CBC in favor of AES-GCM. Callers // that must interoperate with legacy CBC ciphertexts can opt in // after evaluating the attack surface (e.g. ensuring decryption // errors are not exposed to remote attackers). ErrCBCRequiresOptIn = errors.New("xmlenc1: AES-CBC decryption requires AllowUnauthenticatedCBC(true)") // ErrCBCEncryptionRequiresOptIn is returned when an Encryptor is // configured to emit a new AES-CBC ciphertext (via a CBC // BlockAlgorithm) but the caller has not opted in to legacy CBC // encryption via Encryptor.AllowLegacyCBC(true). // // The Encryptor defaults to AES-256-GCM (authenticated). AES-CBC // under XML Encryption 1.0 is unauthenticated and vulnerable to // padding-oracle attacks (Jager/Somorovsky 2011); XML Encryption // 1.1 deprecated it in favor of AES-GCM. Emitting new CBC // ciphertext therefore requires an explicit acknowledgement. ErrCBCEncryptionRequiresOptIn = errors.New("xmlenc1: AES-CBC encryption requires AllowLegacyCBC(true)") )
Functions ¶
This section is empty.
Types ¶
type ConcatKDFParams ¶ added in v0.8.0
type ConcatKDFParams struct {
// AlgorithmID, PartyUInfo, PartyVInfo, SuppPubInfo, and SuppPrivInfo
// are the NIST SP 800-56A OtherInfo fields, decoded from the hexBinary
// attributes of the same names. They are concatenated, in this order,
// into the KDF input, so both parties must agree on them exactly.
//
// The five fields TOGETHER are limited to 4096 bytes, since the document
// under decryption is attacker-supplied and the concatenation costs work
// proportional to its size. Real OtherInfo is identifiers and nonces —
// tens of bytes — so the limit is far above any interoperable value.
// Exceeding it is an error wrapping [ErrMalformedEncrypted], raised when
// parsing a document and when deriving from these parameters.
//
// One parameter set never reaches a derivation and so is never measured:
// the fallback [Encryptor.KeyDerivationParams] documents replaces a set
// whose DigestMethod is empty, wholesale, with the SHA-256 default
// carrying empty OtherInfo. These five fields are discarded there rather
// than checked, so an oversized set paired with an empty DigestMethod
// encrypts successfully and emits no OtherInfo attributes at all.
AlgorithmID []byte
PartyUInfo []byte
PartyVInfo []byte
SuppPubInfo []byte
SuppPrivInfo []byte
// DigestMethod is the hash driving the KDF, taken from the @Algorithm
// of the ds:DigestMethod child. Parsed wire parameters must carry one:
// a xenc11:ConcatKDFParams without it is rejected as malformed. On an
// Encryptor these parameters are configuration, and no wire data,
// and [Encryptor.KeyDerivationParams] states what an empty DigestMethod
// means there.
DigestMethod string
// contains filtered or unexported fields
}
ConcatKDFParams contains the XML Encryption 1.1 ConcatKDF parameters. The parameter attributes are decoded from their hexBinary representation; their unused-bit counts are retained internally for KDF bit-string packing.
type Decryptor ¶
type Decryptor struct {
// contains filtered or unexported fields
}
Decryptor decrypts XML EncryptedData elements. It uses clone-on-write semantics.
func (Decryptor) AllowUnauthenticatedCBC ¶
AllowUnauthenticatedCBC opts the Decryptor in to decrypting AES-CBC ciphertexts. AES-CBC under XML Encryption 1.0 is unauthenticated and vulnerable to padding-oracle attacks (Jager/Somorovsky 2011); XML Encryption 1.1 deprecated CBC in favor of AES-GCM.
By default the Decryptor refuses CBC and returns ErrCBCRequiresOptIn. Set this to true only if you must accept legacy CBC ciphertexts AND you have verified that decryption errors are not exposed to remote attackers (e.g. by surfacing the same generic error for every failure path and never timing-sidechannel distinguishing them).
func (Decryptor) BlockAlgorithm ¶ added in v0.8.0
BlockAlgorithm supplies the block encryption algorithm URI out of band, for an EncryptedData that carries no EncryptionMethod. W3C xmlenc-core1 §3.1 and §3.2 leave that element optional and state that the recipient must then already know the algorithm, and §4.4 step 1 admits obtaining the algorithm information out of band. Support for such a document is therefore opt-in: without this setter one fails with ErrMalformedEncrypted, because nothing says what to decrypt it with.
An empty URI counts as not set. The match against the document is STRICT, so setting this can only narrow what a decrypt accepts, never widen it:
- EncryptionMethod absent and this unset: ErrMalformedEncrypted, naming this setter.
- EncryptionMethod absent and this set: the URI set here is used.
- EncryptionMethod present and this unset: the document's URI is used.
- Both present and different: ErrConflictingBlockAlgorithm, naming both.
A document can never override a caller who stated the algorithm out of band; that sentinel's godoc owns why. Whichever of the two the resolution returns is the algorithm every later step is bound to, exactly as a wire-declared one is: the AES-CBC opt-in gate (see Decryptor.AllowUnauthenticatedCBC), the additional authenticated data of the two 2001-namespace GCM identifiers, the session-key length (KeySizeError), and the length a valid AES key-wrap ciphertext must have.
func (Decryptor) CipherReferenceResolver ¶ added in v0.8.0
func (d Decryptor) CipherReferenceResolver(r ReferenceResolver) Decryptor
CipherReferenceResolver supplies the octets of an xenc:CipherReference whose URI is NOT one of the four same-document forms, i.e. one naming a resource outside the document being decrypted. ReferenceResolver owns what a resolver is asked for and what the shipped FSReferenceResolver refuses.
Nil is the default, and it is a deny, and no gap: an external URI then fails closed with ErrReferenceNotFound, and no document can lift that by itself. The same-document forms need no I/O and never reach a resolver, so setting one changes nothing about how they resolve — it only adds the external form. That split follows the specification: W3C xmlenc-core1 §3.3.1 imports XMLDSig's dereferencing model, in which the same-document forms are normative MUSTs (xmldsig-core1 §4.4.3.2, §4.4.3.3) while HTTP dereferencing is RECOMMENDED (§4.4.3.1).
A resolved resource is charged against the same budget its CipherData would have been: Decryptor.MaxCipherValueBytes for an EncryptedData payload and Decryptor.MaxEncryptedKeyBytes for an EncryptedKey. A resolver returns a stream and this package reads it, so the bound holds for a resolver a caller writes and not merely for the one shipped here: the read stops one byte past what the budget still allows, it stops when ctx is done, and the stream is closed on every one of those paths. What the package cannot constrain is what happens INSIDE a resolver — one that buffers a whole resource itself, or blocks before returning a stream at all, does so on the caller's own account. ReferenceResolver states that division and why it falls there.
func (Decryptor) Decrypt ¶
Decrypt decrypts an EncryptedData element and returns the decrypted nodes.
Unlike Encryptor.EncryptElement and Encryptor.EncryptContent, which splice EncryptedData into the tree, Decrypt does NOT modify the document: elem stays exactly where it is and the returned nodes are detached. Restoring the original document is the caller's decision — call elem.Replace with the single node for a TypeElement payload, or remove elem and insert the nodes at its position for TypeContent.
The plaintext is parsed with DTD loading, external entity resolution, and network access disabled, in the in-scope-namespace context of elem's parent, so prefixes declared only on an ancestor resolve correctly.
Only the two @Type values that declare XML content are parsed: a TypeContent payload yields its children, and a TypeElement payload must yield exactly one element node. Every other @Type — absent, empty, or an unrecognized URI — marks an opaque octet stream and is refused with ErrOpaquePayload, which explains why an unauthenticated attribute never selects the XML path. Use DecryptBytes for such a payload; it returns the plaintext octets without parsing them.
func (Decryptor) DecryptBytes ¶ added in v0.8.0
DecryptBytes decrypts an EncryptedData element and returns its plaintext octets without parsing them as XML. It does not interpret @Type, so use it for opaque or application-defined binary payloads, including values with no Type attribute.
func (Decryptor) ECPrivateKey ¶ added in v0.8.0
func (d Decryptor) ECPrivateKey(key *ecdsa.PrivateKey) Decryptor
ECPrivateKey sets the elliptic-curve private key used for XML Encryption 1.1 ECDH-ES key agreement.
func (Decryptor) KeyEncryptionKey ¶
KeyEncryptionKey sets the key for AES key unwrapping.
func (Decryptor) MaxCipherValueBytes ¶ added in v0.8.0
MaxCipherValueBytes caps the decoded EncryptedData CipherValue payload, in bytes, that decrypting one EncryptedData will hold. This is independent of MaxEncryptedKeyBytes, which bounds only the wrapped session-key candidates.
The budget is charged while the document is read, before the payload CipherValue is assembled or decoded. It measures decoded octets, so XML whitespace and splitting across text or CDATA nodes cannot bypass it.
Zero (the default) uses DefaultMaxCipherValueBytes; a negative value removes the limit. A document over the effective budget fails with ErrCipherValueBytesExceeded before block decryption or plaintext parsing.
func (Decryptor) MaxEncryptedKeyBytes ¶ added in v0.8.0
MaxEncryptedKeyBytes caps the total decoded <EncryptedKey> ciphertext, in bytes, that decrypting one EncryptedData will hold. Decryptor.MaxEncryptedKeys bounds how many candidates a document may carry; this bounds how large they may be together, which that count alone does not.
The budget is charged while the document is read, before each retained candidate's CipherValue is assembled or decoded. An excess candidate is rejected by MaxEncryptedKeys before this budget or its structure is read. A CipherValue the base64 decoder would reject is charged what that rejected decode costs, so malformed ciphertext cannot buy work the budget was set to deny. Only <EncryptedKey> ciphertext counts. The EncryptedData payload is charged separately by MaxCipherValueBytes.
What the budget bounds is memory held for a candidate, not the length of the text it was written as. A CipherValue may carry XML whitespace between its characters and may be spread over any number of text and CDATA nodes, none of which changes the bytes it decodes to; that text is counted where it lies and never gathered into a value of its own, so an unbounded amount of it costs nothing beyond reading it.
Zero (the default) uses DefaultMaxEncryptedKeyBytes; a negative value removes the limit (matching helium's MaxDepth convention). A document over the effective budget fails with ErrEncryptedKeyBytesExceeded, in every key configuration: the budget is charged during parsing, so it holds ahead of both the candidate loop and the Decryptor.SessionKey early return.
func (Decryptor) MaxEncryptedKeys ¶ added in v0.4.0
MaxEncryptedKeys caps the number of <EncryptedKey> candidates the Decryptor will trial-decrypt for a single EncryptedData. A document packed with junk EncryptedKey elements is a CPU amplification (DoS) vector, so the cap is enforced while parsing before an excess candidate is parsed or retained.
A candidate's branch — which key it uses and what it costs — is dispatched on its AgreementMethod first and on its declared algorithm second. An EncryptedKey carrying an AgreementMethod takes the key-agreement branch and uses Decryptor.ECPrivateKey; its declared algorithm is the AES key-wrap URI applied to the agreed key and does not choose the branch. Only a supported ECDH-ES agreement URI then reaches the full cost of a key agreement, a ConcatKDF derivation, and an AES key unwrap; any other agreement URI is rejected before all three. Without an AgreementMethod the declared algorithm decides: an RSA-OAEP URI uses Decryptor.PrivateKey and costs a private-key decrypt, an AES key-wrap URI uses Decryptor.KeyEncryptionKey and costs a plain key unwrap. A candidate whose branch needs a key the caller never configured costs no crypto at all and yields ErrMissingKey.
Zero (the default) uses DefaultMaxEncryptedKeys; a negative value removes the limit (matching helium's MaxDepth convention). A document exceeding the effective cap fails with ErrTooManyEncryptedKeys, in every key configuration: the cap is applied while parsing the candidate list, before the Decryptor.SessionKey early return.
func (Decryptor) PrivateKey ¶
func (d Decryptor) PrivateKey(key *rsa.PrivateKey) Decryptor
PrivateKey sets the RSA private key for key transport decryption.
PrivateKey, ECPrivateKey, and KeyEncryptionKey may all be set at once, so a single Decryptor handles documents protected different ways. Decryptor.MaxEncryptedKeys states which of the three an EncryptedKey candidate uses. A non-empty Decryptor.SessionKey makes all three inert.
func (Decryptor) SessionKey ¶
SessionKey sets a pre-shared session key directly. As on the Encryptor, an empty or nil key counts as not set, and decryption falls back to the EncryptedKey candidates.
A non-empty key is not a preference among keys; it is an early return. Decrypt and DecryptBytes take it as the session key and return before candidate selection, per-candidate validation, and per-candidate key resolution, none of which runs. The Decryptor.MaxEncryptedKeys cap is applied ahead of that return and still holds. Every consequence follows from that one fact:
- PrivateKey, ECPrivateKey, and KeyEncryptionKey have no effect.
- An EncryptedKey that only candidate selection would reject — a missing EncryptionMethod, an unsupported algorithm URI, an algorithm whose key the caller never configured — does not fail the decrypt.
Set it only when the session key is known out of band.
func (Decryptor) StrictPKCS7Padding ¶ added in v0.8.0
StrictPKCS7Padding narrows which AES-CBC padding a decrypt accepts from the XML Encryption rule to the PKCS#7 one. It is off by default and it has no effect on AES-GCM, which does not pad.
W3C xmlenc-core1 §5.2.1 pads a plaintext short by N octets with N-1 octets of ARBITRARY value and a final octet N, so the only thing a conforming decryptor may read is that final octet. PKCS#7 (RFC 5652 §6.3) additionally fixes every one of those octets to N. PKCS#7 padding is therefore always valid XML Encryption padding, and the reverse does not hold: a peer that fills the leading octets with anything else — random bytes above all — writes a perfectly conforming ciphertext that this option refuses. Turning it on can only narrow what a decrypt accepts, and what it excludes is conforming documents, so leave it off for interoperability.
It exists for a caller who controls both ends and wants the tighter check on what it accepts back. That check is worth something small and specific: under the XML Encryption rule any final octet from 1 to the block size is acceptable, so roughly one in sixteen random forgeries survives unpadding, while under PKCS#7 roughly one in 2^(8N) does. Both numbers describe an unauthenticated mode. Neither closes the padding oracle, whose signal is the success or failure of the decrypt as a whole, and Decryptor.AllowUnauthenticatedCBC states the rest of that reasoning. A caller who wants the ciphertext authenticated wants AES-GCM instead.
Encryptor always writes PKCS#7-shaped padding, which both rules accept, so this option never rejects a document this package produced.
type Encryptor ¶
type Encryptor struct {
// contains filtered or unexported fields
}
Encryptor encrypts an XML element, an element's content, or arbitrary octets — one terminal method each. It uses clone-on-write semantics: each builder method returns a new Encryptor and the original is never mutated.
func NewEncryptor ¶
func NewEncryptor() Encryptor
NewEncryptor creates a new Encryptor with default settings.
func (Encryptor) AllowLegacyCBC ¶ added in v0.3.0
AllowLegacyCBC opts the Encryptor in to emitting unauthenticated AES-CBC ciphertext when a CBC BlockAlgorithm is selected.
The Encryptor defaults to authenticated AES-GCM. AES-CBC under XML Encryption 1.0 is unauthenticated and vulnerable to padding-oracle attacks (Jager/Somorovsky 2011); XML Encryption 1.1 deprecated it in favor of AES-GCM. Set this to true only when you must produce ciphertext for a legacy recipient that cannot accept AES-GCM. This does not affect decryption (see Decryptor.AllowUnauthenticatedCBC).
func (Encryptor) BlockAlgorithm ¶
BlockAlgorithm sets the block encryption algorithm URI. If never set, the Encryptor defaults to DefaultBlockAlgorithm (authenticated AES-256-GCM).
Selecting an AES-CBC algorithm (AES128CBC / AES256CBC) additionally requires AllowLegacyCBC(true): CBC under XML Encryption 1.0 is unauthenticated and padding-oracle-prone, so emitting new CBC ciphertext is gated behind an explicit opt-in. Without it, encryption returns ErrCBCEncryptionRequiresOptIn.
func (Encryptor) EncryptBytes ¶ added in v0.8.0
func (e Encryptor) EncryptBytes(ctx context.Context, doc *helium.Document, plaintext []byte) (*helium.Element, error)
EncryptBytes encrypts arbitrary octets and returns a detached EncryptedData element owned by doc. It is the counterpart of Decryptor.DecryptBytes: together they cover the payloads that are not an XML element or element content.
The returned element carries no Type attribute, which is what xmlenc-core1 §3.1 asks of a plaintext that is neither an element nor element content. Recover this payload with DecryptBytes, which returns the plaintext octets without parsing them as XML; Decrypt refuses it with ErrOpaquePayload, parsing no octets whose Type never declared XML. No tree is modified — the caller decides where to insert the element.
func (Encryptor) EncryptContent ¶
func (e Encryptor) EncryptContent(ctx context.Context, elem *helium.Element) (*helium.Element, error)
EncryptContent encrypts the content of an element, replacing the children with an EncryptedData element. Returns the EncryptedData element.
This mutates the document: every child of elem is unlinked and the EncryptedData becomes its only child. elem itself stays in place.
func (Encryptor) EncryptElement ¶
func (e Encryptor) EncryptElement(ctx context.Context, elem *helium.Element) (*helium.Element, error)
EncryptElement encrypts an entire element, replacing it in the tree with an EncryptedData element. Returns the EncryptedData element.
This mutates the document: elem is unlinked from its position and the EncryptedData takes its place among the siblings. Decryptor.Decrypt is not the mirror image — it leaves the tree alone and returns the nodes.
func (Encryptor) KeyDerivationParams ¶ added in v0.8.0
func (e Encryptor) KeyDerivationParams(params *ConcatKDFParams) Encryptor
KeyDerivationParams sets the ConcatKDF parameters used by ECDH-ES key agreement. It has no effect without RecipientECPublicKey.
The five OtherInfo fields are concatenated into the KDF input exactly as given, so the recipient must derive with identical values; they travel on the wire in the emitted xenc11:ConcatKDFParams. A nil params, or one with an empty DigestMethod, falls back to SHA-256 with empty OtherInfo. ConcatKDFParams states the size limit the five OtherInfo fields share. An encryption whose params name a DigestMethod and exceed that limit fails, emitting no document a hardened recipient would refuse. Params with an empty DigestMethod take the fallback above instead: their OtherInfo is discarded before any derivation, so it is never measured against the limit and never reaches the wire.
The parameters are copied, byte slices included, so mutating the caller's arrays afterwards cannot change what a later encryption derives or emits.
func (Encryptor) KeyEncryptionKey ¶
KeyEncryptionKey sets the key encryption key for AES key wrapping. Together with KeyWrapAlgorithm it selects that mechanism, one of the mechanisms that protect the session key; ErrConflictingKeyConfig states how many of them an Encryptor may configure.
func (Encryptor) KeyTransportAlgorithm ¶
KeyTransportAlgorithm sets the key transport algorithm URI. Together with RecipientPublicKey it selects RSA key transport, one of the mechanisms that protect the session key; ErrConflictingKeyConfig states how many of them an Encryptor may configure.
func (Encryptor) KeyWrapAlgorithm ¶
KeyWrapAlgorithm sets the AES Key Wrap algorithm URI. It names the wrap itself, not which mechanism performs it: with KeyEncryptionKey it selects AES key wrapping under the supplied key, and with RecipientECPublicKey it is the wrap applied to the key ECDH-ES derives. Those are two different mechanisms, and ErrConflictingKeyConfig states how many of them an Encryptor may configure.
func (Encryptor) OAEPDigest ¶
OAEPDigest sets the digest algorithm for RSA-OAEP 1.1.
func (Encryptor) OAEPParams ¶
OAEPParams sets the RSA-OAEP label. A label over the 1 KiB limit [encryptionMethod.OAEPParams] documents fails the encryption with ErrEncryptionFailed, and only when key transport is the mechanism in use, since that is the only one that writes a label.
func (Encryptor) RecipientECPublicKey ¶ added in v0.8.0
RecipientECPublicKey sets the recipient's elliptic-curve public key and selects XML Encryption 1.1 ECDH-ES key agreement. It is the encrypt-side counterpart of Decryptor.ECPrivateKey, and supports P-256, P-384, and P-521.
ECDH-ES derives the key-encryption key, and takes none, so KeyWrapAlgorithm still selects the AES Key Wrap variant applied to the session key, while KeyEncryptionKey belongs to the separate AES key wrapping mechanism; ErrConflictingKeyConfig states how many mechanisms an Encryptor may configure. Each encryption generates a fresh ephemeral key pair, and the EncryptedKey carries its public half in an xenc:AgreementMethod.
The key derivation is ConcatKDF; Encryptor.KeyDerivationParams controls it.
func (Encryptor) RecipientPublicKey ¶
RecipientPublicKey sets the recipient's RSA public key for key transport.
func (Encryptor) SessionKey ¶
SessionKey sets a pre-existing session key. An empty or nil key counts as not set: a random key of the length the block algorithm requires is generated per encryption, and that is also what an Encryptor that never calls SessionKey does.
The key is still protected by whichever mechanism is configured (key transport or key wrapping); supplying it does not skip that step. A non-empty key's length must match the block algorithm exactly, or encryption fails with a KeySizeError, and never silently encrypts at a weaker strength than the emitted @Algorithm claims. An empty or nil key never reaches that check, because the generated key is used instead.
A non-empty key with no protection mechanism configured emits no EncryptedKey, and the recipient must already hold this key. An empty or nil key with no protection mechanism configured leaves nothing configured at all, so encryption fails with ErrMissingConfig.
type KeySizeError ¶ added in v0.2.0
type KeySizeError struct {
// Key names the key that was the wrong length, e.g. "session key" or
// "key-encryption key". It is diagnostic text and may be empty when
// the role is not known at the point of failure.
Key string
Algorithm string
Want int
Got int
// contains filtered or unexported fields
}
KeySizeError is returned when a key (session key or key-encryption key) does not match the exact length required by its declared algorithm URI. It guards against algorithm/key-size confusion, e.g. declaring AES-256 on the wire while supplying a 16-byte key that crypto/aes would silently treat as AES-128.
Construct it with keyed fields, as in &KeySizeError{Algorithm: uri, Want: want, Got: got}; see UnsupportedAlgorithmError for why an unkeyed literal cannot compile here.
func (*KeySizeError) Error ¶ added in v0.2.0
func (e *KeySizeError) Error() string
type ReferenceResolver ¶ added in v0.8.0
type ReferenceResolver interface {
ResolveReference(ctx context.Context, uri string) (io.ReadCloser, error)
}
ReferenceResolver supplies the octet stream an xenc:CipherReference names when its URI is NOT one of the four same-document forms (Decryptor.Decrypt states those forms). It is the opt-in seam for decrypting an EncryptedData whose cipher text lives outside the document carrying it.
A resolver is consulted ONLY for a non-same-document (external) URI. Same-document references need no I/O and never reach it. When no resolver is configured an external reference stays fail-closed with ErrReferenceNotFound, which is the default.
That default is what the specification permits, and no shortfall of it. W3C xmlenc-core1 §3.3.1 requires "the same URI encoding, dereferencing, scheme, and HTTP response codes as that of [XMLDSIG-CORE1]" and defines no dereferencing of its own; xmldsig-core1 §4.4.3.1 makes dereferencing URIs in the HTTP scheme RECOMMENDED, while §4.4.3.2 and §4.4.3.3 make the null URI, the shortname XPointer, and same-document dereferencing normative MUSTs. So the imported obligation covers URI="" and URI="#id", which this package implements unconditionally, and external fetching is a RECOMMENDED capability the caller opts into.
The address space ¶
uri is ALWAYS the JOINED, DOCUMENT-SPACE URI: the @URI the document wrote, resolved against the base URI in force at the xenc:CipherReference element itself (the document's own URL, narrowed by every xml:base on the way down to that element). A resolver therefore never sees the raw attribute and never performs the join. A document parsed with helium.Parser.ParseFile carries the file's absolute path as its URL, so a relative @URI arrives here as an absolute path in that document's space — which is why a filesystem resolver has to be told which prefix of that space its fs.FS stands for. See FSReferenceResolver.
Who bounds and who cancels ¶
The returned stream is the resource's raw bytes: an external reference yields an octet stream, so no canonicalization applies to it and only a declared ds:Transform changes what the bytes mean.
This package reads that stream itself, under the decrypt's remaining CipherValue allowance (Decryptor.MaxCipherValueBytes for a payload reference, Decryptor.MaxEncryptedKeyBytes for a key reference) and under the caller's context. It reads one byte past what the allowance still admits and no further, so it never materializes an oversized resource; it abandons the read the moment ctx is done, and it CLOSES the stream on every path — completion, over-budget, and cancellation alike, and equally when ResolveReference itself returns a stream alongside an error. A resolver hands over a stream and owes it nothing more.
Be precise about what that does and does not buy. It removes THIS PACKAGE's complicity: whatever a URI names, the package holds no more than the budget admits and its own reading stops when the caller says stop. It does NOT stop a resolver from buffering a whole resource internally, or from blocking inside ResolveReference before it returns a stream at all — nothing outside a resolver can constrain what happens inside it. That division is the right one because the two sides are trusted differently: the resolver is code the caller chose, while the URI is chosen by an unauthenticated document, and it is the URI the package must be immune to.
ResolveReference must be safe to call from multiple goroutines. Returning a nil stream with a nil error is refused as ErrReferenceNotFound, and never dereferenced.
func FSReferenceResolver ¶ added in v0.8.0
func FSReferenceResolver(fsys fs.FS, root string) ReferenceResolver
FSReferenceResolver returns a ReferenceResolver that serves external CipherReference URIs from fsys. It performs NO network access.
root declares the document-space prefix fsys stands for, which is what lets the resolver map the joined URI it is handed (ReferenceResolver states that the URI is always the joined one) onto a path fsys knows. A URI lying under root is served as the remainder of the path below it; every other URI is taken as a plain relative path in fsys. An empty root serves relative URIs only, which is what a document parsed from memory produces.
So for a document read with helium.Parser.ParseFile from /srv/docs/index.xml, whose sibling cipher text the document names as URI="ct.bin", the joined URI is /srv/docs/ct.bin, and FSReferenceResolver(os.DirFS("/srv/docs"), "/srv/docs") serves it as "ct.bin".
Whatever root admits, the resolved path is still fail-closed on anything that is not a plain in-tree path:
- a URI carrying a scheme (http:, https:, file:, urn:, or any "scheme:" per RFC 3986, including a Windows drive letter) is refused — the resolver never interprets a scheme, so it cannot be steered into a fetch;
- a path escaping the root (an absolute path outside the declared root, or one with ".." segments that leave the root after cleaning) is refused via an fs.ValidPath containment check;
- a leftover fragment ("#...") is refused.
Every rejection wraps ErrReferenceNotFound, so a caller matches them all with errors.Is. This resolver opens the file and hands the stream over; the decrypt's own allowance bounds how much of it is read, and the package closes it — ReferenceResolver owns that division.
Pass helium.PermissiveFS or an os.Root FS to widen what a caller is willing to serve.
type UnsupportedAlgorithmError ¶
type UnsupportedAlgorithmError struct {
// Parameter names the algorithm slot that rejected the URI, e.g.
// "block algorithm" or "MGF algorithm". It is diagnostic text and may
// be empty when the slot is not known at the point of failure.
Parameter string
Algorithm string
// contains filtered or unexported fields
}
UnsupportedAlgorithmError is returned for unrecognized algorithm URIs.
Construct it with keyed fields, as in &UnsupportedAlgorithmError{Algorithm: uri}. An unexported field makes an unkeyed composite literal fail to compile from another package, which is deliberate and is what lets the field set GROW as the diagnostics improve without breaking a caller: nobody can have written the positional form that a new field would invalidate. Reading the exported fields, comparing values, and recovering the type with errors.As are all unaffected.
func (*UnsupportedAlgorithmError) Error ¶
func (e *UnsupportedAlgorithmError) Error() string