Skip to content

feat: added PKCS#7 envelope implementation #298

Open
dallasd1 wants to merge 5 commits into
notaryproject:mainfrom
dallasd1:dadelan/pkcs7-envelope
Open

feat: added PKCS#7 envelope implementation #298
dallasd1 wants to merge 5 commits into
notaryproject:mainfrom
dallasd1:dadelan/pkcs7-envelope

Conversation

@dallasd1
Copy link
Copy Markdown

@dallasd1 dallasd1 commented Mar 12, 2026

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

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>
@github-actions
Copy link
Copy Markdown

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.

@github-actions github-actions Bot added the Stale label Apr 27, 2026
@yizha1 yizha1 removed the Stale label Apr 27, 2026
Copy link
Copy Markdown

@bketelsen bketelsen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread signature/pkcs7/envelope.go Outdated
return nil, &signature.SignatureEnvelopeNotFoundError{}
}
// For detached signatures, the kernel does dm-verity verification
return e.Content()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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()

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread signature/pkcs7/envelope.go Outdated
}

// Parse the result to populate envelope fields
p7, _ := gopkcs7.Parse(encoded)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't swallow an error on parsing... if caller sees success, subsequent calls to Verify() or Content() will be incorrect.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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{}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jws and cose return &base.Envelope{Envelope: &envelope{}} which has critical validations. returning a bare envelope bypasses all of these checks.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need tests to validate other chains still work like RSA-3072 and ECDSA p-384

Copy link
Copy Markdown
Author

@dallasd1 dallasd1 May 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jws and cose have compile-time assertions... this doesn't. var _ signature.Envelope = &envelope{}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assertion has been added to end of file since this envelope is not wrapped in base.Envelope

Copy link
Copy Markdown
Contributor

@shizhMSFT shizhMSFT left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

  1. Do we need go.mozilla.org/pkcs7 at all? tspclient-go already gives us most of the CMS surface in-house. notaryproject/tspclient-go ships internal/cms, internal/oid, internal/encoding/asn1/ber, and internal/hashutil — a pure-stdlib RFC 5652 implementation that has been in production for RFC 3161 timestamping. And notation-core-go/go.mod already directly requires github.com/notaryproject/tspclient-go v1.0.0 (line 8 of this PR's go.mod), so reusing that code costs zero new modules; importing go.mozilla.org/pkcs7 adds one. Concrete contrast points:

    • Correct OIDs. tspclient-go/internal/oid/oid.go defines ECDSAWithSHA256 = {1,2,840,10045,4,3,2} per RFC 5758 §3.2 (a signature-algorithm OID). This PR inherits gopkcs7.OIDEncryptionAlgorithmECDSAP256 = {1,2,840,10045,3,1,7} which is the secp256r1 curve OID — putting it in SignerInfo.signatureAlgorithm violates RFC 5652 §10.1.2 and produces a non-conformant CMS structure (Finding 5).
    • Real Verify(). tspclient-go/internal/cms/signed.go::verifySignature performs cert-chain validation, signature verification via cert.CheckSignature, and signed-attribute processing. This PR's Verify() performs no cryptographic check at all (Finding 2).
    • No dead weight. go.mozilla.org/pkcs7 ships decrypt.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-go and tspclient-go live under notaryproject, copy tspclient-go/internal/cms (+ oid, ber, hashutil) into notation-core-go/internal/cms as a temporary measure, with a // TODO linking 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/cms to a non-internal package. E.g. github.com/notaryproject/tspclient-go/cms. notation-core-go then imports it directly (it already requires tspclient-go v1.0.0). Pros: single source of truth; no new module. Cons: requires a tspclient-go release with new public API surface, and any future change to cms is now subject to the tspclient-go ABI contract.
    • (c) New shared module notaryproject/cms-go. Extract the CMS code into a dedicated repo + module that both tspclient-go and notation-core-go consume. Pros: cleanest separation; mirrors how tspclient-go itself was factored out. Cons: new repo, new release cadence, more moving parts.

    One caveat for any of these options: today tspclient-go/internal/cms only 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 of SignedData.Marshal + SignerInfo.Marshal." Still a substantially smaller delta than importing go.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.

  2. Verify() doesn't verify, and structurally cannot in this shape. envelope.go:188-194 just returns Content(). No cryptographic check. Worse, even fixing it is non-trivial: detached-PKCS#7 verification needs the original content, but signature.Envelope.Verify() takes no argument — after ParseEnvelope the 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 working Verify() through this interface. This is the strongest signal that PKCS#7 doesn't fit the signature.Envelope abstraction (see design question). For now, registering a verifier that returns "verified" without verifying is worse than not registering it at all.

  3. Hardcoded SHA-256 + signature-passthrough adapter is cryptographically broken for any key that isn't RSA-2048. Walk through with RSA-3072: proto.HashAlgorithmFromKeySpec returns SHA-384, AKV's SignDataAsync(RS384, payload) server-side hashes with SHA-384 and returns RSA(SHA-384(payload)), this PR wraps that signature in a PKCS#7 SignerInfo whose digestAlgorithm is hardcoded to id-sha256. Any verifier (kernel dm-verity or compliant CMS verifier) computes SHA-256(content) and the signature does not match. Same for RSA-4096 → SHA-512, EC P-384 → SHA-384, EC P-521 → SHA-512. The signerAdapter.Sign() ignoring the digest parameter (a violation of crypto.Signer's contract) is what hides the mismatch.

  4. SignatureAlgorithm is mislabeled in the parsed envelope. extractAlgorithm (line 220-228) calls keySpec.SignatureAlgorithm(), which for any KeyTypeRSA returns AlgorithmPS{256,384,512} (internal/algorithm/algorithm.go:83-90). But this PR produces RSASSA-PKCS#1 v1.5 signatures, not PSS. So Content().SignerInfo.SignatureAlgorithm will say "RSASSA-PSS" for a signature that is actually PKCS#1 v1.5 — a silent type-system lie. Fixing it requires adding RSASSA-PKCS1-v1_5-SHA-{256,384,512} to signature.Algorithm (the enum currently has only PS/ES — signature/algorithm.go:29-34), which in turn requires a coordinated change to notaryproject/specifications (signature-specification.md algorithm table) and notation-go plugin 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.

  5. EC OIDs are wrong; should we just reject EC at the envelope layer? Tied to Finding 1: getEncryptionOID returns OIDEncryptionAlgorithmECDSAP256/384/521 from go.mozilla.org/pkcs7, which are named-curve OIDs (prime256v1/secp384r1/secp521r1), not signature-algorithm OIDs. RFC 5652 §10.1.2 requires signatureAlgorithm in SignerInfo to identify the signature algorithm (e.g., ecdsa-with-SHA256 1.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?

  6. Test coverage misses the critical path. Tests use a local crypto.Signer. The PR's stated motivation for signerAdapter is "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 against SHA256(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. Once signature.RegisterEnvelopeType("application/pkcs7-signature", ...) runs at import time, every downstream that imports notation-core-go/signature (transitively, through any verifier) pulls in go.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 way database/sql drivers do?
  • go.mod hygiene. Line 15 lists go.mozilla.org/pkcs7 v0.9.0 // indirect, but signature/pkcs7/envelope.go:27 imports it directly. go mod tidy should move it to the direct require block.

Comment on lines +33 to +37
func init() {
if err := signature.RegisterEnvelopeType(MediaTypeEnvelope, NewEnvelope, ParseEnvelope); err != nil {
panic(err)
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +49 to +51
func NewEnvelope() signature.Envelope {
return &envelope{}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NewEnvelope() returns a zero-value *envelope, then Sign() mutates e.raw/p7/certs/sigBytes (lines 134–138) so subsequent Verify/Content calls return what was just signed. This conflates construction with signing-result. The JWS/COSE envelopes use base.Envelope precisely to separate these. The PR comment acknowledges skipping base.Envelope "because dm-verity signatures must not have signing-time" — but base.Envelope doesn't force signing-time on you; only the envelope-specific Sign method does. Please reconsider; sharing base.Envelope (with the signing-time bypass) gives consistent state semantics and free Payload() accessors.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread signature/pkcs7/envelope.go Outdated
Comment on lines +73 to +74
// Sign implements signature.Envelope interface.
// Uses signerAdapter pattern to support both local and remote signers (AKV, plugins).
Copy link
Copy Markdown
Contributor

@shizhMSFT shizhMSFT May 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// 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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adopted option (a) with verifySignerOutput

}

// Set digest algorithm to SHA-256 (kernel dm-verity requirement)
sd.SetDigestAlgorithm(gopkcs7.OIDDigestAlgorithmSHA256)
Copy link
Copy Markdown
Contributor

@shizhMSFT shizhMSFT May 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See top-level Finding 3.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will add comment in a new commit bundled with the pending tspclient change

Comment thread signature/pkcs7/envelope.go Outdated
Comment on lines +132 to +139
// 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
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested 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
}
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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Errors are now checked

Comment thread signature/pkcs7/envelope.go Outdated
Comment on lines +220 to +228
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
Copy link
Copy Markdown
Contributor

@shizhMSFT shizhMSFT May 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See top-level Finding 4. keySpec.SignatureAlgorithm() for KeyTypeRSA returns AlgorithmPS{256,384,512} (internal/algorithm/algorithm.go:83-90) — RSASSA-PSS, not PKCS#1 v1.5. So the parsed EnvelopeContent.SignerInfo.SignatureAlgorithm reports the wrong algorithm. To fix this honestly we need a new signature.Algorithm enum member (RSASSA-PKCS1-v1_5-SHA-{256,384,512}), which in turn requires coordinated changes in notaryproject/specifications and notation-go plugin proto validation. Please file a tracking issue covering all three repos.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will file the issue for the new enums linking the changes in each repo independently of this PR

Comment on lines +32 to +40
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},
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TestSignRejectsNonRSA2048 is added to handle negative tests

Comment thread signature/pkcs7/envelope_test.go Outdated
Comment on lines +124 to +138
// 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")
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The whole stated motivation for signerAdapter is supporting remote signers. Please add a test that:

  1. Uses a fake signature.Signer whose Sign(content) returns a precomputed RSA-PKCS#1 v1.5(SHA-256(content)) from a known cert chain.
  2. Signs via the envelope.
  3. Parses the result and independently recomputes SHA256(content) and verifies the signature with rsa.VerifyPKCS1v15.

This is the contract dm-verity needs end-to-end. If it doesn't pass with RSA-3072 today, the bug surfaces immediately.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test TestSignParseVerifyRoundTrip has been added to sign, parse, verify, and check content's cert chain

Comment thread go.mod Outdated
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
Copy link
Copy Markdown
Contributor

@shizhMSFT shizhMSFT May 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See top-level Finding 1. notation-core-go/go.mod already requires github.com/notaryproject/tspclient-go v1.0.0 directly, whose (currently internal) cms + oid packages already implement RFC 5652 SignedData parse + verify on stdlib with correct OIDs. Three options to reuse that work without adding go.mozilla.org/pkcs7: (a) copy the code into notation-core-go/internal/cms as a temporary measure (same-org duplication, refactor later), (b) lift tspclient-go/internal/cms to a non-internal package and import directly, or (c) extract to a new shared notaryproject/cms-go module. Worth raising with @priteshbandi / @rgnote before merging.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Copilot AI review requested due to automatic review settings May 13, 2026 23:13
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-specific Verify behavior.
  • Adds unit, conformance, and fuzz tests for the new PKCS#7 envelope implementation.
  • Adds the go.mozilla.org/pkcs7 dependency.

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.

Comment thread signature/pkcs7/envelope.go
Comment thread signature/pkcs7/envelope.go
Comment thread signature/pkcs7/envelope.go
Comment thread signature/pkcs7/envelope.go Outdated
Comment thread signature/pkcs7/envelope.go
Comment thread signature/pkcs7/envelope.go
Comment thread signature/pkcs7/envelope.go Outdated
- 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>
@dallasd1 dallasd1 force-pushed the dadelan/pkcs7-envelope branch from caf155a to 59d15e2 Compare May 14, 2026 00:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants