feat: added PKCS#7 envelope implementation #298
Conversation
Adds PKCS#7/CMS signature envelope implementation following the same pattern as existing JWS and COSE envelopes. Features: - Implements signature.Envelope interface (Sign, Verify, Content) - Uses go.mozilla.org/pkcs7 library for ASN.1 encoding - Uses signerAdapter pattern to support both local and remote signers - Works with Azure Key Vault and other plugins via signature.Signer - Produces detached signatures for dm-verity kernel verification - Supports RSA and ECDSA key types with SHA-256 - Registers media type application/pkcs7-signature The signerAdapter wraps pre-computed signatures from any signature.Signer to satisfy the crypto.Signer interface expected by the Mozilla library. This enables remote signers (like Azure Key Vault) that don't expose private keys to work with the library. Signed-off-by: Dallas Delaney <dadelan@microsoft.com>
The base.Envelope wrapper validates that signing-time is present, but PKCS#7 signatures for dm-verity must not include authenticated attributes (including signing-time) per Linux kernel requirements. The kernel's PKCS#7 verifier in crypto/asymmetric_keys/public_key.c expects raw signature data without CMS authenticated attributes. Remove the base.Envelope wrapper from NewEnvelope() and ParseEnvelope() so the pkcs7 envelope implements signature.Envelope directly. Signed-off-by: Dallas Delaney <dadelan@microsoft.com>
Signed-off-by: Dallas Delaney <dadelan@microsoft.com>
9dc2a7d to
7dc8aca
Compare
|
This PR is stale because it has been opened for 45 days with no activity. Remove stale label or comment. Otherwise, it will be closed in 30 days. |
bketelsen
left a comment
There was a problem hiding this comment.
PR looks good but there are a few blockers, critically Verify() doesn't actually do cryptographic validation and SHA256 is hardcoded for all key types.
| return nil, &signature.SignatureEnvelopeNotFoundError{} | ||
| } | ||
| // For detached signatures, the kernel does dm-verity verification | ||
| return e.Content() |
There was a problem hiding this comment.
this method calls Content() which only parses structure... no cert chain validation or expiration check. cose and jws implementations each validate, but this doesn't. should call e.p7.Verify()
There was a problem hiding this comment.
The initial proposal highlights that verification for this envelope happens in the host kernel via a trusted keyring, so Verify() doesn't check the layer signatures but informs the caller of the type of signatures included. In the current implementation, Verify() implements this by returning ErrDetachedNotVerifiable.
This envelope is an addition to the existing JWS/COSE digest signatures rather than a replacement, so users still run the existing JWS/COSE Verify() on the image manifest before pulling the image.
Alternatively, we could extend notation-go's verifier to not only pull the manifest signatures but also these new layer signatures. Then we could add a new API to verify this detached signature against the payload and validate the certificate chain. Since this would be non-trivial and the new dm-verity signing is gated behind the NOTATION_EXPERIMENTAL flag, it may be worth considering as a follow-up. Happy to create an issue for this if you'd like.
| } | ||
|
|
||
| // Set digest algorithm to SHA-256 (kernel dm-verity requirement) | ||
| sd.SetDigestAlgorithm(gopkcs7.OIDDigestAlgorithmSHA256) |
There was a problem hiding this comment.
notary spec has algo/hash pairings:
- RSA-3072 → SHA-384; RSA-4096 → SHA-512
- ECDSA P-384 → SHA-384; ECDSA P-521 → SHA-512
but this hardcodes SHA-256 for all. Tests only cover RSA-2048 so this bug is never exposed.
There was a problem hiding this comment.
The hardcoded SHA-256 has been removed and validation API validateSignRequest rejects everything except RSA-2048.
The tests now cover some rejected RSA and ECDSA types
| } | ||
|
|
||
| // Parse the result to populate envelope fields | ||
| p7, _ := gopkcs7.Parse(encoded) |
There was a problem hiding this comment.
don't swallow an error on parsing... if caller sees success, subsequent calls to Verify() or Content() will be incorrect.
There was a problem hiding this comment.
Error will now be surfaced on failure
| // Note: Unlike JWS/COSE, PKCS#7 for dm-verity does NOT use base.Envelope wrapper | ||
| // because dm-verity signatures must not have signing-time (kernel requirement). | ||
| func NewEnvelope() signature.Envelope { | ||
| return &envelope{} |
There was a problem hiding this comment.
jws and cose return &base.Envelope{Envelope: &envelope{}} which has critical validations. returning a bare envelope bypasses all of these checks.
There was a problem hiding this comment.
This initial PR is scoped to a narrow profile of detached PKCS#7, no signed attributes, rsa-2048, and sha-256, validated end-to-end. The long-term plan would be to broaden this profile after the initial change lands. Broadening the profile would likely mean also coordinating changes to notation/specifications
Given the scope described above, this envelope intentionally does not wrap base.Envelope because it would require SigningTime and SigningScheme to be set. The PR as-is will only require RSA-2048 + SHA-256.
To ensure validation is still present, we use validateSignRequest() and verifySignerOutput(), which does similar validation checks (except for ValidateCodeSigningCertChain(), which can be added if you prefer)
|
|
||
| const testPayload = "test dm-verity root hash payload" | ||
|
|
||
| // newRSATestSigner creates an RSA-2048 test signer using testhelper certs. |
There was a problem hiding this comment.
need tests to validate other chains still work like RSA-3072 and ECDSA p-384
There was a problem hiding this comment.
Other chains like RSA-3072 and ECDSA are intentionally unsupported in this initial PR for this envelope type, so tests have now been added to verify those produce the expected errors when requested.
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| package pkcs7 |
There was a problem hiding this comment.
this test suite is missing many of the patterns present in jws and cose - fuzzing, negative tests, ECDSA, conformance. It is not complete without them.
There was a problem hiding this comment.
Fuzzing, negative tests, conformance tests are now covered. ECDSA is covered as a rejected key spec.
| "github.com/notaryproject/notation-core-go/signature" | ||
| gopkcs7 "go.mozilla.org/pkcs7" | ||
| ) | ||
|
|
There was a problem hiding this comment.
jws and cose have compile-time assertions... this doesn't. var _ signature.Envelope = &envelope{}
There was a problem hiding this comment.
Assertion has been added to end of file since this envelope is not wrapped in base.Envelope
There was a problem hiding this comment.
Important
Hybrid review. The dependency concern (Finding 1 on go.mozilla.org/pkcs7) reflects @shizhMSFT's own position and is the basis for this REQUEST_CHANGES. Everything else in this review was drafted by an AI agent (Claude Opus 4.7, shizh-reviewer skill) and is offered for the author's consideration only — please weigh those points on their merits, not as gating concerns.
Thanks for kicking this off, @dallasd1. notation-core-go is the cryptographic core SDK for the entire Notary Project ecosystem, so the bar on new direct dependencies is high.
Findings
Finding 1 below is the gating concern from @shizhMSFT. Findings 2–6 are AI-drafted observations to consider on their merits.
-
Do we need
go.mozilla.org/pkcs7at all?tspclient-goalready gives us most of the CMS surface in-house.notaryproject/tspclient-goshipsinternal/cms,internal/oid,internal/encoding/asn1/ber, andinternal/hashutil— a pure-stdlib RFC 5652 implementation that has been in production for RFC 3161 timestamping. Andnotation-core-go/go.modalready directly requiresgithub.com/notaryproject/tspclient-go v1.0.0(line 8 of this PR's go.mod), so reusing that code costs zero new modules; importinggo.mozilla.org/pkcs7adds one. Concrete contrast points:- Correct OIDs.
tspclient-go/internal/oid/oid.godefinesECDSAWithSHA256 = {1,2,840,10045,4,3,2}per RFC 5758 §3.2 (a signature-algorithm OID). This PR inheritsgopkcs7.OIDEncryptionAlgorithmECDSAP256 = {1,2,840,10045,3,1,7}which is thesecp256r1curve OID — putting it inSignerInfo.signatureAlgorithmviolates RFC 5652 §10.1.2 and produces a non-conformant CMS structure (Finding 5). - Real
Verify().tspclient-go/internal/cms/signed.go::verifySignatureperforms cert-chain validation, signature verification viacert.CheckSignature, and signed-attribute processing. This PR'sVerify()performs no cryptographic check at all (Finding 2). - No dead weight.
go.mozilla.org/pkcs7shipsdecrypt.go,encrypt.go, a BER parser, and DSA support — none of which dm-verity needs.
Concrete proposal — three options, in increasing order of permanence:
- (a) Copy + refactor later. Since both
notation-core-goandtspclient-golive undernotaryproject, copytspclient-go/internal/cms(+oid,ber,hashutil) intonotation-core-go/internal/cmsas a temporary measure, with a// TODOlinking to a tracking issue. Pros: zero coordination cost, lands in this PR. Cons: code duplication until we deduplicate; both copies will drift unless we explicitly track them. - (b) Lift
tspclient-go/internal/cmsto a non-internal package. E.g.github.com/notaryproject/tspclient-go/cms.notation-core-gothen imports it directly (it already requirestspclient-go v1.0.0). Pros: single source of truth; no new module. Cons: requires atspclient-gorelease with new public API surface, and any future change tocmsis now subject to thetspclient-goABI contract. - (c) New shared module
notaryproject/cms-go. Extract the CMS code into a dedicated repo + module that bothtspclient-goandnotation-core-goconsume. Pros: cleanest separation; mirrors howtspclient-goitself was factored out. Cons: new repo, new release cadence, more moving parts.
One caveat for any of these options: today
tspclient-go/internal/cmsonly implements parse + verify. It does not implement sign. So this is not a literal drop-in replacement for gopkcs7's signing path; it's "reuse the structs + BER + correct OIDs + verify, then add ~150 lines ofSignedData.Marshal+SignerInfo.Marshal." Still a substantially smaller delta than importinggo.mozilla.org/pkcs7, with correct OIDs from day one and a verifier that actually verifies./cc @priteshbandi @yizha1 — happy to file a tracking issue covering whichever path the maintainers prefer.
- Correct OIDs.
-
Verify()doesn't verify, and structurally cannot in this shape.envelope.go:188-194just returnsContent(). No cryptographic check. Worse, even fixing it is non-trivial: detached-PKCS#7 verification needs the original content, butsignature.Envelope.Verify()takes no argument — afterParseEnvelopethe envelope only holds the DER bytes. So unless we change the interface or stash the content in the envelope itself (which JWS/COSE don't have to do because they're attached), a freshly-parsed PKCS#7 envelope can never have a workingVerify()through this interface. This is the strongest signal that PKCS#7 doesn't fit thesignature.Envelopeabstraction (see design question). For now, registering a verifier that returns "verified" without verifying is worse than not registering it at all. -
Hardcoded SHA-256 + signature-passthrough adapter is cryptographically broken for any key that isn't RSA-2048. Walk through with RSA-3072:
proto.HashAlgorithmFromKeySpecreturns SHA-384, AKV'sSignDataAsync(RS384, payload)server-side hashes with SHA-384 and returnsRSA(SHA-384(payload)), this PR wraps that signature in a PKCS#7 SignerInfo whosedigestAlgorithmis hardcoded toid-sha256. Any verifier (kernel dm-verity or compliant CMS verifier) computesSHA-256(content)and the signature does not match. Same for RSA-4096 → SHA-512, EC P-384 → SHA-384, EC P-521 → SHA-512. ThesignerAdapter.Sign()ignoring thedigestparameter (a violation ofcrypto.Signer's contract) is what hides the mismatch. -
SignatureAlgorithmis mislabeled in the parsed envelope.extractAlgorithm(line 220-228) callskeySpec.SignatureAlgorithm(), which for anyKeyTypeRSAreturnsAlgorithmPS{256,384,512}(internal/algorithm/algorithm.go:83-90). But this PR produces RSASSA-PKCS#1 v1.5 signatures, not PSS. SoContent().SignerInfo.SignatureAlgorithmwill say "RSASSA-PSS" for a signature that is actually PKCS#1 v1.5 — a silent type-system lie. Fixing it requires addingRSASSA-PKCS1-v1_5-SHA-{256,384,512}tosignature.Algorithm(the enum currently has only PS/ES —signature/algorithm.go:29-34), which in turn requires a coordinated change tonotaryproject/specifications(signature-specification.md algorithm table) andnotation-goplugin proto validation. This is a cross-PR cascade — the feature stack does not work end-to-end without all three repos moving together. Worth filing a tracking issue. -
EC OIDs are wrong; should we just reject EC at the envelope layer? Tied to Finding 1:
getEncryptionOIDreturnsOIDEncryptionAlgorithmECDSAP256/384/521fromgo.mozilla.org/pkcs7, which are named-curve OIDs (prime256v1/secp384r1/secp521r1), not signature-algorithm OIDs. RFC 5652 §10.1.2 requiressignatureAlgorithminSignerInfoto identify the signature algorithm (e.g.,ecdsa-with-SHA2561.2.840.10045.4.3.2); putting a curve OID there fails any conformant CMS verifier. Given that gopkcs7 ships these incorrectly and the kernel dm-verity flow does not consume ECDSA anyway, should we just reject EC keys explicitly at the envelope boundary and remove the EC branches? -
Test coverage misses the critical path. Tests use a local
crypto.Signer. The PR's stated motivation forsignerAdapteris "to support remote signers (Azure Key Vault, plugins)" — but no test exercises that path. Please add a test that signs via a fake remote signer with a non-SHA-256 hash and asserts the resulting PKCS#7 either (a) fails to produce a SHA-256 envelope or (b) verifies againstSHA256(content)end-to-end with the leaf cert. The bug in Finding 3 will surface immediately.
Design question
Per the issue thread, dm-verity needs detached PKCS#7 with no signed attributes, hardcoded SHA-256, and RSA-PKCS#1 v1.5. That's a very narrow contract — barely a "signature envelope" in the JWS/COSE sense (no expiry, no signing-time, no envelope verify). And as Finding 2 shows, Verify() is structurally broken in this shape. Should this instead be a small pkcs7 helper package that exposes Sign(payload, signer) ([]byte, error) directly, without registering through signature.Envelope? Once we register MediaTypeEnvelope = "application/pkcs7-signature" in a release, removing or renaming it is a breaking change every future notation-core-go release must honor — for a single-consumer envelope. Are we OK with that ABI commitment?
Verdict: REQUEST_CHANGES from @shizhMSFT, gating specifically on Finding 1 (the go.mozilla.org/pkcs7 dependency in notation-core-go). Findings 2–6, the design question, the non-blocking suggestions, and all inline comments below are AI-drafted and offered for the author's consideration — not gating. Happy to discuss in the next community meeting.
Non-blocking suggestions
init()registration. Oncesignature.RegisterEnvelopeType("application/pkcs7-signature", ...)runs at import time, every downstream that importsnotation-core-go/signature(transitively, through any verifier) pulls ingo.mozilla.org/pkcs7. JWS/COSE do the same today, so this is an established pattern, not a new contract — but PKCS#7 has exactly one consumer (dm-verity). Should it be opt-in registration via blank import (_ "github.com/notaryproject/notation-core-go/signature/pkcs7"), the waydatabase/sqldrivers do?go.modhygiene. Line 15 listsgo.mozilla.org/pkcs7 v0.9.0 // indirect, butsignature/pkcs7/envelope.go:27imports it directly.go mod tidyshould move it to the directrequireblock.
| func init() { | ||
| if err := signature.RegisterEnvelopeType(MediaTypeEnvelope, NewEnvelope, ParseEnvelope); err != nil { | ||
| panic(err) | ||
| } | ||
| } |
There was a problem hiding this comment.
See top-level "non-blocking suggestions". JWS/COSE register the same way, but PKCS#7 has a single consumer (dm-verity). Worth considering blank-import opt-in (
database/sql-driver pattern).
There was a problem hiding this comment.
Will leave the init() as is for now to be consistent with JWS/COSE since I'm not sure if having a single consumer justifies deviating from the pattern.
| func NewEnvelope() signature.Envelope { | ||
| return &envelope{} | ||
| } |
There was a problem hiding this comment.
NewEnvelope()returns a zero-value*envelope, thenSign()mutatese.raw/p7/certs/sigBytes(lines 134–138) so subsequentVerify/Contentcalls return what was just signed. This conflates construction with signing-result. The JWS/COSE envelopes usebase.Envelopeprecisely to separate these. The PR comment acknowledges skippingbase.Envelope"because dm-verity signatures must not have signing-time" — butbase.Envelopedoesn't force signing-time on you; only the envelope-specificSignmethod does. Please reconsider; sharingbase.Envelope(with the signing-time bypass) gives consistent state semantics and freePayload()accessors.
There was a problem hiding this comment.
Sharing from comment above, this initial PR is scoped to a narrow profile of detached PKCS#7 and gated behind NOTATION_EXPERIMENTAL, but if base.Envelope is required at this stage I can draft up that change.
| // Sign implements signature.Envelope interface. | ||
| // Uses signerAdapter pattern to support both local and remote signers (AKV, plugins). |
There was a problem hiding this comment.
| // Sign implements signature.Envelope interface. | |
| // Uses signerAdapter pattern to support both local and remote signers (AKV, plugins). | |
| // Sign implements signature.Envelope interface. | |
| // | |
| // Constraints (kernel dm-verity, see kernel docs/admin-guide/device-mapper/verity.rst): | |
| // - RSASSA-PKCS#1 v1.5 only (no PSS, no ECDSA) | |
| // - SHA-256 only | |
| // - No signed attributes (so the signature is over SHA-256(content) directly) | |
| // | |
| // The implementation MUST therefore reject any KeySpec for which the upstream | |
| // signer would not produce a SHA-256 RSASSA-PKCS#1 v1.5 signature (today, | |
| // only RSA-2048 — see proto.HashAlgorithmFromKeySpec), or it will declare | |
| // digestAlgorithm = id-sha256 in the envelope while the actual signature | |
| // covers a SHA-384/SHA-512 digest. |
The current code declares SHA-256 in the envelope but accepts any signature the upstream signer produces. See top-level Finding 3 for the full chain. Two ways to fix the body: (a) restrict req.Signer keyspecs to RSA-2048 and reject anything else with UnsupportedSigningKeyError, or (b) introduce a signature.Signer extension that lets us request an explicit hash algorithm before signing.
There was a problem hiding this comment.
Adopted option (a) with verifySignerOutput
| } | ||
|
|
||
| // Set digest algorithm to SHA-256 (kernel dm-verity requirement) | ||
| sd.SetDigestAlgorithm(gopkcs7.OIDDigestAlgorithmSHA256) |
There was a problem hiding this comment.
See top-level Finding 3.
There was a problem hiding this comment.
Addressed by narrowing to RSA-2048
| sd.SetEncryptionAlgorithm(encryptionOID) | ||
|
|
||
| // Sign without authenticated attributes (kernel dm-verity requirement) | ||
| if err := sd.SignWithoutAttr(certs[0], adapter, gopkcs7.SignerInfoConfig{}); err != nil { |
There was a problem hiding this comment.
Per the upstream comment: "This function is needed to sign old Android APKs, something you probably shouldn't do unless you're maintaining backward compatibility for old applications." Please add a comment explaining why we use it here (kernel dm-verity requires no
SignedAttributes) so future readers don't think we hit this by accident.
There was a problem hiding this comment.
Will add comment in a new commit bundled with the pending tspclient change
| // Parse the result to populate envelope fields | ||
| p7, _ := gopkcs7.Parse(encoded) | ||
| e.raw = encoded | ||
| e.p7 = p7 | ||
| e.certs = certs | ||
| if p7 != nil && len(p7.Signers) > 0 { | ||
| e.sigBytes = p7.Signers[0].EncryptedDigest | ||
| } |
There was a problem hiding this comment.
| // Parse the result to populate envelope fields | |
| p7, _ := gopkcs7.Parse(encoded) | |
| e.raw = encoded | |
| e.p7 = p7 | |
| e.certs = certs | |
| if p7 != nil && len(p7.Signers) > 0 { | |
| e.sigBytes = p7.Signers[0].EncryptedDigest | |
| } | |
| p7, err := gopkcs7.Parse(encoded) | |
| if err != nil { | |
| return nil, &signature.InvalidSignatureError{Msg: fmt.Sprintf("self-parse failed after Sign: %v", err)} | |
| } | |
| e.raw = encoded | |
| e.p7 = p7 | |
| e.certs = certs | |
| if len(p7.Signers) > 0 { | |
| e.sigBytes = p7.Signers[0].EncryptedDigest | |
| } |
"Always check errors before processing any return values." We just produced the bytes ourselves; if Parse fails on our own output, something is very wrong and we should fail fast.
| func extractAlgorithm(certs []*x509.Certificate) (signature.Algorithm, error) { | ||
| if len(certs) == 0 { | ||
| return 0, fmt.Errorf("no certificates available to determine algorithm") | ||
| } | ||
| keySpec, err := signature.ExtractKeySpec(certs[0]) | ||
| if err != nil { | ||
| return 0, err | ||
| } | ||
| return keySpec.SignatureAlgorithm(), nil |
There was a problem hiding this comment.
See top-level Finding 4.
keySpec.SignatureAlgorithm()forKeyTypeRSAreturnsAlgorithmPS{256,384,512}(internal/algorithm/algorithm.go:83-90) — RSASSA-PSS, not PKCS#1 v1.5. So the parsedEnvelopeContent.SignerInfo.SignatureAlgorithmreports the wrong algorithm. To fix this honestly we need a newsignature.Algorithmenum member (RSASSA-PKCS1-v1_5-SHA-{256,384,512}), which in turn requires coordinated changes innotaryproject/specificationsandnotation-goplugin proto validation. Please file a tracking issue covering all three repos.
There was a problem hiding this comment.
Will file the issue for the new enums linking the changes in each repo independently of this PR
| func newRSATestSigner() *testPrimitiveSigner { | ||
| tuple := testhelper.GetRSACertTuple(2048) | ||
| rootCert := testhelper.GetRSARootCertificate().Cert | ||
| return &testPrimitiveSigner{ | ||
| key: tuple.PrivateKey, | ||
| certs: []*x509.Certificate{tuple.Cert, rootCert}, | ||
| keySpec: signature.KeySpec{Type: signature.KeyTypeRSA, Size: 2048}, | ||
| } | ||
| } |
There was a problem hiding this comment.
Once the keyspec restriction lands per the comment above, please add explicit negative tests: RSA-3072, RSA-4096, EC P-256/P-384/P-521 should all return
UnsupportedSigningKeyError. Right now the test signs with RSA-2048 and never exercises the buggy paths.
There was a problem hiding this comment.
TestSignRejectsNonRSA2048 is added to handle negative tests
| // TestSignError tests that Sign fails with a broken signer. | ||
| func TestSignError(t *testing.T) { | ||
| env := NewEnvelope() | ||
| req := &signature.SignRequest{ | ||
| Payload: signature.Payload{ | ||
| ContentType: MediaTypeEnvelope, | ||
| Content: []byte(testPayload), | ||
| }, | ||
| Signer: &failingSigner{}, | ||
| } | ||
| _, err := env.Sign(req) | ||
| if err == nil { | ||
| t.Fatal("Sign() with failing signer expected error, got nil") | ||
| } | ||
| } |
There was a problem hiding this comment.
The whole stated motivation for
signerAdapteris supporting remote signers. Please add a test that:
- Uses a fake
signature.SignerwhoseSign(content)returns a precomputedRSA-PKCS#1 v1.5(SHA-256(content))from a known cert chain.- Signs via the envelope.
- Parses the result and independently recomputes
SHA256(content)and verifies the signature withrsa.VerifyPKCS1v15.This is the contract dm-verity needs end-to-end. If it doesn't pass with RSA-3072 today, the bug surfaces immediately.
There was a problem hiding this comment.
Test TestSignParseVerifyRoundTrip has been added to sign, parse, verify, and check content's cert chain
| require github.com/x448/float16 v0.8.4 // indirect | ||
| require ( | ||
| github.com/x448/float16 v0.8.4 // indirect | ||
| go.mozilla.org/pkcs7 v0.9.0 // indirect |
There was a problem hiding this comment.
See top-level Finding 1.
notation-core-go/go.modalready requiresgithub.com/notaryproject/tspclient-go v1.0.0directly, whose (currentlyinternal)cms+oidpackages already implement RFC 5652 SignedData parse + verify on stdlib with correct OIDs. Three options to reuse that work without addinggo.mozilla.org/pkcs7: (a) copy the code intonotation-core-go/internal/cmsas a temporary measure (same-org duplication, refactor later), (b) lifttspclient-go/internal/cmsto a non-internal package and import directly, or (c) extract to a new sharednotaryproject/cms-gomodule. Worth raising with @priteshbandi / @rgnote before merging.
There was a problem hiding this comment.
Thanks for pointing to the in-tree CMS library @shizhMSFT. Agreed that dropping go.mozilla.org/pkcs7 is the right call here.
I see two reasonable paths and would like guidance before pushing:
(a) Copy tspclient-go/internal/cms into notation-core-go/internal/cms. This would be fast to land but introduces duplication and the two copies could drift. tspclient-go/internal/cms has been stable for ~a year so that risk seems to be low.
(b) Lift tspclient-go/internal/cms to a public tspclient-go/cms package and have notation-core-go import it. This seems to be cleaner long-term but requires a new PR and a new tspclient-go release.
I'm happy to push (a) with a tracking issue, or switch to (b) if you'd prefer. Please let me know which direction you'd like to take.
- Restrict to RSA-2048 + SHA-256 + RSASSA-PKCS#1 v1.5; reject other keys with UnsupportedSigningKeyError and remove the unreachable EC branches in getEncryptionOID. - Reject SignRequest fields the dm-verity profile cannot honor (SigningTime, Expiry, SigningScheme, SigningAgent, ExtendedSignedAttributes) and nil Signer with InvalidSignRequestError. - Verify upstream signer output is RSASSA-PKCS#1 v1.5 over SHA-256 of the payload before wrapping it. - Verify() returns exported sentinel ErrDetachedNotVerifiable; detached PKCS#7 cannot be verified through signature.Envelope.Verify. - Content() leaves SignerInfo.SignatureAlgorithm zero rather than mislabeling PKCS#1 v1.5 as PSS (enum lacks PKCS#1 v1.5 constants). - Surface the post-Sign Parse error instead of swallowing it. - Recover from gopkcs7 v0.9.0 panics on malformed BER. - signerAdapter panics on a second Sign call (single-use invariant). - Add compile-time signature.Envelope assertion; go mod tidy moves gopkcs7 to direct require. - Add negative tests, seeded fuzz target, and conformance test (OIDs, no signed attrs, detached, chain ordering, independent rsa.VerifyPKCS1v15). Signed-off-by: Dallas Delaney <dadelan@microsoft.com>
There was a problem hiding this comment.
Pull request overview
Adds a new signature/pkcs7 envelope implementation intended to produce dm-verity–compatible PKCS#7/CMS SignedData (detached, no authenticated/signed attributes), leveraging go.mozilla.org/pkcs7.
Changes:
- Introduces a PKCS#7 envelope with
Sign,ParseEnvelope,Content, and a dm-verity-specificVerifybehavior. - Adds unit, conformance, and fuzz tests for the new PKCS#7 envelope implementation.
- Adds the
go.mozilla.org/pkcs7dependency.
Reviewed changes
Copilot reviewed 5 out of 6 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| signature/pkcs7/envelope.go | Implements PKCS#7 envelope creation/parsing for dm-verity profile and registers the envelope type |
| signature/pkcs7/envelope_test.go | Adds unit tests for signing/parsing and request validation behaviors |
| signature/pkcs7/conformance_test.go | Validates produced PKCS#7 structure matches dm-verity expectations |
| signature/pkcs7/fuzz_test.go | Adds fuzz coverage for parsing and calling Verify/Content |
| go.mod | Adds go.mozilla.org/pkcs7 dependency |
| go.sum | Records checksums for the new dependency |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- ParseEnvelope rejects non-1 signer count and empty EncryptedDigest - validateSignRequest rejects empty Payload.Content (matches base.Envelope) - verifySignerOutput guards against nil leaf certificate - signerAdapter.Sign returns error instead of panicking on second call - Content().Payload.ContentType is empty (PKCS#7 detached doesn't carry it) - add tests for new validation checks Signed-off-by: Dallas Delaney <dadelan@microsoft.com>
caf155a to
59d15e2
Compare
Add the PKCS#7 envelope implementation to support creating signatures for dm-verity. The kernel's dm-verity feature requires PKCS#7 signatures without authenticated attributes.
This PR leverages the open source mozilla pkcs7 package.
#298