// The verdicts of a signature of alg 2 (CMS with certificates) and of a seal of
// seal_type 2 (RFC 3161), as the Go package capsule gives them (signature2.go,
// spec v0.11 §29.7, §29.10, §29.11): F1, F2, F5 and F6 with the signers named,
// and S1 to S5 with the authority of a valid seal. Internal: index.ts does not
// re-export it.
import { ALG_CMS , authorMessage , sealSubject , signersDigest } from './author.ts' ;
import { equalBytes , toHex , utf8Length } from './bytes.ts' ;
import { type Decoder , Encoder , unmarshal } from './cbor.ts' ;
import {
addInstants ,
certHolder ,
CMS as Go reads it: the issuer by its Name, uncompressed keys, the BOM, linear readers
Fixes T1, T3, T13 and the CMS part of T5 of the review of the session of
1 and 2 October (docs, spec_v0.11/revision_sesion_1_2_octubre.md):
- securitycms.ts: an issuer that breaks the rules of the declared author
shows the SHA-256 of the DER of its Name, Go's sha256.Sum256(RawIssuer),
and no longer that of the certificate (cms.certIssuerHash).
- cms.ts: an ECDSA key counts only with its point uncompressed, 0x04 and
the two coordinates, the only form that Go's x509.ParsePKIXPublicKey
reads: a compressed one makes a signer not verifiable (F5) and a seal S1.
- cms.ts: a UTF8String and the times of a certificate and of a token keep a
leading U+FEFF, as Go reads the bytes: such a name shows the hash, and
such a time breaks the profile (F1, S2).
- cms.ts: oidOf and intOf take time linear in the length of the element.
An arc of up to seven digits accumulates in a number, a longer one and
every INTEGER are read whole from hexadecimal, never by a shift per byte,
which took some 700 ms for 60 KB; attributes of one type are appended,
not copied.
- security.ts: evaluateSecurity never throws. A fault while it evaluates
the signature gives F1, one while it evaluates the seal S2, each apart,
and one while it decodes the area X, as the Go reference will from v0.12.
- Tests: securitycms.test.ts and security.failure.test.ts are new.
cms.test.ts now refuses a second content-type and two signature-time-
stamps for the rule of the count, with every SET OF in DER order, and
der.test.ts tests the depth at its boundary. cmsbuild.ts makes names,
validities and compressed points of its own.
capsule.EvaluateSecurityIn of the Go reference at spec-v0.11 gives the same
verdicts, signer lines and Spanish lines on 25 areas made with cmsbuild.ts:
issuers with ESC, U+202E, empty, of 300 bytes or with a leading U+FEFF;
names and times with a leading U+FEFF; and compressed keys on P-256, P-384
and P-521, as signers and as authorities of a seal. HEAD gave other ones in
19 of them. npm run verify passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
certIssuerHash ,
certIssuerName ,
certValidAt ,
checkSigner ,
checkToken ,
CmsAlgorithmError ,
CmsFormError ,
parseSignature ,
parseToken ,
type SignerInfo ,
tokenImprintIsSHA256 ,
} from './cms.ts' ;
import { compareInstants , type Instant } from './datekey.ts' ;
import { DateKeysError } from './errors.ts' ;
import { checkAuthor } from './pathrule.ts' ;
/** The most required signers of an alg 2 signature (spec §29.10). */
export const MAX_SIGNERS = 16 ;
const MAX_AUTHOR_LEN = 256 ;
/** A signer of an alg 2 signature as a reader shows it (spec §29.7, §29.10). */
export interface SignerLine {
/** The name of the certificate as §29.7 shows it, or the SHA-256 of the certificate in hexadecimal when it does not meet the rules of the declared author. */
readonly holder : string ;
CMS as Go reads it: the issuer by its Name, uncompressed keys, the BOM, linear readers
Fixes T1, T3, T13 and the CMS part of T5 of the review of the session of
1 and 2 October (docs, spec_v0.11/revision_sesion_1_2_octubre.md):
- securitycms.ts: an issuer that breaks the rules of the declared author
shows the SHA-256 of the DER of its Name, Go's sha256.Sum256(RawIssuer),
and no longer that of the certificate (cms.certIssuerHash).
- cms.ts: an ECDSA key counts only with its point uncompressed, 0x04 and
the two coordinates, the only form that Go's x509.ParsePKIXPublicKey
reads: a compressed one makes a signer not verifiable (F5) and a seal S1.
- cms.ts: a UTF8String and the times of a certificate and of a token keep a
leading U+FEFF, as Go reads the bytes: such a name shows the hash, and
such a time breaks the profile (F1, S2).
- cms.ts: oidOf and intOf take time linear in the length of the element.
An arc of up to seven digits accumulates in a number, a longer one and
every INTEGER are read whole from hexadecimal, never by a shift per byte,
which took some 700 ms for 60 KB; attributes of one type are appended,
not copied.
- security.ts: evaluateSecurity never throws. A fault while it evaluates
the signature gives F1, one while it evaluates the seal S2, each apart,
and one while it decodes the area X, as the Go reference will from v0.12.
- Tests: securitycms.test.ts and security.failure.test.ts are new.
cms.test.ts now refuses a second content-type and two signature-time-
stamps for the rule of the count, with every SET OF in DER order, and
der.test.ts tests the depth at its boundary. cmsbuild.ts makes names,
validities and compressed points of its own.
capsule.EvaluateSecurityIn of the Go reference at spec-v0.11 gives the same
verdicts, signer lines and Spanish lines on 25 areas made with cmsbuild.ts:
issuers with ESC, U+202E, empty, of 300 bytes or with a leading U+FEFF;
names and times with a leading U+FEFF; and compressed keys on P-256, P-384
and P-521, as signers and as authorities of a seal. HEAD gave other ones in
19 of them. npm run verify passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
/** The issuer that the certificate says, with the same rules, and the SHA-256 of the DER of its Name when it does not meet them. */
readonly issuer : string ;
/** 'valid', 'invalid', 'absent', 'not verifiable', 'without seal', 'invalid seal' or 'out of validity'. */
readonly result : string ;
/** t, undefined without a seal that verifies. */
readonly sealTime? : Instant ;
/** Whether t plus the accuracy of the seal is before round_time. */
readonly before : boolean ;
}
/** What the texts of F6, S4 and S5 name (spec §29.7, §29.10). */
export interface Detail {
/** The required signers, in the order of SIGNERS, and the SignerInfo of other certificates, which never count. */
readonly signers : readonly SignerLine [ ] ;
readonly foreign : readonly SignerLine [ ] ;
/** The holder of the certificate of the authority of a valid seal, as §29.7 writes it, and t. */
readonly sealHolder? : string ;
readonly sealTime? : Instant ;
}
// SIGNERS: a CBOR array of 1 to 16 strings of 32 bytes in strictly ascending order of bytes (spec §29.10). Throws a DateKeysError when it is not.
function decodeSigners ( b : Uint8Array ) : Uint8Array [ ] {
const out : Uint8Array [ ] = [ ] ;
const decode = ( d : Decoder ) : void = > {
const n = d . array ( MAX_SIGNERS ) ;
if ( n < 1 ) throw new DateKeysError ( 'ERR_NON_CANONICAL_CBOR' , 'SIGNERS is empty' ) ;
for ( let i = 0 ; i < n ; i ++ ) {
const h = d . bstr ( 32 , 32 ) ;
const last = out [ out . length - 1 ] ;
if ( last !== undefined && compare ( last , h ) >= 0 ) throw new DateKeysError ( 'ERR_NON_CANONICAL_CBOR' , 'SIGNERS is not in strictly ascending order' ) ;
out . push ( h ) ;
}
} ;
const encode = ( e : Encoder ) : void = > {
e . array ( out . length ) ;
for ( const h of out ) e . bstr ( h ) ;
} ;
unmarshal ( b , decode , encode ) ;
return out ;
}
function compare ( a : Uint8Array , b : Uint8Array ) : number {
for ( let i = 0 ; i < Math . min ( a . length , b . length ) ; i ++ ) if ( a [ i ] !== b [ i ] ) return a [ i ] ! < b [ i ] ! ? - 1 : 1 ;
return a . length - b . length ;
}
// How §29.7 shows a name: the name, when it meets the rules of the declared author, and the SHA-256 otherwise.
function holderText ( name : string , hash : Uint8Array ) : string {
if ( name !== '' && utf8Length ( name ) <= MAX_AUTHOR_LEN ) {
try {
checkAuthor ( name ) ;
return name ;
} catch ( err ) {
/* v8 ignore next -- @preserve: checkAuthor throws only its own error */
if ( ! ( err instanceof DateKeysError || err instanceof Error ) ) throw err ;
}
}
return toHex ( hash ) ;
}
// One SignerInfo as §29.10 orders: not verifiable, invalid, without seal, with an invalid seal, out of validity, or valid.
CMS as Go reads it: the issuer by its Name, uncompressed keys, the BOM, linear readers
Fixes T1, T3, T13 and the CMS part of T5 of the review of the session of
1 and 2 October (docs, spec_v0.11/revision_sesion_1_2_octubre.md):
- securitycms.ts: an issuer that breaks the rules of the declared author
shows the SHA-256 of the DER of its Name, Go's sha256.Sum256(RawIssuer),
and no longer that of the certificate (cms.certIssuerHash).
- cms.ts: an ECDSA key counts only with its point uncompressed, 0x04 and
the two coordinates, the only form that Go's x509.ParsePKIXPublicKey
reads: a compressed one makes a signer not verifiable (F5) and a seal S1.
- cms.ts: a UTF8String and the times of a certificate and of a token keep a
leading U+FEFF, as Go reads the bytes: such a name shows the hash, and
such a time breaks the profile (F1, S2).
- cms.ts: oidOf and intOf take time linear in the length of the element.
An arc of up to seven digits accumulates in a number, a longer one and
every INTEGER are read whole from hexadecimal, never by a shift per byte,
which took some 700 ms for 60 KB; attributes of one type are appended,
not copied.
- security.ts: evaluateSecurity never throws. A fault while it evaluates
the signature gives F1, one while it evaluates the seal S2, each apart,
and one while it decodes the area X, as the Go reference will from v0.12.
- Tests: securitycms.test.ts and security.failure.test.ts are new.
cms.test.ts now refuses a second content-type and two signature-time-
stamps for the rule of the count, with every SET OF in DER order, and
der.test.ts tests the depth at its boundary. cmsbuild.ts makes names,
validities and compressed points of its own.
capsule.EvaluateSecurityIn of the Go reference at spec-v0.11 gives the same
verdicts, signer lines and Spanish lines on 25 areas made with cmsbuild.ts:
issuers with ESC, U+202E, empty, of 300 bytes or with a leading U+FEFF;
names and times with a leading U+FEFF; and compressed keys on P-256, P-384
and P-521, as signers and as authorities of a seal. HEAD gave other ones in
19 of them. npm run verify passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
// The issuer is text of the certificate, as the holder is: an issuer that breaks the rules shows the SHA-256 of its Name,
// so that no escape, no control and no bidirectional character reaches a line of the verdicts.
function signerLine ( s : SignerInfo , msg : Uint8Array , roundTime : Instant | undefined ) : SignerLine {
CMS as Go reads it: the issuer by its Name, uncompressed keys, the BOM, linear readers
Fixes T1, T3, T13 and the CMS part of T5 of the review of the session of
1 and 2 October (docs, spec_v0.11/revision_sesion_1_2_octubre.md):
- securitycms.ts: an issuer that breaks the rules of the declared author
shows the SHA-256 of the DER of its Name, Go's sha256.Sum256(RawIssuer),
and no longer that of the certificate (cms.certIssuerHash).
- cms.ts: an ECDSA key counts only with its point uncompressed, 0x04 and
the two coordinates, the only form that Go's x509.ParsePKIXPublicKey
reads: a compressed one makes a signer not verifiable (F5) and a seal S1.
- cms.ts: a UTF8String and the times of a certificate and of a token keep a
leading U+FEFF, as Go reads the bytes: such a name shows the hash, and
such a time breaks the profile (F1, S2).
- cms.ts: oidOf and intOf take time linear in the length of the element.
An arc of up to seven digits accumulates in a number, a longer one and
every INTEGER are read whole from hexadecimal, never by a shift per byte,
which took some 700 ms for 60 KB; attributes of one type are appended,
not copied.
- security.ts: evaluateSecurity never throws. A fault while it evaluates
the signature gives F1, one while it evaluates the seal S2, each apart,
and one while it decodes the area X, as the Go reference will from v0.12.
- Tests: securitycms.test.ts and security.failure.test.ts are new.
cms.test.ts now refuses a second content-type and two signature-time-
stamps for the rule of the count, with every SET OF in DER order, and
der.test.ts tests the depth at its boundary. cmsbuild.ts makes names,
validities and compressed points of its own.
capsule.EvaluateSecurityIn of the Go reference at spec-v0.11 gives the same
verdicts, signer lines and Spanish lines on 25 areas made with cmsbuild.ts:
issuers with ESC, U+202E, empty, of 300 bytes or with a leading U+FEFF;
names and times with a leading U+FEFF; and compressed keys on P-256, P-384
and P-521, as signers and as authorities of a seal. HEAD gave other ones in
19 of them. npm run verify passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
const base = { holder : holderText ( certHolder ( s . cert ) , s . cert . hash ) , issuer : holderText ( certIssuerName ( s . cert ) , certIssuerHash ( s . cert ) ) } ;
const r = checkSigner ( s , msg ) ;
if ( r === 'not verifiable' ) return { . . . base , result : 'not verifiable' , before : false } ;
if ( r === 'invalid' ) return { . . . base , result : 'invalid' , before : false } ;
if ( s . token === undefined ) return { . . . base , result : 'without seal' , before : false } ;
let ok = false ;
let tok ;
try {
tok = parseToken ( s . token ) ;
ok = checkToken ( tok , s . signature ) ;
} catch ( err ) {
if ( ! ( err instanceof CmsFormError || err instanceof CmsAlgorithmError ) ) throw err ;
}
if ( ! ok || tok === undefined ) return { . . . base , result : 'invalid seal' , before : false } ;
if ( ! certValidAt ( s . cert , tok . genTime ) ) return { . . . base , result : 'out of validity' , before : false } ;
return { . . . base , result : 'valid' , sealTime : tok.genTime , before : roundTime !== undefined && compareInstants ( addInstants ( tok . genTime , tok . accuracy ) , roundTime ) < 0 } ;
}
/ * *
* The verdict of a signature of alg 2 ( spec § 29.10 ) : undefined for F1 ( content
* that breaks its profile ) , F2 when the signature of a required signer is
* invalid , F5 when something the capsule demands is missing , F6 when every
* required signer is valid and sealed . ` signers ` and ` value ` are keys 1 and 2
* of the author - signature ; ` hasSeal ` is whether key 3 exists , which an alg 2
* signature forbids .
* /
export function evaluateCMS (
signers : Uint8Array ,
value : Uint8Array ,
hasSeal : boolean ,
controlCommit : Uint8Array ,
headDigest : Uint8Array ,
roundTime : Instant | undefined ,
) : { signature : 'F2' | 'F5' | 'F6' ; detail : Detail } | undefined {
let required : Uint8Array [ ] ;
let sd ;
try {
required = decodeSigners ( signers ) ;
sd = parseSignature ( value ) ;
} catch ( err ) {
if ( ! ( err instanceof DateKeysError || err instanceof CmsFormError ) ) throw err ;
return undefined ;
}
const msg = authorMessage ( controlCommit , headDigest , signersDigest ( ALG_CMS , signers ) ) ;
const byHash = new Map < string , SignerInfo > ( sd . signers . map ( ( s ) = > [ toHex ( s . cert . hash ) , s ] ) ) ;
let invalid = false ;
let incomplete = hasSeal ;
const lines : SignerLine [ ] = [ ] ;
for ( const h of required ) {
const s = byHash . get ( toHex ( h ) ) ;
if ( s === undefined ) {
lines . push ( { holder : toHex ( h ) , issuer : '' , result : 'absent' , before : false } ) ;
incomplete = true ;
continue ;
}
const line = signerLine ( s , msg , roundTime ) ;
if ( line . result === 'invalid' ) invalid = true ;
else if ( line . result !== 'valid' ) incomplete = true ;
lines . push ( line ) ;
}
const foreign = sd . signers . filter ( ( s ) = > ! required . some ( ( h ) = > equalBytes ( h , s . cert . hash ) ) ) . map ( ( s ) = > signerLine ( s , msg , roundTime ) ) ;
return { signature : invalid ? 'F2' : incomplete ? 'F5' : 'F6' , detail : { signers : lines , foreign } } ;
}
/ * *
* The verdict of a seal of seal_type 2 ( spec § 29.11 ) : S2 or S1 for the form and
* the algorithms , S3 when it does not verify , and S4 or S5 when it does , with
* the authority and t . ` signature ` is the content of key 2 , undefined without it .
* /
export function evaluateSeal (
token : Uint8Array ,
signature : Uint8Array | undefined ,
controlCommit : Uint8Array ,
headDigest : Uint8Array ,
roundTime : Instant | undefined ,
) : { seal : 'S1' | 'S2' | 'S3' | 'S4' | 'S5' ; sealHolder? : string ; sealTime? : Instant } {
let tok ;
try {
tok = parseToken ( token ) ;
} catch ( err ) {
if ( err instanceof CmsFormError ) return { seal : 'S2' } ;
if ( err instanceof CmsAlgorithmError ) return { seal : 'S1' } ;
throw err ;
}
if ( ! tokenImprintIsSHA256 ( tok ) ) return { seal : 'S1' } ;
if ( ! checkToken ( tok , sealSubject ( controlCommit , headDigest , signature ) ) ) return { seal : 'S3' } ;
const sealHolder = holderText ( certHolder ( tok . tsa ) , tok . tsa . hash ) ;
const before = roundTime !== undefined && compareInstants ( addInstants ( tok . genTime , tok . accuracy ) , roundTime ) < 0 ;
return { seal : before ? 'S4' : 'S5' , sealHolder , sealTime : tok.genTime } ;
}